Compare commits
283 Commits
10.0.0-nex
...
9.1.4
Author | SHA1 | Date | |
---|---|---|---|
9b961a410a | |||
420c73c722 | |||
62930aac7b | |||
d7433316b0 | |||
c440165384 | |||
0ce96f1d78 | |||
d3deaf9a99 | |||
5c88a9fcfe | |||
0bd7517c73 | |||
d669429bd2 | |||
6a9b2c9a67 | |||
0f389fa5ae | |||
2a27f69522 | |||
180a32894e | |||
00724dcdbb | |||
cc71116ba6 | |||
ddfcf082ca | |||
c7e4f5eb4d | |||
b3fe9017f7 | |||
d3a77ea91a | |||
1b5a931d63 | |||
d840503d35 | |||
b49a734f0d | |||
695f83529d | |||
f898c9ab57 | |||
5337e138e3 | |||
e33047454f | |||
858fa45556 | |||
b97211ee2b | |||
25d4238371 | |||
7743c43529 | |||
54883cb477 | |||
91cef8cfc7 | |||
d40bcce84e | |||
7a18fb2448 | |||
ec39bdcc15 | |||
b7070b0ad6 | |||
aa94cd505c | |||
cc6ccf28a6 | |||
e1071615c6 | |||
8bd5374cfd | |||
b9b9cc2ba8 | |||
a6e10ef869 | |||
9724169bf4 | |||
c0ed57db76 | |||
0bd50e2e50 | |||
0ceb27041f | |||
ec2affe104 | |||
c590e8ca7a | |||
254b9ea44c | |||
2a53f47159 | |||
722d9397b0 | |||
03de31a78e | |||
b22c5a953d | |||
24222e0c1f | |||
95f45e8070 | |||
18be33a9d1 | |||
a22d4f6c98 | |||
5ae8473c6b | |||
fd7c39e3cf | |||
d85d91df66 | |||
15930d21c7 | |||
61a7f98b98 | |||
c3c7bf6509 | |||
b2e7ce47ec | |||
94e518e3c7 | |||
0fa5ac8d0d | |||
f2fca3e243 | |||
5bab49828d | |||
db4e93d0ca | |||
479a59be43 | |||
52aab63dd9 | |||
506beeddc1 | |||
0075078179 | |||
bb7edc52aa | |||
ed2b0e945e | |||
da159bde83 | |||
06a9809e32 | |||
1e4fb74ec8 | |||
797c306306 | |||
972fc06135 | |||
a9117061d0 | |||
fe1d9bacc3 | |||
08b8b51486 | |||
1d4af3f734 | |||
609d81c65e | |||
af30efddc5 | |||
15115f6179 | |||
eec9b6bbb5 | |||
45fd77ead1 | |||
f16587e9b7 | |||
4f9991534e | |||
51a0ed2222 | |||
a5ea100e7c | |||
0429c7f5e9 | |||
1756cced4a | |||
793a001d7c | |||
5c3774cfe6 | |||
12266b2042 | |||
e385abc83c | |||
933cbfb828 | |||
c5e725111d | |||
93993864b1 | |||
26f49151e7 | |||
4b3f9ac739 | |||
80604d3a76 | |||
e615a10371 | |||
c8f2ca2349 | |||
a67afcc932 | |||
fd7698253e | |||
32de025dce | |||
a4e1768a74 | |||
b336871303 | |||
92fa6399f7 | |||
3992341d34 | |||
8c559ef104 | |||
0bac2b062c | |||
58d028178f | |||
e06512b22f | |||
603b0944d5 | |||
918e628f9b | |||
468cf69c55 | |||
c8f3fa9f3e | |||
78136cc3a7 | |||
66724fd159 | |||
8e7f9033a3 | |||
c8f9092364 | |||
bc995835b9 | |||
1bfa908ab3 | |||
2479f7d7ef | |||
333b8679ad | |||
5da621d3dd | |||
cbed582a1a | |||
d5aa6b5bd6 | |||
33eee43263 | |||
72053b0f27 | |||
d20ef47b16 | |||
bdc05aef64 | |||
798d959bee | |||
3570aaa363 | |||
421b6a97d6 | |||
b28a5f6eef | |||
52ab9397a0 | |||
b99e539b0e | |||
fd2ef74a31 | |||
cbc25bbf31 | |||
7385b2cf25 | |||
0daa48800e | |||
ffa4e11db1 | |||
41a41e741d | |||
c5384aee58 | |||
b02c950aac | |||
2eaa304aba | |||
9faf2e4a96 | |||
0e7a89a391 | |||
af4269471d | |||
915a6c17b9 | |||
93b32d3023 | |||
bd82b34f79 | |||
c626e90ab0 | |||
c6bbb0d8e5 | |||
f48a065db0 | |||
aeb6d0d0f7 | |||
655c9e386b | |||
2ad20b953f | |||
7059eaf2d2 | |||
939fda410f | |||
35ab6acab3 | |||
f4ee067cb8 | |||
068e28381d | |||
dff52ecb11 | |||
56af303dc3 | |||
136596ddb4 | |||
e515c913d9 | |||
31eaf78035 | |||
b0d680daa0 | |||
93cbef26c7 | |||
e2b221b8fa | |||
7a33c456d3 | |||
61db817eed | |||
4894220acf | |||
74b7a8eaf5 | |||
bfa55162de | |||
776f991c1a | |||
d9d1a1e682 | |||
867ff14a4a | |||
879457ca05 | |||
efbe4ad28a | |||
64f72c0600 | |||
81c05bfa01 | |||
56abcf088d | |||
6818432cd0 | |||
79bfb037df | |||
0fce20d79d | |||
9f486c31d2 | |||
d36066dd6c | |||
a631b99c69 | |||
78211c42ea | |||
5ff5a11509 | |||
e80047e897 | |||
eee5eeae76 | |||
c3ce1903b6 | |||
21da0346c7 | |||
af2d74fde9 | |||
8de62a9811 | |||
26e28453d6 | |||
8ea30b662f | |||
cb0a2a055d | |||
14ae3c0a21 | |||
90cae34c05 | |||
7bb35889fa | |||
03a3c753bb | |||
74b1464dff | |||
ff523c9ec9 | |||
392ef93c2b | |||
29dcb5bf68 | |||
de594ca221 | |||
3545a49a79 | |||
f4681b6e40 | |||
fb16557381 | |||
61c9da5542 | |||
dbb1eb0ffe | |||
002b81c0b0 | |||
beb3f19dc0 | |||
e8f3c47b03 | |||
c58e6ba13a | |||
61e5ab4703 | |||
10385c27da | |||
0ab244f39e | |||
eafa5260db | |||
15cffa210c | |||
df8e45a193 | |||
bb8744db94 | |||
43bad87ae1 | |||
c3b297a318 | |||
6ea232eb3c | |||
0baab0f3db | |||
e53b686375 | |||
5c28af0e74 | |||
60adc35d30 | |||
024cc3b84a | |||
ac17142001 | |||
8d7066223f | |||
0cb3c04128 | |||
2649794e65 | |||
bf426c5f0d | |||
aa6b9f0382 | |||
822b420a11 | |||
294c6297d7 | |||
58fc65d198 | |||
b6bd8d7572 | |||
b08168bb90 | |||
407fa42679 | |||
aef432384a | |||
fb70083339 | |||
c9c2408176 | |||
e066bddfe9 | |||
447a600477 | |||
70f9bfff43 | |||
57c02b044c | |||
6defe962c8 | |||
267bcb3e9c | |||
b0b66881b4 | |||
9ff8d78bcd | |||
563b707497 | |||
5357e643b3 | |||
f71d132f7c | |||
ba3edda230 | |||
0767d37c07 | |||
8ba24578bc | |||
133a97ad67 | |||
4e67a3ab3f | |||
377f0010fc | |||
6e09129e4c | |||
d80e51a6b1 | |||
feb66b00da | |||
cb19eac105 | |||
6e0564ade6 | |||
05eeb7d279 | |||
2ce5fa3cce | |||
e140cdcb34 | |||
14b2db1d43 | |||
2afc7e982e |
@ -236,7 +236,7 @@ jobs:
|
|||||||
git config user.name "angular-ci"
|
git config user.name "angular-ci"
|
||||||
git config user.email "angular-ci"
|
git config user.email "angular-ci"
|
||||||
# Rebase PR on top of target branch.
|
# Rebase PR on top of target branch.
|
||||||
node tools/rebase-pr.js angular/angular ${CIRCLE_PR_NUMBER}
|
node tools/rebase-pr.js
|
||||||
else
|
else
|
||||||
echo "This build is not over a PR, nothing to do."
|
echo "This build is not over a PR, nothing to do."
|
||||||
fi
|
fi
|
||||||
@ -278,7 +278,8 @@ jobs:
|
|||||||
- run: 'yarn bazel:lint ||
|
- run: 'yarn bazel:lint ||
|
||||||
(echo -e "\n.bzl files have lint errors. Please run ''yarn bazel:lint-fix''"; exit 1)'
|
(echo -e "\n.bzl files have lint errors. Please run ''yarn bazel:lint-fix''"; exit 1)'
|
||||||
|
|
||||||
- run: yarn -s lint --branch $CI_GIT_BASE_REVISION
|
- run: yarn -s tslint
|
||||||
|
- run: yarn -s ng-dev format changed $CI_GIT_BASE_REVISION --check
|
||||||
- run: yarn -s ts-circular-deps:check
|
- run: yarn -s ts-circular-deps:check
|
||||||
- run: yarn -s ng-dev pullapprove verify
|
- run: yarn -s ng-dev pullapprove verify
|
||||||
- run: yarn -s ng-dev commit-message validate-range --range $CI_COMMIT_RANGE
|
- run: yarn -s ng-dev commit-message validate-range --range $CI_COMMIT_RANGE
|
||||||
|
@ -22,6 +22,7 @@ else
|
|||||||
####################################################################################################
|
####################################################################################################
|
||||||
# See https://circleci.com/docs/2.0/env-vars/#built-in-environment-variables for more info.
|
# See https://circleci.com/docs/2.0/env-vars/#built-in-environment-variables for more info.
|
||||||
####################################################################################################
|
####################################################################################################
|
||||||
|
setPublicVar CI "$CI"
|
||||||
setPublicVar PROJECT_ROOT "$projectDir";
|
setPublicVar PROJECT_ROOT "$projectDir";
|
||||||
setPublicVar CI_AIO_MIN_PWA_SCORE "95";
|
setPublicVar CI_AIO_MIN_PWA_SCORE "95";
|
||||||
# This is the branch being built; e.g. `pull/12345` for PR builds.
|
# This is the branch being built; e.g. `pull/12345` for PR builds.
|
||||||
@ -36,9 +37,8 @@ else
|
|||||||
setPublicVar CI_PULL_REQUEST "${CIRCLE_PR_NUMBER:-false}";
|
setPublicVar CI_PULL_REQUEST "${CIRCLE_PR_NUMBER:-false}";
|
||||||
setPublicVar CI_REPO_NAME "$CIRCLE_PROJECT_REPONAME";
|
setPublicVar CI_REPO_NAME "$CIRCLE_PROJECT_REPONAME";
|
||||||
setPublicVar CI_REPO_OWNER "$CIRCLE_PROJECT_USERNAME";
|
setPublicVar CI_REPO_OWNER "$CIRCLE_PROJECT_USERNAME";
|
||||||
|
setPublicVar CI_PR_REPONAME "$CIRCLE_PR_REPONAME";
|
||||||
# Store a PR's refs and shas so they don't need to be requested multiple times.
|
setPublicVar CI_PR_USERNAME "$CIRCLE_PR_USERNAME";
|
||||||
setPublicVar GITHUB_REFS_AND_SHAS $(node tools/utils/get-refs-and-shas-for-target.js ${CIRCLE_PR_NUMBER:-false} | awk '{ gsub(/"/,"\\\"") } 1');
|
|
||||||
|
|
||||||
|
|
||||||
####################################################################################################
|
####################################################################################################
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"commitMessage": {
|
"commitMessage": {
|
||||||
"maxLength": 120,
|
"maxLength": 120,
|
||||||
"minBodyLength": 0,
|
"minBodyLength": 100,
|
||||||
"types": [
|
"types": [
|
||||||
"build",
|
"build",
|
||||||
"ci",
|
"ci",
|
||||||
@ -43,5 +43,27 @@
|
|||||||
"ve",
|
"ve",
|
||||||
"zone.js"
|
"zone.js"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"format": {
|
||||||
|
"matchers": [
|
||||||
|
"dev-infra/**/*.{js,ts}",
|
||||||
|
"packages/**/*.{js,ts}",
|
||||||
|
"!packages/zone.js",
|
||||||
|
"!packages/common/locales/**/*.{js,ts}",
|
||||||
|
"!packages/common/src/i18n/available_locales.ts",
|
||||||
|
"!packages/common/src/i18n/currencies.ts",
|
||||||
|
"!packages/common/src/i18n/locale_en.ts",
|
||||||
|
"modules/benchmarks/**/*.{js,ts}",
|
||||||
|
"modules/playground/**/*.{js,ts}",
|
||||||
|
"tools/**/*.{js,ts}",
|
||||||
|
"!tools/gulp-tasks/cldr/extract.js",
|
||||||
|
"!tools/public_api_guard/**/*.d.ts",
|
||||||
|
"!tools/ts-api-guardian/test/fixtures/**",
|
||||||
|
"./*.{js,ts}",
|
||||||
|
"!**/node_modules/**",
|
||||||
|
"!**/dist/**",
|
||||||
|
"!**/built/**",
|
||||||
|
"!shims_for_IE.js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -189,7 +189,7 @@ groups:
|
|||||||
- *can-be-global-approved
|
- *can-be-global-approved
|
||||||
- *can-be-global-docs-approved
|
- *can-be-global-docs-approved
|
||||||
- >
|
- >
|
||||||
contains_any_globs(files, [
|
contains_any_globs(files.exclude('packages/compiler-cli/ngcc/**'), [
|
||||||
'packages/compiler/**',
|
'packages/compiler/**',
|
||||||
'packages/examples/compiler/**',
|
'packages/examples/compiler/**',
|
||||||
'packages/compiler-cli/**',
|
'packages/compiler-cli/**',
|
||||||
@ -198,10 +198,6 @@ groups:
|
|||||||
'aio/content/guide/aot-metadata-errors.md',
|
'aio/content/guide/aot-metadata-errors.md',
|
||||||
'aio/content/guide/template-typecheck.md '
|
'aio/content/guide/template-typecheck.md '
|
||||||
])
|
])
|
||||||
- >
|
|
||||||
not contains_any_globs(files, [
|
|
||||||
'packages/compiler-cli/ngcc/**'
|
|
||||||
])
|
|
||||||
reviewers:
|
reviewers:
|
||||||
users:
|
users:
|
||||||
- alxhub
|
- alxhub
|
||||||
@ -217,10 +213,7 @@ groups:
|
|||||||
conditions:
|
conditions:
|
||||||
- *can-be-global-approved
|
- *can-be-global-approved
|
||||||
- *can-be-global-docs-approved
|
- *can-be-global-docs-approved
|
||||||
- >
|
- files.include('packages/compiler-cli/ngcc/**')
|
||||||
contains_any_globs(files, [
|
|
||||||
'packages/compiler-cli/ngcc/**'
|
|
||||||
])
|
|
||||||
reviewers:
|
reviewers:
|
||||||
users:
|
users:
|
||||||
- alxhub
|
- alxhub
|
||||||
@ -229,6 +222,22 @@ groups:
|
|||||||
- petebacondarwin
|
- petebacondarwin
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# Framework: Migrations
|
||||||
|
# =========================================================
|
||||||
|
fw-migrations:
|
||||||
|
conditions:
|
||||||
|
- *can-be-global-approved
|
||||||
|
- *can-be-global-docs-approved
|
||||||
|
- files.include("packages/core/schematics/**")
|
||||||
|
reviewers:
|
||||||
|
users:
|
||||||
|
- alxhub
|
||||||
|
- crisbeto
|
||||||
|
- devversion
|
||||||
|
- kara
|
||||||
|
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# Framework: Core
|
# Framework: Core
|
||||||
# =========================================================
|
# =========================================================
|
||||||
@ -237,7 +246,7 @@ groups:
|
|||||||
- *can-be-global-approved
|
- *can-be-global-approved
|
||||||
- *can-be-global-docs-approved
|
- *can-be-global-docs-approved
|
||||||
- >
|
- >
|
||||||
contains_any_globs(files, [
|
contains_any_globs(files.exclude("packages/core/schematics/**"), [
|
||||||
'packages/core/**',
|
'packages/core/**',
|
||||||
'packages/examples/core/**',
|
'packages/examples/core/**',
|
||||||
'packages/common/**',
|
'packages/common/**',
|
||||||
@ -566,6 +575,7 @@ groups:
|
|||||||
])
|
])
|
||||||
reviewers:
|
reviewers:
|
||||||
users:
|
users:
|
||||||
|
- AndrewKushnir
|
||||||
- IgorMinar
|
- IgorMinar
|
||||||
- kara
|
- kara
|
||||||
- pkozlowski-opensource
|
- pkozlowski-opensource
|
||||||
@ -848,7 +858,7 @@ groups:
|
|||||||
'aio/content/images/guide/deployment/**',
|
'aio/content/images/guide/deployment/**',
|
||||||
'aio/content/guide/file-structure.md',
|
'aio/content/guide/file-structure.md',
|
||||||
'aio/content/guide/ivy.md',
|
'aio/content/guide/ivy.md',
|
||||||
'aio/content/guide/web-worker.md'
|
'aio/content/guide/web-worker.md',
|
||||||
'aio/content/guide/workspace-config.md',
|
'aio/content/guide/workspace-config.md',
|
||||||
])
|
])
|
||||||
reviewers:
|
reviewers:
|
||||||
@ -1027,8 +1037,7 @@ groups:
|
|||||||
- *can-be-global-approved
|
- *can-be-global-approved
|
||||||
- >
|
- >
|
||||||
contains_any_globs(files, [
|
contains_any_globs(files, [
|
||||||
'aio/scripts/_payload-limits.json',
|
'goldens/size-tracking/**'
|
||||||
'integration/_payload-limits.json'
|
|
||||||
])
|
])
|
||||||
reviewers:
|
reviewers:
|
||||||
users:
|
users:
|
||||||
@ -1044,7 +1053,7 @@ groups:
|
|||||||
- *can-be-global-approved
|
- *can-be-global-approved
|
||||||
- >
|
- >
|
||||||
contains_any_globs(files, [
|
contains_any_globs(files, [
|
||||||
'goldens/packages-circular-deps.json'
|
'goldens/circular-deps/packages.json'
|
||||||
])
|
])
|
||||||
reviewers:
|
reviewers:
|
||||||
users:
|
users:
|
||||||
|
115
CHANGELOG.md
115
CHANGELOG.md
@ -1,26 +1,33 @@
|
|||||||
<a name="10.0.0-next.2"></a>
|
<a name="9.1.4"></a>
|
||||||
# [10.0.0-next.2](https://github.com/angular/angular/compare/10.0.0-next.1...10.0.0-next.2) (2020-04-15)
|
## [9.1.4](https://github.com/angular/angular/compare/9.1.3...9.1.4) (2020-04-29)
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
* **common:** `locales/global/*.js` are not ES5 compliant ([#36342](https://github.com/angular/angular/issues/36342)) ([078b0be](https://github.com/angular/angular/commit/078b0be)), closes [angular/angular-cli#16394](https://github.com/angular/angular-cli/issues/16394)
|
* **core:** attempt to recover from user errors during creation ([#36381](https://github.com/angular/angular/issues/36381)) ([d743331](https://github.com/angular/angular/commit/d743331)), closes [#31221](https://github.com/angular/angular/issues/31221)
|
||||||
* **compiler:** handle type references to namespaced symbols correctly ([#36106](https://github.com/angular/angular/issues/36106)) ([4aa4e6f](https://github.com/angular/angular/commit/4aa4e6f)), closes [#36006](https://github.com/angular/angular/issues/36006)
|
* **core:** handle synthetic props in Directive host bindings correctly ([#35568](https://github.com/angular/angular/issues/35568)) ([0f389fa](https://github.com/angular/angular/commit/0f389fa)), closes [#35501](https://github.com/angular/angular/issues/35501)
|
||||||
* **core:** undecorated-classes-with-decorated-fields migration should avoid error if base class has no value declaration ([#36543](https://github.com/angular/angular/issues/36543)) ([ca67748](https://github.com/angular/angular/commit/ca67748)), closes [#36522](https://github.com/angular/angular/issues/36522)
|
* **language-service:** disable update the `[@angular](https://github.com/angular)/core` module ([#36783](https://github.com/angular/angular/issues/36783)) ([d3a77ea](https://github.com/angular/angular/commit/d3a77ea))
|
||||||
* **ngcc:** correctly detect external files from nested `node_modules/` ([#36559](https://github.com/angular/angular/issues/36559)) ([6ab43d7](https://github.com/angular/angular/commit/6ab43d7)), closes [#36526](https://github.com/angular/angular/issues/36526)
|
* **localize:** include legacy ids when describing messages ([#36761](https://github.com/angular/angular/issues/36761)) ([aa94cd5](https://github.com/angular/angular/commit/aa94cd5))
|
||||||
* **ngcc:** display output from the unlocker process on Windows ([#36569](https://github.com/angular/angular/issues/36569)) ([e041ac6](https://github.com/angular/angular/commit/e041ac6))
|
* **ngcc:** recognize enum declarations emitted in JavaScript ([#36550](https://github.com/angular/angular/issues/36550)) ([c440165](https://github.com/angular/angular/commit/c440165)), closes [#35584](https://github.com/angular/angular/issues/35584)
|
||||||
* **ngcc:** do not spawn unlocker processes on cluster workers ([#36569](https://github.com/angular/angular/issues/36569)) ([66effde](https://github.com/angular/angular/commit/66effde)), closes [#35861](https://github.com/angular/angular/issues/35861)
|
|
||||||
* **ngcc:** do not warn if `paths` mapping does not exist ([#36525](https://github.com/angular/angular/issues/36525)) ([717df13](https://github.com/angular/angular/commit/717df13)), closes [#36518](https://github.com/angular/angular/issues/36518)
|
|
||||||
* **ngcc:** force ngcc to exit on error ([#36622](https://github.com/angular/angular/issues/36622)) ([663b768](https://github.com/angular/angular/commit/663b768)), closes [#36616](https://github.com/angular/angular/issues/36616)
|
|
||||||
* **router:** pass correct component to canDeactivate checks when using two or more sibling router-outlets ([#36302](https://github.com/angular/angular/issues/36302)) ([80e6c07](https://github.com/angular/angular/commit/80e6c07)), closes [#34614](https://github.com/angular/angular/issues/34614)
|
|
||||||
* **upgrade:** update $locationShim to handle Location changes before initialization ([#36498](https://github.com/angular/angular/issues/36498)) ([0cc53fb](https://github.com/angular/angular/commit/0cc53fb)), closes [#36492](https://github.com/angular/angular/issues/36492)
|
|
||||||
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* **ngcc:** only load if it is needed ([#36486](https://github.com/angular/angular/issues/36486)) ([3bedfda](https://github.com/angular/angular/commit/3bedfda))
|
<a name="9.1.3"></a>
|
||||||
* **ngcc:** read dependencies from entry-point manifest ([#36486](https://github.com/angular/angular/issues/36486)) ([a185efb](https://github.com/angular/angular/commit/a185efb)), closes [#issuecomment-608401834](https://github.com/angular/angular/issues/issuecomment-608401834)
|
## [9.1.3](https://github.com/angular/angular/compare/9.1.2...9.1.3) (2020-04-22)
|
||||||
* **ngcc:** reduce the size of the entry-point manifest file ([#36486](https://github.com/angular/angular/issues/36486)) ([ec0ce60](https://github.com/angular/angular/commit/ec0ce60))
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **compiler:** avoid generating i18n attributes in plain form ([#36422](https://github.com/angular/angular/issues/36422)) ([08b8b51](https://github.com/angular/angular/commit/08b8b51))
|
||||||
|
* **core:** do not use unbound attributes as inputs to structural directives ([#36441](https://github.com/angular/angular/issues/36441)) ([c0ed57d](https://github.com/angular/angular/commit/c0ed57d))
|
||||||
|
* **core:** handle empty translations correctly ([#36499](https://github.com/angular/angular/issues/36499)) ([a5ea100](https://github.com/angular/angular/commit/a5ea100)), closes [#36476](https://github.com/angular/angular/issues/36476)
|
||||||
|
* **core:** missing-injectable migration should not migrate `@NgModule` classes ([#36369](https://github.com/angular/angular/issues/36369)) ([0bd50e2](https://github.com/angular/angular/commit/0bd50e2)), closes [#35700](https://github.com/angular/angular/issues/35700)
|
||||||
|
* **core:** pipes injecting viewProviders when used on a component host node ([#36512](https://github.com/angular/angular/issues/36512)) ([5ae8473](https://github.com/angular/angular/commit/5ae8473)), closes [#36146](https://github.com/angular/angular/issues/36146)
|
||||||
|
* **core:** prevent unknown property check for AOT-compiled components ([#36072](https://github.com/angular/angular/issues/36072)) ([fe1d9ba](https://github.com/angular/angular/commit/fe1d9ba)), closes [#35945](https://github.com/angular/angular/issues/35945)
|
||||||
|
* **core:** properly identify modules affected by overrides in TestBed ([#36649](https://github.com/angular/angular/issues/36649)) ([9724169](https://github.com/angular/angular/commit/9724169)), closes [#36619](https://github.com/angular/angular/issues/36619)
|
||||||
|
* **language-service:** properly evaluate types in comparable expressions ([#36529](https://github.com/angular/angular/issues/36529)) ([5bab498](https://github.com/angular/angular/commit/5bab498))
|
||||||
|
* **ngcc:** display unlocker process output in sync mode ([#36637](https://github.com/angular/angular/issues/36637)) ([da159bd](https://github.com/angular/angular/commit/da159bd)), closes [/github.com/nodejs/node/issues/3596#issuecomment-250890218](https://github.com//github.com/nodejs/node/issues/3596/issues/issuecomment-250890218)
|
||||||
|
* **ngcc:** do not use cached file-system ([#36687](https://github.com/angular/angular/issues/36687)) ([18be33a](https://github.com/angular/angular/commit/18be33a)), closes [/github.com/angular/angular-cli/issues/16860#issuecomment-614694269](https://github.com//github.com/angular/angular-cli/issues/16860/issues/issuecomment-614694269)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -36,79 +43,18 @@
|
|||||||
* **ngcc:** display output from the unlocker process on Windows ([#36569](https://github.com/angular/angular/issues/36569)) ([12266b2](https://github.com/angular/angular/commit/12266b2))
|
* **ngcc:** display output from the unlocker process on Windows ([#36569](https://github.com/angular/angular/issues/36569)) ([12266b2](https://github.com/angular/angular/commit/12266b2))
|
||||||
* **ngcc:** do not spawn unlocker processes on cluster workers ([#36569](https://github.com/angular/angular/issues/36569)) ([e385abc](https://github.com/angular/angular/commit/e385abc)), closes [#35861](https://github.com/angular/angular/issues/35861)
|
* **ngcc:** do not spawn unlocker processes on cluster workers ([#36569](https://github.com/angular/angular/issues/36569)) ([e385abc](https://github.com/angular/angular/commit/e385abc)), closes [#35861](https://github.com/angular/angular/issues/35861)
|
||||||
* **ngcc:** do not warn if `paths` mapping does not exist ([#36525](https://github.com/angular/angular/issues/36525)) ([33eee43](https://github.com/angular/angular/commit/33eee43)), closes [#36518](https://github.com/angular/angular/issues/36518)
|
* **ngcc:** do not warn if `paths` mapping does not exist ([#36525](https://github.com/angular/angular/issues/36525)) ([33eee43](https://github.com/angular/angular/commit/33eee43)), closes [#36518](https://github.com/angular/angular/issues/36518)
|
||||||
* **ngcc:** force ngcc to exit on error ([#36622](https://github.com/angular/angular/issues/36622)) ([933cbfb](https://github.com/angular/angular/commit/933cbfb)), closes [#36616](https://github.com/angular/angular/issues/36616) * **router:** pass correct component to canDeactivate checks when using two or more sibling router-outlets ([#36302](https://github.com/angular/angular/issues/36302)) ([8e7f903](https://github.com/angular/angular/commit/8e7f903)), closes [#34614](https://github.com/angular/angular/issues/34614) * **upgrade:** update $locationShim to handle Location changes before initialization ([#36498](https://github.com/angular/angular/issues/36498)) ([a67afcc](https://github.com/angular/angular/commit/a67afcc)), closes [#36492](https://github.com/angular/angular/issues/36492)
|
* **ngcc:** force ngcc to exit on error ([#36622](https://github.com/angular/angular/issues/36622)) ([933cbfb](https://github.com/angular/angular/commit/933cbfb)), closes [#36616](https://github.com/angular/angular/issues/36616)
|
||||||
|
* **router:** pass correct component to canDeactivate checks when using two or more sibling router-outlets ([#36302](https://github.com/angular/angular/issues/36302)) ([8e7f903](https://github.com/angular/angular/commit/8e7f903)), closes [#34614](https://github.com/angular/angular/issues/34614)
|
||||||
|
* **upgrade:** update $locationShim to handle Location changes before initialization ([#36498](https://github.com/angular/angular/issues/36498)) ([a67afcc](https://github.com/angular/angular/commit/a67afcc)), closes [#36492](https://github.com/angular/angular/issues/36492)
|
||||||
|
|
||||||
|
|
||||||
### Performance Improvements
|
### Performance Improvements
|
||||||
* **ngcc:** only load if it is needed ([#36486](https://github.com/angular/angular/issues/36486)) ([e06512b](https://github.com/angular/angular/commit/e06512b)) * **ngcc:** read dependencies from entry-point manifest ([#36486](https://github.com/angular/angular/issues/36486)) ([918e628](https://github.com/angular/angular/commit/918e628)), closes [#issuecomment-608401834](https://github.com/angular/angular/issues/issuecomment-608401834)
|
|
||||||
|
* **ngcc:** only load if it is needed ([#36486](https://github.com/angular/angular/issues/36486)) ([e06512b](https://github.com/angular/angular/commit/e06512b))
|
||||||
|
* **ngcc:** read dependencies from entry-point manifest ([#36486](https://github.com/angular/angular/issues/36486)) ([918e628](https://github.com/angular/angular/commit/918e628)), closes [#issuecomment-608401834](https://github.com/angular/angular/issues/issuecomment-608401834)
|
||||||
* **ngcc:** reduce the size of the entry-point manifest file ([#36486](https://github.com/angular/angular/issues/36486)) ([603b094](https://github.com/angular/angular/commit/603b094))
|
* **ngcc:** reduce the size of the entry-point manifest file ([#36486](https://github.com/angular/angular/issues/36486)) ([603b094](https://github.com/angular/angular/commit/603b094))
|
||||||
|
|
||||||
|
|
||||||
<a name="10.0.0-next.1"></a>
|
|
||||||
# [10.0.0-next.1](https://github.com/angular/angular/compare/10.0.0-next.0...10.0.0-next.1) (2020-04-08)
|
|
||||||
|
|
||||||
This release contains various API docs improvements.
|
|
||||||
|
|
||||||
<a name="10.0.0-next.0"></a>
|
|
||||||
# [10.0.0-next.0](https://github.com/angular/angular/compare/9.1.0-rc.0...10.0.0-next.0) (2020-04-08)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **common:** let `KeyValuePipe` accept type unions with `null` ([#36093](https://github.com/angular/angular/issues/36093)) ([d783519](https://github.com/angular/angular/commit/d783519)), closes [#35743](https://github.com/angular/angular/issues/35743)
|
|
||||||
* **compiler:** avoid undefined expressions in holey array ([#36343](https://github.com/angular/angular/issues/36343)) ([5516802](https://github.com/angular/angular/commit/5516802))
|
|
||||||
* **compiler:** record correct end of expression ([#34690](https://github.com/angular/angular/issues/34690)) ([df890d7](https://github.com/angular/angular/commit/df890d7)), closes [#33477](https://github.com/angular/angular/issues/33477)
|
|
||||||
* **compiler:** resolve enum values in binary operations ([#36461](https://github.com/angular/angular/issues/36461)) ([64022f5](https://github.com/angular/angular/commit/64022f5)), closes [#35584](https://github.com/angular/angular/issues/35584)
|
|
||||||
* **compiler-cli:** pass real source spans where they are empty ([#31805](https://github.com/angular/angular/issues/31805)) ([e893c5a](https://github.com/angular/angular/commit/e893c5a))
|
|
||||||
* **core:** avoid migration error when non-existent symbol is imported ([#36367](https://github.com/angular/angular/issues/36367)) ([d43c306](https://github.com/angular/angular/commit/d43c306)), closes [#36346](https://github.com/angular/angular/issues/36346)
|
|
||||||
* **core:** ngOnDestroy on multi providers called with incorrect context ([#35840](https://github.com/angular/angular/issues/35840)) ([95fc3d4](https://github.com/angular/angular/commit/95fc3d4)), closes [#35231](https://github.com/angular/angular/issues/35231)
|
|
||||||
* **core:** run `APP_INITIALIZER`s before accessing `LOCALE_ID` token in Ivy TestBed ([#36237](https://github.com/angular/angular/issues/36237)) ([1649743](https://github.com/angular/angular/commit/1649743)), closes [#36230](https://github.com/angular/angular/issues/36230)
|
|
||||||
* **core:** undecorated-classes-with-decorated-fields migration does not decorate derived classes ([#35339](https://github.com/angular/angular/issues/35339)) ([32eafef](https://github.com/angular/angular/commit/32eafef)), closes [#34376](https://github.com/angular/angular/issues/34376)
|
|
||||||
* **core:** workaround Terser inlining bug ([#36200](https://github.com/angular/angular/issues/36200)) ([0ce8ad3](https://github.com/angular/angular/commit/0ce8ad3))
|
|
||||||
* **elements:** correctly handle setting inputs to `undefined` ([#36140](https://github.com/angular/angular/issues/36140)) ([9ba46d9](https://github.com/angular/angular/commit/9ba46d9))
|
|
||||||
* **elements:** correctly set `SimpleChange#firstChange` for pre-existing inputs ([#36140](https://github.com/angular/angular/issues/36140)) ([b14ac96](https://github.com/angular/angular/commit/b14ac96)), closes [#36130](https://github.com/angular/angular/issues/36130)
|
|
||||||
* **language-service:** infer type of elements of array-like objects ([#36312](https://github.com/angular/angular/issues/36312)) ([fe2b692](https://github.com/angular/angular/commit/fe2b692)), closes [#36191](https://github.com/angular/angular/issues/36191)
|
|
||||||
* **language-service:** use the `HtmlAst` to get the span of HTML tag ([#36371](https://github.com/angular/angular/issues/36371)) ([81195a2](https://github.com/angular/angular/commit/81195a2))
|
|
||||||
* **localize:** allow ICU expansion case to start with any character except `}` ([#36123](https://github.com/angular/angular/issues/36123)) ([fced8ee](https://github.com/angular/angular/commit/fced8ee)), closes [#31586](https://github.com/angular/angular/issues/31586)
|
|
||||||
* **ngcc:** add process title ([#36448](https://github.com/angular/angular/issues/36448)) ([76a8cd5](https://github.com/angular/angular/commit/76a8cd5)), closes [/github.com/angular/angular/issues/36414#issuecomment-609644282](https://github.com//github.com/angular/angular/issues/36414/issues/issuecomment-609644282)
|
|
||||||
* **ngcc:** allow ngcc configuration to match pre-release versions of packages ([#36370](https://github.com/angular/angular/issues/36370)) ([326240e](https://github.com/angular/angular/commit/326240e))
|
|
||||||
* **ngcc:** correctly detect imported TypeScript helpers ([#36284](https://github.com/angular/angular/issues/36284)) ([ca25c95](https://github.com/angular/angular/commit/ca25c95)), closes [#36089](https://github.com/angular/angular/issues/36089)
|
|
||||||
* **ngcc:** correctly identify relative Windows-style import paths ([#36372](https://github.com/angular/angular/issues/36372)) ([aecf9de](https://github.com/angular/angular/commit/aecf9de))
|
|
||||||
* **ngcc:** correctly identify the package path of secondary entry-points ([#36249](https://github.com/angular/angular/issues/36249)) ([995cd15](https://github.com/angular/angular/commit/995cd15)), closes [#35747](https://github.com/angular/angular/issues/35747)
|
|
||||||
* **ngcc:** detect non-emitted, non-imported TypeScript helpers ([#36418](https://github.com/angular/angular/issues/36418)) ([5fa7b8b](https://github.com/angular/angular/commit/5fa7b8b))
|
|
||||||
* **ngcc:** do not spawn more processes than intended in parallel mode ([#36280](https://github.com/angular/angular/issues/36280)) ([5cee709](https://github.com/angular/angular/commit/5cee709)), closes [#35719](https://github.com/angular/angular/issues/35719) [#36278](https://github.com/angular/angular/issues/36278) [/github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/main.ts#L429](https://github.com//github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/main.ts/issues/L429) [/github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/execution/cluster/master.ts#L108](https://github.com//github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/execution/cluster/master.ts/issues/L108) [/github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/execution/cluster/master.ts#L110](https://github.com//github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/execution/cluster/master.ts/issues/L110) [/github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/execution/cluster/master.ts#L199](https://github.com//github.com/angular/angular/blob/b8e9a30d3b6/packages/compiler-cli/ngcc/src/execution/cluster/master.ts/issues/L199)
|
|
||||||
* **ngcc:** do not write entry-point manifest outside node_modules ([#36299](https://github.com/angular/angular/issues/36299)) ([c6dd900](https://github.com/angular/angular/commit/c6dd900)), closes [#36296](https://github.com/angular/angular/issues/36296)
|
|
||||||
* **ngcc:** don't crash on cyclic source-map references ([#36452](https://github.com/angular/angular/issues/36452)) ([ee70a18](https://github.com/angular/angular/commit/ee70a18)), closes [#35727](https://github.com/angular/angular/issues/35727) [#35757](https://github.com/angular/angular/issues/35757)
|
|
||||||
* **ngcc:** handle bad path mappings when finding entry-points ([#36331](https://github.com/angular/angular/issues/36331)) ([cc4b813](https://github.com/angular/angular/commit/cc4b813)), closes [#36313](https://github.com/angular/angular/issues/36313) [#36283](https://github.com/angular/angular/issues/36283)
|
|
||||||
* **ngcc:** handle entry-points within container folders ([#36305](https://github.com/angular/angular/issues/36305)) ([38ad1d9](https://github.com/angular/angular/commit/38ad1d9)), closes [#35756](https://github.com/angular/angular/issues/35756) [#36216](https://github.com/angular/angular/issues/36216)
|
|
||||||
* **ngcc:** sniff `main` property for ESM5 format ([#36396](https://github.com/angular/angular/issues/36396)) ([2463548](https://github.com/angular/angular/commit/2463548)), closes [#35788](https://github.com/angular/angular/issues/35788)
|
|
||||||
* **ngcc:** support ignoring deep-imports via package config ([#36423](https://github.com/angular/angular/issues/36423)) ([f9fb833](https://github.com/angular/angular/commit/f9fb833)), closes [#35750](https://github.com/angular/angular/issues/35750)
|
|
||||||
* **ngcc:** support simple `browser` property in entry-points ([#36396](https://github.com/angular/angular/issues/36396)) ([6b3aa60](https://github.com/angular/angular/commit/6b3aa60)), closes [#36062](https://github.com/angular/angular/issues/36062)
|
|
||||||
* **ngcc:** use path-mappings from tsconfig in dependency resolution ([#36180](https://github.com/angular/angular/issues/36180)) ([380de1e](https://github.com/angular/angular/commit/380de1e)), closes [#36119](https://github.com/angular/angular/issues/36119)
|
|
||||||
* **ngcc:** use preserve whitespaces from tsconfig if provided ([#36189](https://github.com/angular/angular/issues/36189)) ([b8e9a30](https://github.com/angular/angular/commit/b8e9a30)), closes [#35871](https://github.com/angular/angular/issues/35871)
|
|
||||||
* **platform-server:** update `xhr2` dependency ([#36366](https://github.com/angular/angular/issues/36366)) ([b59bc0e](https://github.com/angular/angular/commit/b59bc0e)), closes [#36358](https://github.com/angular/angular/issues/36358)
|
|
||||||
* **router:** allow UrlMatcher to return null ([#36402](https://github.com/angular/angular/issues/36402)) ([568e9df](https://github.com/angular/angular/commit/568e9df)), closes [#29824](https://github.com/angular/angular/issues/29824)
|
|
||||||
* **router:** state data missing in routerLink ([#36462](https://github.com/angular/angular/issues/36462)) ([e0415db](https://github.com/angular/angular/commit/e0415db)), closes [#33173](https://github.com/angular/angular/issues/33173)
|
|
||||||
* **service-worker:** by default register the SW after 30s even the app never stabilizes ([#35870](https://github.com/angular/angular/issues/35870)) ([29e8a64](https://github.com/angular/angular/commit/29e8a64)), closes [#34464](https://github.com/angular/angular/issues/34464)
|
|
||||||
* **service-worker:** prevent SW registration strategies from affecting app stabilization ([#35870](https://github.com/angular/angular/issues/35870)) ([2d7c95f](https://github.com/angular/angular/commit/2d7c95f))
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* **compiler:** add dependency info and ng-content selectors to metadata ([#35695](https://github.com/angular/angular/issues/35695)) ([32ce8b1](https://github.com/angular/angular/commit/32ce8b1))
|
|
||||||
* **compiler:** Propagate value span of ExpressionBinding to ParsedProperty ([#36133](https://github.com/angular/angular/issues/36133)) ([d714b95](https://github.com/angular/angular/commit/d714b95))
|
|
||||||
* **core:** undecorated-classes migration should handle derived abstract classes ([#35339](https://github.com/angular/angular/issues/35339)) ([c24ad56](https://github.com/angular/angular/commit/c24ad56))
|
|
||||||
* **service-worker:** support timeout in `registerWhenStable` SW registration strategy ([#35870](https://github.com/angular/angular/issues/35870)) ([00efacf](https://github.com/angular/angular/commit/00efacf)), closes [#34464](https://github.com/angular/angular/issues/34464)
|
|
||||||
|
|
||||||
|
|
||||||
### BREAKING CHANGES
|
|
||||||
|
|
||||||
* **router:** UrlMatcher's type now reflects that it could always return
|
|
||||||
null.
|
|
||||||
|
|
||||||
If you implemented your own Router or Recognizer class, please update it to
|
|
||||||
handle matcher returning null.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a name="9.1.1"></a>
|
<a name="9.1.1"></a>
|
||||||
## [9.1.1](https://github.com/angular/angular/compare/9.1.0...9.1.1) (2020-04-07)
|
## [9.1.1](https://github.com/angular/angular/compare/9.1.0...9.1.1) (2020-04-07)
|
||||||
@ -144,6 +90,7 @@ This release contains various API docs improvements.
|
|||||||
* **router:** state data missing in routerLink ([#36462](https://github.com/angular/angular/issues/36462)) ([0e7a89a](https://github.com/angular/angular/commit/0e7a89a)), closes [#33173](https://github.com/angular/angular/issues/33173)
|
* **router:** state data missing in routerLink ([#36462](https://github.com/angular/angular/issues/36462)) ([0e7a89a](https://github.com/angular/angular/commit/0e7a89a)), closes [#33173](https://github.com/angular/angular/issues/33173)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a name="9.1.0"></a>
|
<a name="9.1.0"></a>
|
||||||
# [9.1.0](https://github.com/angular/angular/compare/9.0.0...9.1.0) (2020-03-25)
|
# [9.1.0](https://github.com/angular/angular/compare/9.0.0...9.1.0) (2020-03-25)
|
||||||
|
|
||||||
|
3
aio/content/examples/.gitignore
vendored
3
aio/content/examples/.gitignore
vendored
@ -82,9 +82,6 @@ upgrade-phonecat-2-hybrid/aot/**/*
|
|||||||
# styleguide
|
# styleguide
|
||||||
!styleguide/src/systemjs.custom.js
|
!styleguide/src/systemjs.custom.js
|
||||||
|
|
||||||
# universal
|
|
||||||
!universal/webpack.server.config.js
|
|
||||||
|
|
||||||
# stackblitz
|
# stackblitz
|
||||||
*stackblitz.no-link.html
|
*stackblitz.no-link.html
|
||||||
|
|
||||||
|
11
aio/content/examples/router/src/app/app-routing.module.7.ts
Normal file
11
aio/content/examples/router/src/app/app-routing.module.7.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router'; // CLI imports router
|
||||||
|
|
||||||
|
const routes: Routes = []; // sets up routes constant where you define your routes
|
||||||
|
|
||||||
|
// configures NgModule imports and exports
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forRoot(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class AppRoutingModule { }
|
26
aio/content/examples/router/src/app/app-routing.module.8.ts
Normal file
26
aio/content/examples/router/src/app/app-routing.module.8.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// #docplaster
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router'; // CLI imports router
|
||||||
|
|
||||||
|
// #docregion routes, routes-with-wildcard, redirect
|
||||||
|
const routes: Routes = [
|
||||||
|
{ path: 'first-component', component: FirstComponent },
|
||||||
|
{ path: 'second-component', component: SecondComponent },
|
||||||
|
// #enddocregion routes
|
||||||
|
{ path: '', redirectTo: '/first-component', pathMatch: 'full' }, // redirect to `first-component`
|
||||||
|
{ path: '**', component: FirstComponent },
|
||||||
|
// #enddocregion redirect
|
||||||
|
{ path: '**', component: PageNotFoundComponent }, // Wildcard route for a 404 page
|
||||||
|
// #docregion routes
|
||||||
|
// #docregion redirect
|
||||||
|
];
|
||||||
|
// #enddocregion routes, routes-with-wildcard, redirect
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forRoot(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class AppRoutingModule { }
|
||||||
|
|
||||||
|
|
28
aio/content/examples/router/src/app/app-routing.module.9.ts
Normal file
28
aio/content/examples/router/src/app/app-routing.module.9.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// #docplaster
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router'; // CLI imports router
|
||||||
|
|
||||||
|
// #docregion child-routes
|
||||||
|
const routes: Routes = [
|
||||||
|
{ path: 'first-component',
|
||||||
|
component: FirstComponent, // this is the component with the <router-outlet> in the template
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'child-a', // child route path
|
||||||
|
component: ChildAComponent // child route component that the router renders
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'child-b',
|
||||||
|
component: ChildBComponent // another child route component that the router renders
|
||||||
|
}
|
||||||
|
] },
|
||||||
|
// #enddocregion child-routes
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forRoot(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class AppRoutingModule { }
|
||||||
|
|
||||||
|
|
15
aio/content/examples/router/src/app/app.component.4.ts
Normal file
15
aio/content/examples/router/src/app/app.component.4.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
templateUrl: 'app.component.html',
|
||||||
|
styleUrls: ['app.component.css']
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
// #docregion relative-to
|
||||||
|
goToItems() {
|
||||||
|
this.router.navigate(['items'], { relativeTo: this.route });
|
||||||
|
}
|
||||||
|
// #enddocregion relative-to
|
||||||
|
|
||||||
|
}
|
10
aio/content/examples/router/src/app/app.component.7.html
Normal file
10
aio/content/examples/router/src/app/app.component.7.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<h1>Angular Router App</h1>
|
||||||
|
<!-- This nav gives you links to click, which tells the router which route to use (defined in the routes constant in AppRoutingModule) -->
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li><a routerLink="/first-component" routerLinkActive="active">First Component</a></li>
|
||||||
|
<li><a routerLink="/second-component" routerLinkActive="active">Second Component</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<!-- The routed views render in the <router-outlet>-->
|
||||||
|
<router-outlet></router-outlet>
|
26
aio/content/examples/router/src/app/app.component.8.html
Normal file
26
aio/content/examples/router/src/app/app.component.8.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<!-- #docregion child-routes-->
|
||||||
|
<h2>First Component</h2>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li><a routerLink="child-a">Child A</a></li>
|
||||||
|
<li><a routerLink="child-b">Child B</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
<!-- #enddocregion child-routes-->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- #docregion relative-route-->
|
||||||
|
|
||||||
|
<h2>First Component</h2>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li><a routerLink="../second-component">Relative Route to second component</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
|
||||||
|
<!-- #enddocregion relative-route-->
|
17
aio/content/examples/router/src/app/app.module.8.ts
Normal file
17
aio/content/examples/router/src/app/app.module.8.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { AppRoutingModule } from './app-routing.module'; // CLI imports AppRoutingModule
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AppComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
AppRoutingModule // CLI adds AppRoutingModule to the AppModule's imports array
|
||||||
|
],
|
||||||
|
providers: [],
|
||||||
|
bootstrap: [AppComponent]
|
||||||
|
})
|
||||||
|
export class AppModule { }
|
@ -2,7 +2,9 @@
|
|||||||
// #docregion
|
// #docregion
|
||||||
import { switchMap } from 'rxjs/operators';
|
import { switchMap } from 'rxjs/operators';
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
// #docregion imports-route-info
|
||||||
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
|
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
|
||||||
|
// #enddocregion imports-route-info
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
import { HeroService } from '../hero.service';
|
import { HeroService } from '../hero.service';
|
||||||
@ -16,11 +18,16 @@ import { Hero } from '../hero';
|
|||||||
export class HeroDetailComponent implements OnInit {
|
export class HeroDetailComponent implements OnInit {
|
||||||
hero$: Observable<Hero>;
|
hero$: Observable<Hero>;
|
||||||
|
|
||||||
|
// #docregion activated-route
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
// #enddocregion activated-route
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private service: HeroService
|
private service: HeroService
|
||||||
|
// #docregion activated-route
|
||||||
) {}
|
) {}
|
||||||
|
// #enddocregion activated-route
|
||||||
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.hero$ = this.route.paramMap.pipe(
|
this.hero$ = this.route.paramMap.pipe(
|
||||||
|
@ -8,4 +8,6 @@ if (environment.production) {
|
|||||||
enableProdMode();
|
enableProdMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
|
||||||
|
@ -9,5 +9,6 @@ if (environment.production) {
|
|||||||
enableProdMode();
|
enableProdMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||||
|
.catch(err => console.error(err));
|
||||||
// #enddocregion
|
// #enddocregion
|
||||||
|
@ -9,5 +9,6 @@ if (environment.production) {
|
|||||||
enableProdMode();
|
enableProdMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||||
|
.catch(err => console.error(err));
|
||||||
// #enddocregion
|
// #enddocregion
|
||||||
|
@ -9,5 +9,6 @@ if (environment.production) {
|
|||||||
enableProdMode();
|
enableProdMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||||
|
.catch(err => console.error(err));
|
||||||
// #enddocregion
|
// #enddocregion
|
||||||
|
@ -8,4 +8,5 @@ if (environment.production) {
|
|||||||
enableProdMode();
|
enableProdMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
@ -19,7 +19,6 @@ button {
|
|||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
cursor: hand;
|
|
||||||
}
|
}
|
||||||
button:hover {
|
button:hover {
|
||||||
background-color: #cfd8dc;
|
background-color: #cfd8dc;
|
||||||
|
@ -8,4 +8,5 @@ if (environment.production) {
|
|||||||
enableProdMode();
|
enableProdMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// #docplaster
|
// #docplaster
|
||||||
// #docregion, v1
|
// #docregion , v1
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
@ -59,6 +59,5 @@ import { MessagesComponent } from './messages/messages.component';
|
|||||||
// #docregion import-httpclientmodule
|
// #docregion import-httpclientmodule
|
||||||
})
|
})
|
||||||
// #enddocregion import-httpclientmodule
|
// #enddocregion import-httpclientmodule
|
||||||
|
|
||||||
export class AppModule { }
|
export class AppModule { }
|
||||||
// #enddocregion , v1
|
// #enddocregion , v1
|
||||||
|
@ -18,7 +18,7 @@ button {
|
|||||||
border: none;
|
border: none;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer; cursor: hand;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
button:hover {
|
button:hover {
|
||||||
background-color: #cfd8dc;
|
background-color: #cfd8dc;
|
||||||
|
@ -38,5 +38,5 @@ export class HeroDetailComponent implements OnInit {
|
|||||||
this.heroService.updateHero(this.hero)
|
this.heroService.updateHero(this.hero)
|
||||||
.subscribe(() => this.goBack());
|
.subscribe(() => this.goBack());
|
||||||
}
|
}
|
||||||
// #enddocregion save
|
// #enddocregion save
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ export class HeroService {
|
|||||||
// #docregion getHeroes, getHeroes-1
|
// #docregion getHeroes, getHeroes-1
|
||||||
/** GET heroes from the server */
|
/** GET heroes from the server */
|
||||||
// #docregion getHeroes-2
|
// #docregion getHeroes-2
|
||||||
getHeroes (): Observable<Hero[]> {
|
getHeroes(): Observable<Hero[]> {
|
||||||
return this.http.get<Hero[]>(this.heroesUrl)
|
return this.http.get<Hero[]>(this.heroesUrl)
|
||||||
// #enddocregion getHeroes-1
|
// #enddocregion getHeroes-1
|
||||||
.pipe(
|
.pipe(
|
||||||
@ -98,7 +98,7 @@ export class HeroService {
|
|||||||
|
|
||||||
// #docregion addHero
|
// #docregion addHero
|
||||||
/** POST: add a new hero to the server */
|
/** POST: add a new hero to the server */
|
||||||
addHero (hero: Hero): Observable<Hero> {
|
addHero(hero: Hero): Observable<Hero> {
|
||||||
return this.http.post<Hero>(this.heroesUrl, hero, this.httpOptions).pipe(
|
return this.http.post<Hero>(this.heroesUrl, hero, this.httpOptions).pipe(
|
||||||
tap((newHero: Hero) => this.log(`added hero w/ id=${newHero.id}`)),
|
tap((newHero: Hero) => this.log(`added hero w/ id=${newHero.id}`)),
|
||||||
catchError(this.handleError<Hero>('addHero'))
|
catchError(this.handleError<Hero>('addHero'))
|
||||||
@ -108,7 +108,7 @@ export class HeroService {
|
|||||||
|
|
||||||
// #docregion deleteHero
|
// #docregion deleteHero
|
||||||
/** DELETE: delete the hero from the server */
|
/** DELETE: delete the hero from the server */
|
||||||
deleteHero (hero: Hero | number): Observable<Hero> {
|
deleteHero(hero: Hero | number): Observable<Hero> {
|
||||||
const id = typeof hero === 'number' ? hero : hero.id;
|
const id = typeof hero === 'number' ? hero : hero.id;
|
||||||
const url = `${this.heroesUrl}/${id}`;
|
const url = `${this.heroesUrl}/${id}`;
|
||||||
|
|
||||||
@ -121,7 +121,7 @@ export class HeroService {
|
|||||||
|
|
||||||
// #docregion updateHero
|
// #docregion updateHero
|
||||||
/** PUT: update the hero on the server */
|
/** PUT: update the hero on the server */
|
||||||
updateHero (hero: Hero): Observable<any> {
|
updateHero(hero: Hero): Observable<any> {
|
||||||
return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe(
|
return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe(
|
||||||
tap(_ => this.log(`updated hero id=${hero.id}`)),
|
tap(_ => this.log(`updated hero id=${hero.id}`)),
|
||||||
catchError(this.handleError<any>('updateHero'))
|
catchError(this.handleError<any>('updateHero'))
|
||||||
@ -136,7 +136,7 @@ export class HeroService {
|
|||||||
* @param operation - name of the operation that failed
|
* @param operation - name of the operation that failed
|
||||||
* @param result - optional value to return as the observable result
|
* @param result - optional value to return as the observable result
|
||||||
*/
|
*/
|
||||||
private handleError<T> (operation = 'operation', result?: T) {
|
private handleError<T>(operation = 'operation', result?: T) {
|
||||||
return (error: any): Observable<T> => {
|
return (error: any): Observable<T> => {
|
||||||
|
|
||||||
// TODO: send the error to remote logging infrastructure
|
// TODO: send the error to remote logging infrastructure
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.heroes a:hover {
|
.heroes a:hover {
|
||||||
color:#607D8B;
|
color: #607D8B;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heroes .badge {
|
.heroes .badge {
|
||||||
@ -38,7 +38,7 @@
|
|||||||
font-size: small;
|
font-size: small;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 0.8em 0.7em 0 0.7em;
|
padding: 0.8em 0.7em 0 0.7em;
|
||||||
background-color:#405061;
|
background-color: #405061;
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
position: relative;
|
position: relative;
|
||||||
left: -1px;
|
left: -1px;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// #docregion , init
|
// #docregion , init
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
import { InMemoryDbService } from 'angular-in-memory-web-api';
|
import { InMemoryDbService } from 'angular-in-memory-web-api';
|
||||||
import { Hero } from './hero';
|
import { Hero } from './hero';
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
|
@ -9,4 +9,5 @@ if (environment.production) {
|
|||||||
enableProdMode();
|
enableProdMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
300
aio/content/examples/universal/e2e/src/app.e2e-spec.ts
Normal file
300
aio/content/examples/universal/e2e/src/app.e2e-spec.ts
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
import { browser, by, element, ElementArrayFinder, ElementFinder, logging } from 'protractor';
|
||||||
|
|
||||||
|
class Hero {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
// Factory methods
|
||||||
|
|
||||||
|
// Hero from string formatted as '<id> <name>'.
|
||||||
|
static fromString(s: string): Hero {
|
||||||
|
return {
|
||||||
|
id: +s.substr(0, s.indexOf(' ')),
|
||||||
|
name: s.substr(s.indexOf(' ') + 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hero from hero list <li> element.
|
||||||
|
static async fromLi(li: ElementFinder): Promise<Hero> {
|
||||||
|
const stringsFromA = await li.all(by.css('a')).getText();
|
||||||
|
const strings = stringsFromA[0].split(' ');
|
||||||
|
return { id: +strings[0], name: strings[1] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hero id and name from the given detail element.
|
||||||
|
static async fromDetail(detail: ElementFinder): Promise<Hero> {
|
||||||
|
// Get hero id from the first <div>
|
||||||
|
const id = await detail.all(by.css('div')).first().getText();
|
||||||
|
// Get name from the h2
|
||||||
|
const name = await detail.element(by.css('h2')).getText();
|
||||||
|
return {
|
||||||
|
id: +id.substr(id.indexOf(' ') + 1),
|
||||||
|
name: name.substr(0, name.lastIndexOf(' '))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Universal', () => {
|
||||||
|
const expectedH1 = 'Tour of Heroes';
|
||||||
|
const expectedTitle = `${expectedH1}`;
|
||||||
|
const targetHero = { id: 15, name: 'Magneta' };
|
||||||
|
const targetHeroDashboardIndex = 3;
|
||||||
|
const nameSuffix = 'X';
|
||||||
|
const newHeroName = targetHero.name + nameSuffix;
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
// Assert that there are no errors emitted from the browser
|
||||||
|
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||||
|
const severeLogs = logs.filter(entry => entry.level === logging.Level.SEVERE);
|
||||||
|
expect(severeLogs).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Initial page', () => {
|
||||||
|
beforeAll(() => browser.get(''));
|
||||||
|
|
||||||
|
it(`has title '${expectedTitle}'`, () => {
|
||||||
|
expect(browser.getTitle()).toEqual(expectedTitle);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`has h1 '${expectedH1}'`, () => {
|
||||||
|
expectHeading(1, expectedH1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedViewNames = ['Dashboard', 'Heroes'];
|
||||||
|
it(`has views ${expectedViewNames}`, () => {
|
||||||
|
const viewNames = getPageElts().navElts.map((el: ElementFinder) => el.getText());
|
||||||
|
expect(viewNames).toEqual(expectedViewNames);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has dashboard as the active view', () => {
|
||||||
|
const page = getPageElts();
|
||||||
|
expect(page.appDashboard.isPresent()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Dashboard tests', () => {
|
||||||
|
beforeAll(() => browser.get(''));
|
||||||
|
|
||||||
|
it('has top heroes', () => {
|
||||||
|
const page = getPageElts();
|
||||||
|
expect(page.topHeroes.count()).toEqual(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero);
|
||||||
|
|
||||||
|
it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView);
|
||||||
|
|
||||||
|
it(`cancels and shows ${targetHero.name} in Dashboard`, () => {
|
||||||
|
element(by.buttonText('go back')).click();
|
||||||
|
browser.waitForAngular(); // seems necessary to gets tests to pass for toh-pt6
|
||||||
|
|
||||||
|
const targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex);
|
||||||
|
expect(targetHeroElt.getText()).toEqual(targetHero.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero);
|
||||||
|
|
||||||
|
it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView);
|
||||||
|
|
||||||
|
it(`saves and shows ${newHeroName} in Dashboard`, () => {
|
||||||
|
element(by.buttonText('save')).click();
|
||||||
|
browser.waitForAngular(); // seems necessary to gets tests to pass for toh-pt6
|
||||||
|
|
||||||
|
const targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex);
|
||||||
|
expect(targetHeroElt.getText()).toEqual(newHeroName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Heroes tests', () => {
|
||||||
|
beforeAll(() => browser.get(''));
|
||||||
|
|
||||||
|
it('can switch to Heroes view', () => {
|
||||||
|
getPageElts().appHeroesHref.click();
|
||||||
|
const page = getPageElts();
|
||||||
|
expect(page.appHeroes.isPresent()).toBeTruthy();
|
||||||
|
expect(page.allHeroes.count()).toEqual(10, 'number of heroes');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can route to hero details', async () => {
|
||||||
|
getHeroLiEltById(targetHero.id).click();
|
||||||
|
|
||||||
|
const page = getPageElts();
|
||||||
|
expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail');
|
||||||
|
const hero = await Hero.fromDetail(page.heroDetail);
|
||||||
|
expect(hero.id).toEqual(targetHero.id);
|
||||||
|
expect(hero.name).toEqual(targetHero.name.toUpperCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView);
|
||||||
|
|
||||||
|
it(`shows ${newHeroName} in Heroes list`, () => {
|
||||||
|
element(by.buttonText('save')).click();
|
||||||
|
browser.waitForAngular();
|
||||||
|
const expectedText = `${targetHero.id} ${newHeroName}`;
|
||||||
|
expect(getHeroAEltById(targetHero.id).getText()).toEqual(expectedText);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`deletes ${newHeroName} from Heroes list`, async () => {
|
||||||
|
const heroesBefore = await toHeroArray(getPageElts().allHeroes);
|
||||||
|
const li = getHeroLiEltById(targetHero.id);
|
||||||
|
li.element(by.buttonText('x')).click();
|
||||||
|
|
||||||
|
const page = getPageElts();
|
||||||
|
expect(page.appHeroes.isPresent()).toBeTruthy();
|
||||||
|
expect(page.allHeroes.count()).toEqual(9, 'number of heroes');
|
||||||
|
const heroesAfter = await toHeroArray(page.allHeroes);
|
||||||
|
// console.log(await Hero.fromLi(page.allHeroes[0]));
|
||||||
|
const expectedHeroes = heroesBefore.filter(h => h.name !== newHeroName);
|
||||||
|
expect(heroesAfter).toEqual(expectedHeroes);
|
||||||
|
// expect(page.selectedHeroSubview.isPresent()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`adds back ${targetHero.name}`, async () => {
|
||||||
|
const updatedHeroName = 'Alice';
|
||||||
|
const heroesBefore = await toHeroArray(getPageElts().allHeroes);
|
||||||
|
const numHeroes = heroesBefore.length;
|
||||||
|
|
||||||
|
element(by.css('input')).sendKeys(updatedHeroName);
|
||||||
|
element(by.buttonText('add')).click();
|
||||||
|
|
||||||
|
const page = getPageElts();
|
||||||
|
const heroesAfter = await toHeroArray(page.allHeroes);
|
||||||
|
expect(heroesAfter.length).toEqual(numHeroes + 1, 'number of heroes');
|
||||||
|
|
||||||
|
expect(heroesAfter.slice(0, numHeroes)).toEqual(heroesBefore, 'Old heroes are still there');
|
||||||
|
|
||||||
|
const maxId = heroesBefore[heroesBefore.length - 1].id;
|
||||||
|
expect(heroesAfter[numHeroes]).toEqual({id: maxId + 1, name: updatedHeroName});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays correctly styled buttons', async () => {
|
||||||
|
element.all(by.buttonText('x')).then(buttons => {
|
||||||
|
for (const button of buttons) {
|
||||||
|
// Inherited styles from styles.css
|
||||||
|
expect(button.getCssValue('font-family')).toBe('Arial');
|
||||||
|
expect(button.getCssValue('border')).toContain('none');
|
||||||
|
expect(button.getCssValue('padding')).toBe('5px 10px');
|
||||||
|
expect(button.getCssValue('border-radius')).toBe('4px');
|
||||||
|
// Styles defined in heroes.component.css
|
||||||
|
expect(button.getCssValue('left')).toBe('194px');
|
||||||
|
expect(button.getCssValue('top')).toBe('-32px');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const addButton = element(by.buttonText('add'));
|
||||||
|
// Inherited styles from styles.css
|
||||||
|
expect(addButton.getCssValue('font-family')).toBe('Arial');
|
||||||
|
expect(addButton.getCssValue('border')).toContain('none');
|
||||||
|
expect(addButton.getCssValue('padding')).toBe('5px 10px');
|
||||||
|
expect(addButton.getCssValue('border-radius')).toBe('4px');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Progressive hero search', () => {
|
||||||
|
beforeAll(() => browser.get(''));
|
||||||
|
|
||||||
|
it(`searches for 'Ma'`, async () => {
|
||||||
|
getPageElts().searchBox.sendKeys('Ma');
|
||||||
|
browser.sleep(1000);
|
||||||
|
|
||||||
|
expect(getPageElts().searchResults.count()).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`continues search with 'g'`, async () => {
|
||||||
|
getPageElts().searchBox.sendKeys('g');
|
||||||
|
browser.sleep(1000);
|
||||||
|
expect(getPageElts().searchResults.count()).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`continues search with 'e' and gets ${targetHero.name}`, async () => {
|
||||||
|
getPageElts().searchBox.sendKeys('n');
|
||||||
|
browser.sleep(1000);
|
||||||
|
const page = getPageElts();
|
||||||
|
expect(page.searchResults.count()).toBe(1);
|
||||||
|
const hero = page.searchResults.get(0);
|
||||||
|
expect(hero.getText()).toEqual(targetHero.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`navigates to ${targetHero.name} details view`, async () => {
|
||||||
|
const hero = getPageElts().searchResults.get(0);
|
||||||
|
expect(hero.getText()).toEqual(targetHero.name);
|
||||||
|
hero.click();
|
||||||
|
|
||||||
|
const page = getPageElts();
|
||||||
|
expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail');
|
||||||
|
const hero2 = await Hero.fromDetail(page.heroDetail);
|
||||||
|
expect(hero2.id).toEqual(targetHero.id);
|
||||||
|
expect(hero2.name).toEqual(targetHero.name.toUpperCase());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
function addToHeroName(text: string): Promise<void> {
|
||||||
|
return element(by.css('input')).sendKeys(text) as Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dashboardSelectTargetHero(): Promise<void> {
|
||||||
|
const targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex);
|
||||||
|
expect(targetHeroElt.getText()).toEqual(targetHero.name);
|
||||||
|
targetHeroElt.click();
|
||||||
|
browser.waitForAngular(); // seems necessary to gets tests to pass for toh-pt6
|
||||||
|
|
||||||
|
const page = getPageElts();
|
||||||
|
expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail');
|
||||||
|
const hero = await Hero.fromDetail(page.heroDetail);
|
||||||
|
expect(hero.id).toEqual(targetHero.id);
|
||||||
|
expect(hero.name).toEqual(targetHero.name.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectHeading(hLevel: number, expectedText: string): void {
|
||||||
|
const hTag = `h${hLevel}`;
|
||||||
|
const hText = element(by.css(hTag)).getText();
|
||||||
|
expect(hText).toEqual(expectedText, hTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHeroAEltById(id: number): ElementFinder {
|
||||||
|
const spanForId = element(by.cssContainingText('li span.badge', id.toString()));
|
||||||
|
return spanForId.element(by.xpath('..'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHeroLiEltById(id: number): ElementFinder {
|
||||||
|
const spanForId = element(by.cssContainingText('li span.badge', id.toString()));
|
||||||
|
return spanForId.element(by.xpath('../..'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPageElts() {
|
||||||
|
const navElts = element.all(by.css('app-root nav a'));
|
||||||
|
|
||||||
|
return {
|
||||||
|
navElts,
|
||||||
|
|
||||||
|
appDashboardHref: navElts.get(0),
|
||||||
|
appDashboard: element(by.css('app-root app-dashboard')),
|
||||||
|
topHeroes: element.all(by.css('app-root app-dashboard > div h4')),
|
||||||
|
|
||||||
|
appHeroesHref: navElts.get(1),
|
||||||
|
appHeroes: element(by.css('app-root app-heroes')),
|
||||||
|
allHeroes: element.all(by.css('app-root app-heroes li')),
|
||||||
|
selectedHeroSubview: element(by.css('app-root app-heroes > div:last-child')),
|
||||||
|
|
||||||
|
heroDetail: element(by.css('app-root app-hero-detail > div')),
|
||||||
|
|
||||||
|
searchBox: element(by.css('#search-box')),
|
||||||
|
searchResults: element.all(by.css('.search-result li'))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toHeroArray(allHeroes: ElementArrayFinder): Promise<Hero[]> {
|
||||||
|
return await allHeroes.map(Hero.fromLi);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateHeroNameInDetailView(): Promise<void> {
|
||||||
|
// Assumes that the current view is the hero details view.
|
||||||
|
addToHeroName(nameSuffix);
|
||||||
|
|
||||||
|
const page = getPageElts();
|
||||||
|
const hero = await Hero.fromDetail(page.heroDetail);
|
||||||
|
expect(hero.id).toEqual(targetHero.id);
|
||||||
|
expect(hero.name).toEqual(newHeroName.toUpperCase());
|
||||||
|
}
|
||||||
|
});
|
@ -1,3 +1,7 @@
|
|||||||
{
|
{
|
||||||
"projectType": "universal"
|
"projectType": "universal",
|
||||||
|
"e2e": [
|
||||||
|
{"cmd": "yarn", "args": ["e2e", "--prod", "--protractor-config=e2e/protractor-puppeteer.conf.js", "--no-webdriver-update", "--port={PORT}"]},
|
||||||
|
{"cmd": "yarn", "args": ["run", "build:ssr"]}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@ -6,24 +6,28 @@ import { join } from 'path';
|
|||||||
|
|
||||||
import { AppServerModule } from './src/main.server';
|
import { AppServerModule } from './src/main.server';
|
||||||
import { APP_BASE_HREF } from '@angular/common';
|
import { APP_BASE_HREF } from '@angular/common';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
|
||||||
// The Express app is exported so that it can be used by serverless Functions.
|
// The Express app is exported so that it can be used by serverless Functions.
|
||||||
export function app() {
|
export function app() {
|
||||||
const server = express();
|
const server = express();
|
||||||
const distFolder = join(process.cwd(), 'dist/express-engine-ivy/browser');
|
const distFolder = join(process.cwd(), 'dist/browser');
|
||||||
|
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
|
||||||
|
|
||||||
// #docregion ngExpressEngine
|
// #docregion ngExpressEngine
|
||||||
|
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
|
||||||
server.engine('html', ngExpressEngine({
|
server.engine('html', ngExpressEngine({
|
||||||
bootstrap: AppServerModule,
|
bootstrap: AppServerModule,
|
||||||
}));
|
}));
|
||||||
// #enddocregion ngExpressEngine
|
// #enddocregion ngExpressEngine
|
||||||
|
|
||||||
server.set('view engine', 'html');
|
server.set('view engine', 'html');
|
||||||
server.set('views', distFolder);
|
server.set('views', distFolder);
|
||||||
|
|
||||||
// #docregion data-request
|
// #docregion data-request
|
||||||
// TODO: implement data requests securely
|
// TODO: implement data requests securely
|
||||||
server.get('/api/*', (req, res) => {
|
server.get('/api/**', (req, res) => {
|
||||||
res.status(404).send('data requests are not supported');
|
res.status(404).send('data requests are not yet supported');
|
||||||
});
|
});
|
||||||
// #enddocregion data-request
|
// #enddocregion data-request
|
||||||
|
|
||||||
@ -37,7 +41,7 @@ export function app() {
|
|||||||
// #docregion navigation-request
|
// #docregion navigation-request
|
||||||
// All regular routes use the Universal engine
|
// All regular routes use the Universal engine
|
||||||
server.get('*', (req, res) => {
|
server.get('*', (req, res) => {
|
||||||
res.render('index', { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
|
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
|
||||||
});
|
});
|
||||||
// #enddocregion navigation-request
|
// #enddocregion navigation-request
|
||||||
|
|
||||||
@ -59,7 +63,8 @@ function run() {
|
|||||||
// The below code is to ensure that the server is run only when not requiring the bundle.
|
// The below code is to ensure that the server is run only when not requiring the bundle.
|
||||||
declare const __non_webpack_require__: NodeRequire;
|
declare const __non_webpack_require__: NodeRequire;
|
||||||
const mainModule = __non_webpack_require__.main;
|
const mainModule = __non_webpack_require__.main;
|
||||||
if (mainModule && mainModule.filename === __filename) {
|
const moduleFilename = mainModule && mainModule.filename || '';
|
||||||
|
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
|
||||||
run();
|
run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
/* AppComponent's private CSS styles */
|
/* AppComponent's private CSS styles */
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
color: #999;
|
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
@ -18,7 +17,7 @@ nav a {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
nav a:visited, a:link {
|
nav a:visited, a:link {
|
||||||
color: #607D8B;
|
color: #334953;
|
||||||
}
|
}
|
||||||
nav a:hover {
|
nav a:hover {
|
||||||
color: #039be5;
|
color: #039be5;
|
||||||
|
@ -14,8 +14,6 @@ import { DashboardComponent } from './dashboard/dashboard.component';
|
|||||||
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
|
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
|
||||||
import { HeroesComponent } from './heroes/heroes.component';
|
import { HeroesComponent } from './heroes/heroes.component';
|
||||||
import { HeroSearchComponent } from './hero-search/hero-search.component';
|
import { HeroSearchComponent } from './hero-search/hero-search.component';
|
||||||
import { HeroService } from './hero.service';
|
|
||||||
import { MessageService } from './message.service';
|
|
||||||
import { MessagesComponent } from './messages/messages.component';
|
import { MessagesComponent } from './messages/messages.component';
|
||||||
|
|
||||||
// #docregion platform-detection
|
// #docregion platform-detection
|
||||||
@ -32,6 +30,10 @@ import { isPlatformBrowser } from '@angular/common';
|
|||||||
FormsModule,
|
FormsModule,
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
|
|
||||||
|
// The HttpClientInMemoryWebApiModule module intercepts HTTP requests
|
||||||
|
// and returns simulated server responses.
|
||||||
|
// Remove it when a real server is ready to receive requests.
|
||||||
HttpClientInMemoryWebApiModule.forRoot(
|
HttpClientInMemoryWebApiModule.forRoot(
|
||||||
InMemoryDataService, { dataEncapsulation: false }
|
InMemoryDataService, { dataEncapsulation: false }
|
||||||
)
|
)
|
||||||
@ -44,7 +46,6 @@ import { isPlatformBrowser } from '@angular/common';
|
|||||||
MessagesComponent,
|
MessagesComponent,
|
||||||
HeroSearchComponent
|
HeroSearchComponent
|
||||||
],
|
],
|
||||||
providers: [ HeroService, MessageService ],
|
|
||||||
bootstrap: [ AppComponent ]
|
bootstrap: [ AppComponent ]
|
||||||
})
|
})
|
||||||
export class AppModule {
|
export class AppModule {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { ServerModule } from '@angular/platform-server';
|
import { ServerModule } from '@angular/platform-server';
|
||||||
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';
|
|
||||||
|
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
@ -9,11 +8,10 @@ import { AppComponent } from './app.component';
|
|||||||
imports: [
|
imports: [
|
||||||
AppModule,
|
AppModule,
|
||||||
ServerModule,
|
ServerModule,
|
||||||
ModuleMapLoaderModule
|
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
// Add universal-only providers here
|
// Add server-only providers here.
|
||||||
],
|
],
|
||||||
bootstrap: [ AppComponent ],
|
bootstrap: [AppComponent],
|
||||||
})
|
})
|
||||||
export class AppServerModule {}
|
export class AppServerModule {}
|
||||||
|
@ -34,7 +34,7 @@ h4 {
|
|||||||
color: #eee;
|
color: #eee;
|
||||||
max-height: 120px;
|
max-height: 120px;
|
||||||
min-width: 120px;
|
min-width: 120px;
|
||||||
background-color: #607D8B;
|
background-color: #3f525c;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
.module:hover {
|
.module:hover {
|
||||||
|
@ -8,4 +8,4 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hero-search></hero-search>
|
<app-hero-search></app-hero-search>
|
||||||
|
@ -19,7 +19,6 @@ button {
|
|||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
cursor: hand;
|
|
||||||
}
|
}
|
||||||
button:hover {
|
button:hover {
|
||||||
background-color: #cfd8dc;
|
background-color: #cfd8dc;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<div *ngIf="hero">
|
<div *ngIf="hero">
|
||||||
<h2>{{ hero.name | uppercase }} Details</h2>
|
<h2>{{hero.name | uppercase}} Details</h2>
|
||||||
<div><span>id: </span>{{hero.id}}</div>
|
<div><span>id: </span>{{hero.id}}</div>
|
||||||
<div>
|
<div>
|
||||||
<label>name:
|
<label>name:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit, Input } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { Location } from '@angular/common';
|
import { Location } from '@angular/common';
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ import { HeroService } from '../hero.service';
|
|||||||
styleUrls: [ './hero-detail.component.css' ]
|
styleUrls: [ './hero-detail.component.css' ]
|
||||||
})
|
})
|
||||||
export class HeroDetailComponent implements OnInit {
|
export class HeroDetailComponent implements OnInit {
|
||||||
hero: Hero;
|
@Input() hero: Hero;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<div id="search-component">
|
<div id="search-component">
|
||||||
<h4>Hero Search</h4>
|
<h4><label for="search-box">Hero Search</label></h4>
|
||||||
|
|
||||||
<input #searchBox id="search-box" (input)="search(searchBox.value)" />
|
<input #searchBox id="search-box" (input)="search(searchBox.value)" />
|
||||||
|
|
||||||
<ul class="search-result">
|
<ul class="search-result">
|
||||||
<li *ngFor="let hero of heroes | async" >
|
<li *ngFor="let hero of heroes$ | async" >
|
||||||
<a routerLink="/detail/{{hero.id}}">
|
<a routerLink="/detail/{{hero.id}}">
|
||||||
{{hero.name}}
|
{{hero.name}}
|
||||||
</a>
|
</a>
|
||||||
|
@ -10,12 +10,12 @@ import { Hero } from '../hero';
|
|||||||
import { HeroService } from '../hero.service';
|
import { HeroService } from '../hero.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'hero-search',
|
selector: 'app-hero-search',
|
||||||
templateUrl: './hero-search.component.html',
|
templateUrl: './hero-search.component.html',
|
||||||
styleUrls: [ './hero-search.component.css' ]
|
styleUrls: [ './hero-search.component.css' ]
|
||||||
})
|
})
|
||||||
export class HeroSearchComponent implements OnInit {
|
export class HeroSearchComponent implements OnInit {
|
||||||
heroes: Observable<Hero[]>;
|
heroes$: Observable<Hero[]>;
|
||||||
private searchTerms = new Subject<string>();
|
private searchTerms = new Subject<string>();
|
||||||
|
|
||||||
constructor(private heroService: HeroService) {}
|
constructor(private heroService: HeroService) {}
|
||||||
@ -26,7 +26,7 @@ export class HeroSearchComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.heroes = this.searchTerms.pipe(
|
this.heroes$ = this.searchTerms.pipe(
|
||||||
// wait 300ms after each keystroke before considering the term
|
// wait 300ms after each keystroke before considering the term
|
||||||
debounceTime(300),
|
debounceTime(300),
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Injectable, Inject, Optional } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { APP_BASE_HREF } from '@angular/common';
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
import { HttpClient, HttpHeaders }from '@angular/common/http';
|
|
||||||
|
|
||||||
import { Observable, of } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
import { catchError, map, tap } from 'rxjs/operators';
|
import { catchError, map, tap } from 'rxjs/operators';
|
||||||
@ -8,30 +7,26 @@ import { catchError, map, tap } from 'rxjs/operators';
|
|||||||
import { Hero } from './hero';
|
import { Hero } from './hero';
|
||||||
import { MessageService } from './message.service';
|
import { MessageService } from './message.service';
|
||||||
|
|
||||||
const httpOptions = {
|
|
||||||
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable({ providedIn: 'root' })
|
||||||
export class HeroService {
|
export class HeroService {
|
||||||
|
|
||||||
private heroesUrl = 'api/heroes'; // URL to web api
|
private heroesUrl = 'api/heroes'; // URL to web api
|
||||||
|
|
||||||
// #docregion ctor
|
httpOptions = {
|
||||||
|
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
|
||||||
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private messageService: MessageService,
|
private messageService: MessageService) { }
|
||||||
@Optional() @Inject(APP_BASE_HREF) origin?: string) {
|
|
||||||
this.heroesUrl = `${origin}${this.heroesUrl}`;
|
|
||||||
}
|
|
||||||
// #enddocregion ctor
|
|
||||||
|
|
||||||
/** GET heroes from the server */
|
/** GET heroes from the server */
|
||||||
getHeroes (): Observable<Hero[]> {
|
getHeroes(): Observable<Hero[]> {
|
||||||
return this.http.get<Hero[]>(this.heroesUrl)
|
return this.http.get<Hero[]>(this.heroesUrl)
|
||||||
.pipe(
|
.pipe(
|
||||||
tap(heroes => this.log('fetched heroes')),
|
tap(_ => this.log('fetched heroes')),
|
||||||
catchError(this.handleError('getHeroes', []))
|
catchError(this.handleError<Hero[]>('getHeroes', []))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,7 +60,9 @@ export class HeroService {
|
|||||||
return of([]);
|
return of([]);
|
||||||
}
|
}
|
||||||
return this.http.get<Hero[]>(`${this.heroesUrl}/?name=${term}`).pipe(
|
return this.http.get<Hero[]>(`${this.heroesUrl}/?name=${term}`).pipe(
|
||||||
tap(_ => this.log(`found heroes matching "${term}"`)),
|
tap(x => x.length ?
|
||||||
|
this.log(`found heroes matching "${term}"`) :
|
||||||
|
this.log(`no heroes matching "${term}"`)),
|
||||||
catchError(this.handleError<Hero[]>('searchHeroes', []))
|
catchError(this.handleError<Hero[]>('searchHeroes', []))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -73,29 +70,27 @@ export class HeroService {
|
|||||||
//////// Save methods //////////
|
//////// Save methods //////////
|
||||||
|
|
||||||
/** POST: add a new hero to the server */
|
/** POST: add a new hero to the server */
|
||||||
addHero (name: string): Observable<Hero> {
|
addHero(hero: Hero): Observable<Hero> {
|
||||||
const hero = { name };
|
return this.http.post<Hero>(this.heroesUrl, hero, this.httpOptions).pipe(
|
||||||
|
tap((newHero: Hero) => this.log(`added hero w/ id=${newHero.id}`)),
|
||||||
return this.http.post<Hero>(this.heroesUrl, hero, httpOptions).pipe(
|
|
||||||
tap((hero: Hero) => this.log(`added hero w/ id=${hero.id}`)),
|
|
||||||
catchError(this.handleError<Hero>('addHero'))
|
catchError(this.handleError<Hero>('addHero'))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** DELETE: delete the hero from the server */
|
/** DELETE: delete the hero from the server */
|
||||||
deleteHero (hero: Hero | number): Observable<Hero> {
|
deleteHero(hero: Hero | number): Observable<Hero> {
|
||||||
const id = typeof hero === 'number' ? hero : hero.id;
|
const id = typeof hero === 'number' ? hero : hero.id;
|
||||||
const url = `${this.heroesUrl}/${id}`;
|
const url = `${this.heroesUrl}/${id}`;
|
||||||
|
|
||||||
return this.http.delete<Hero>(url, httpOptions).pipe(
|
return this.http.delete<Hero>(url, this.httpOptions).pipe(
|
||||||
tap(_ => this.log(`deleted hero id=${id}`)),
|
tap(_ => this.log(`deleted hero id=${id}`)),
|
||||||
catchError(this.handleError<Hero>('deleteHero'))
|
catchError(this.handleError<Hero>('deleteHero'))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** PUT: update the hero on the server */
|
/** PUT: update the hero on the server */
|
||||||
updateHero (hero: Hero): Observable<any> {
|
updateHero(hero: Hero): Observable<any> {
|
||||||
return this.http.put(this.heroesUrl, hero, httpOptions).pipe(
|
return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe(
|
||||||
tap(_ => this.log(`updated hero id=${hero.id}`)),
|
tap(_ => this.log(`updated hero id=${hero.id}`)),
|
||||||
catchError(this.handleError<any>('updateHero'))
|
catchError(this.handleError<any>('updateHero'))
|
||||||
);
|
);
|
||||||
@ -107,7 +102,7 @@ export class HeroService {
|
|||||||
* @param operation - name of the operation that failed
|
* @param operation - name of the operation that failed
|
||||||
* @param result - optional value to return as the observable result
|
* @param result - optional value to return as the observable result
|
||||||
*/
|
*/
|
||||||
private handleError<T> (operation = 'operation', result?: T) {
|
private handleError<T>(operation = 'operation', result?: T) {
|
||||||
return (error: any): Observable<T> => {
|
return (error: any): Observable<T> => {
|
||||||
|
|
||||||
// TODO: send the error to remote logging infrastructure
|
// TODO: send the error to remote logging infrastructure
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.heroes a {
|
.heroes a {
|
||||||
color: #888;
|
color: #333;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
@ -30,7 +30,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.heroes a:hover {
|
.heroes a:hover {
|
||||||
color:#607D8B;
|
color: #607D8B;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heroes .badge {
|
.heroes .badge {
|
||||||
@ -38,7 +38,7 @@
|
|||||||
font-size: small;
|
font-size: small;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 0.8em 0.7em 0 0.7em;
|
padding: 0.8em 0.7em 0 0.7em;
|
||||||
background-color: #607D8B;
|
background-color: #405061;
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
position: relative;
|
position: relative;
|
||||||
left: -1px;
|
left: -1px;
|
||||||
@ -50,7 +50,7 @@
|
|||||||
border-radius: 4px 0 0 4px;
|
border-radius: 4px 0 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
button {
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
|
@ -16,6 +16,6 @@
|
|||||||
<span class="badge">{{hero.id}}</span> {{hero.name}}
|
<span class="badge">{{hero.id}}</span> {{hero.name}}
|
||||||
</a>
|
</a>
|
||||||
<button class="delete" title="delete hero"
|
<button class="delete" title="delete hero"
|
||||||
(click)="delete(hero);$event.stopPropagation()">x</button>
|
(click)="delete(hero)">x</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -25,17 +25,15 @@ export class HeroesComponent implements OnInit {
|
|||||||
add(name: string): void {
|
add(name: string): void {
|
||||||
name = name.trim();
|
name = name.trim();
|
||||||
if (!name) { return; }
|
if (!name) { return; }
|
||||||
this.heroService.addHero(name)
|
this.heroService.addHero({ name } as Hero)
|
||||||
.subscribe(hero => {
|
.subscribe(hero => {
|
||||||
this.heroes.push(hero);
|
this.heroes.push(hero);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(hero: Hero): void {
|
delete(hero: Hero): void {
|
||||||
this.heroService.deleteHero(hero)
|
|
||||||
.subscribe(() => {
|
|
||||||
this.heroes = this.heroes.filter(h => h !== hero);
|
this.heroes = this.heroes.filter(h => h !== hero);
|
||||||
});
|
this.heroService.deleteHero(hero).subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
import { InMemoryDbService } from 'angular-in-memory-web-api';
|
import { InMemoryDbService } from 'angular-in-memory-web-api';
|
||||||
|
import { Hero } from './hero';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
export class InMemoryDataService implements InMemoryDbService {
|
export class InMemoryDataService implements InMemoryDbService {
|
||||||
createDb() {
|
createDb() {
|
||||||
const heroes = [
|
const heroes = [
|
||||||
@ -16,4 +21,13 @@ export class InMemoryDataService implements InMemoryDbService {
|
|||||||
];
|
];
|
||||||
return {heroes};
|
return {heroes};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Overrides the genId method to ensure that a hero always has an id.
|
||||||
|
// If the heroes array is empty,
|
||||||
|
// the method below returns the initial number (11).
|
||||||
|
// if the heroes array is not empty, the method below returns the highest
|
||||||
|
// hero id + 1.
|
||||||
|
genId(heroes: Hero[]): number {
|
||||||
|
return heroes.length > 0 ? Math.max(...heroes.map(hero => hero.id)) + 1 : 11;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable({ providedIn: 'root' })
|
||||||
export class MessageService {
|
export class MessageService {
|
||||||
messages: string[] = [];
|
messages: string[] = [];
|
||||||
|
|
||||||
|
@ -30,6 +30,6 @@ button:disabled {
|
|||||||
cursor: auto;
|
cursor: auto;
|
||||||
}
|
}
|
||||||
button.clear {
|
button.clear {
|
||||||
color: #888;
|
color: #333;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
@ -8,4 +8,7 @@ if (environment.production) {
|
|||||||
enableProdMode();
|
enableProdMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
});
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const webpack = require('webpack');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: { server: './server.ts' },
|
|
||||||
resolve: { extensions: ['.js', '.ts'] },
|
|
||||||
target: 'node',
|
|
||||||
mode: 'none',
|
|
||||||
// this makes sure we include node_modules and other 3rd party libraries
|
|
||||||
externals: [/node_modules/],
|
|
||||||
output: {
|
|
||||||
path: path.join(__dirname, 'dist'),
|
|
||||||
filename: '[name].js'
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [{ test: /\.ts$/, loader: 'ts-loader' }]
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
// Temporary Fix for issue: https://github.com/angular/angular/issues/11580
|
|
||||||
// for 'WARNING Critical dependency: the request of a dependency is an expression'
|
|
||||||
new webpack.ContextReplacementPlugin(
|
|
||||||
/(.+)?angular(\\|\/)core(.+)?/,
|
|
||||||
path.join(__dirname, 'src'), // location of your src
|
|
||||||
{} // a map of your routes
|
|
||||||
),
|
|
||||||
new webpack.ContextReplacementPlugin(
|
|
||||||
/(.+)?express(\\|\/)(.+)?/,
|
|
||||||
path.join(__dirname, 'src'),
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
};
|
|
@ -10,6 +10,12 @@ For an in-depth introduction to issues and techniques for designing accessible a
|
|||||||
This page discusses best practices for designing Angular applications that
|
This page discusses best practices for designing Angular applications that
|
||||||
work well for all users, including those who rely on assistive technologies.
|
work well for all users, including those who rely on assistive technologies.
|
||||||
|
|
||||||
|
<div class="alert is-helpful">
|
||||||
|
|
||||||
|
For the sample app that this page describes, see the <live-example></live-example>.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
## Accessibility attributes
|
## Accessibility attributes
|
||||||
|
|
||||||
Building accessible web experience often involves setting [ARIA attributes](https://developers.google.com/web/fundamentals/accessibility/semantics-aria)
|
Building accessible web experience often involves setting [ARIA attributes](https://developers.google.com/web/fundamentals/accessibility/semantics-aria)
|
||||||
@ -92,8 +98,6 @@ The following example shows how to make a simple progress bar accessible by usin
|
|||||||
<code-example path="accessibility/src/app/app.component.html" header="src/app/app.component.html" region="template"></code-example>
|
<code-example path="accessibility/src/app/app.component.html" header="src/app/app.component.html" region="template"></code-example>
|
||||||
|
|
||||||
|
|
||||||
To see the progress bar in a working example app, refer to the <live-example></live-example>.
|
|
||||||
|
|
||||||
## Routing and focus management
|
## Routing and focus management
|
||||||
|
|
||||||
Tracking and controlling [focus](https://developers.google.com/web/fundamentals/accessibility/focus/) in a UI is an important consideration in designing for accessibility.
|
Tracking and controlling [focus](https://developers.google.com/web/fundamentals/accessibility/focus/) in a UI is an important consideration in designing for accessibility.
|
||||||
|
@ -415,8 +415,8 @@ The following are some of the key AngularJS built-in directives and their equiva
|
|||||||
<code-example hideCopy path="ajs-quick-reference/src/app/app.component.html" region="router-link"></code-example>
|
<code-example hideCopy path="ajs-quick-reference/src/app/app.component.html" region="router-link"></code-example>
|
||||||
|
|
||||||
|
|
||||||
For more information on routing, see the [RouterLink binding](guide/router#router-link)
|
For more information on routing, see [Defining a basic route](guide/router#basic-route)
|
||||||
section of the [Routing & Navigation](guide/router) page.
|
in the [Routing & Navigation](guide/router) page.
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
@ -19,12 +19,17 @@ Both components and services are simply classes, with *decorators* that mark the
|
|||||||
|
|
||||||
An app's components typically define many views, arranged hierarchically. Angular provides the `Router` service to help you define navigation paths among views. The router provides sophisticated in-browser navigational capabilities.
|
An app's components typically define many views, arranged hierarchically. Angular provides the `Router` service to help you define navigation paths among views. The router provides sophisticated in-browser navigational capabilities.
|
||||||
|
|
||||||
<div class="alert is-helpful>
|
<div class="alert is-helpful">
|
||||||
|
|
||||||
See the [Angular Glossary](guide/glossary) for basic definitions of important Angular terms and usage.
|
See the [Angular Glossary](guide/glossary) for basic definitions of important Angular terms and usage.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="alert is-helpful">
|
||||||
|
|
||||||
|
For the sample app that this page describes, see the <live-example></live-example>.
|
||||||
|
</div>
|
||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
Angular *NgModules* differ from and complement JavaScript (ES2015) modules. An NgModule declares a compilation context for a set of components that is dedicated to an application domain, a workflow, or a closely related set of capabilities. An NgModule can associate its components with related code, such as services, to form functional units.
|
Angular *NgModules* differ from and complement JavaScript (ES2015) modules. An NgModule declares a compilation context for a set of components that is dedicated to an application domain, a workflow, or a closely related set of capabilities. An NgModule can associate its components with related code, such as services, to form functional units.
|
||||||
@ -148,10 +153,5 @@ Each of these subjects is introduced in more detail in the following pages.
|
|||||||
|
|
||||||
* [Introduction to services and dependency injection](guide/architecture-services)
|
* [Introduction to services and dependency injection](guide/architecture-services)
|
||||||
|
|
||||||
<div class="alert is-helpful">
|
|
||||||
|
|
||||||
Note that the code referenced on these pages is available as a <live-example></live-example>.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
When you're familiar with these fundamental building blocks, you can explore them in more detail in the documentation. To learn about more tools and techniques that are available to help you build and deploy Angular applications, see [Next steps: tools and techniques](guide/architecture-next-steps).
|
When you're familiar with these fundamental building blocks, you can explore them in more detail in the documentation. To learn about more tools and techniques that are available to help you build and deploy Angular applications, see [Next steps: tools and techniques](guide/architecture-next-steps).
|
||||||
</div>
|
</div>
|
||||||
|
@ -303,7 +303,7 @@ Some features of Angular may require additional polyfills.
|
|||||||
<td>
|
<td>
|
||||||
|
|
||||||
[Router](guide/router) when using
|
[Router](guide/router) when using
|
||||||
[hash-based routing](guide/router#appendix-locationstrategy-and-browser-url-styles)
|
[hash-based routing](guide/router#location-strategy)
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
|
@ -311,11 +311,11 @@ To use CSS grid with IE10/11, you must explicitly enable it using the `autoplace
|
|||||||
To do this, add the following to the top of the global styles file (or within a specific css selector scope):
|
To do this, add the following to the top of the global styles file (or within a specific css selector scope):
|
||||||
|
|
||||||
```
|
```
|
||||||
/* autoprefixer grid: autoplace /
|
/* autoprefixer grid: autoplace */
|
||||||
```
|
```
|
||||||
or
|
or
|
||||||
```
|
```
|
||||||
/ autoprefixer grid: no-autoplace */
|
/* autoprefixer grid: no-autoplace */
|
||||||
```
|
```
|
||||||
|
|
||||||
For more information, see [Autoprefixer documentation](https://autoprefixer.github.io/).
|
For more information, see [Autoprefixer documentation](https://autoprefixer.github.io/).
|
||||||
|
@ -321,7 +321,7 @@ absolutely must be present when the app starts.
|
|||||||
|
|
||||||
Configure the Angular Router to defer loading of all other modules (and their associated code), either by
|
Configure the Angular Router to defer loading of all other modules (and their associated code), either by
|
||||||
[waiting until the app has launched](guide/router#preloading "Preloading")
|
[waiting until the app has launched](guide/router#preloading "Preloading")
|
||||||
or by [_lazy loading_](guide/router#asynchronous-routing "Lazy loading")
|
or by [_lazy loading_](guide/router#lazy-loading "Lazy loading")
|
||||||
them on demand.
|
them on demand.
|
||||||
|
|
||||||
<div class="callout is-helpful">
|
<div class="callout is-helpful">
|
||||||
|
@ -318,6 +318,7 @@ const routes: Routes = [{
|
|||||||
|
|
||||||
|
|
||||||
{@a activatedroute-props}
|
{@a activatedroute-props}
|
||||||
|
|
||||||
### ActivatedRoute params and queryParams properties
|
### ActivatedRoute params and queryParams properties
|
||||||
|
|
||||||
[ActivatedRoute](api/router/ActivatedRoute) contains two [properties](api/router/ActivatedRoute#properties) that are less capable than their replacements and may be deprecated in a future Angular version.
|
[ActivatedRoute](api/router/ActivatedRoute) contains two [properties](api/router/ActivatedRoute#properties) that are less capable than their replacements and may be deprecated in a future Angular version.
|
||||||
@ -327,7 +328,7 @@ const routes: Routes = [{
|
|||||||
| `params` | `paramMap` |
|
| `params` | `paramMap` |
|
||||||
| `queryParams` | `queryParamMap` |
|
| `queryParams` | `queryParamMap` |
|
||||||
|
|
||||||
For more information see the [Router guide](guide/router#activated-route).
|
For more information see the [Getting route information](guide/router#activated-route) section of the [Router guide](guide/router).
|
||||||
|
|
||||||
|
|
||||||
{@a reflect-metadata}
|
{@a reflect-metadata}
|
||||||
|
@ -99,7 +99,7 @@ Project-specific [TypeScript](https://www.typescriptlang.org/) configuration fil
|
|||||||
|
|
||||||
| APPLICATION-SPECIFIC CONFIG FILES | PURPOSE |
|
| APPLICATION-SPECIFIC CONFIG FILES | PURPOSE |
|
||||||
| :--------------------- | :------------------------------------------|
|
| :--------------------- | :------------------------------------------|
|
||||||
| `.browserslistrc` | Configures sharing of target browsers and Node.js versions among various front-end tools. See [Browserslist on GitHub](https://github.com/browserslist/browserslist) for more information. |
|
| `browserslist` | Configures sharing of target browsers and Node.js versions among various front-end tools. See [Browserslist on GitHub](https://github.com/browserslist/browserslist) for more information. |
|
||||||
| `karma.conf.js` | Application-specific [Karma](https://karma-runner.github.io/2.0/config/configuration-file.html) configuration. |
|
| `karma.conf.js` | Application-specific [Karma](https://karma-runner.github.io/2.0/config/configuration-file.html) configuration. |
|
||||||
| `tsconfig.app.json` | Application-specific [TypeScript](https://www.typescriptlang.org/) configuration, including TypeScript and Angular template compiler options. See [TypeScript Configuration](guide/typescript-configuration) and [Angular Compiler Options](guide/angular-compiler-options). |
|
| `tsconfig.app.json` | Application-specific [TypeScript](https://www.typescriptlang.org/) configuration, including TypeScript and Angular template compiler options. See [TypeScript Configuration](guide/typescript-configuration) and [Angular Compiler Options](guide/angular-compiler-options). |
|
||||||
| `tsconfig.spec.json` | [TypeScript](https://www.typescriptlang.org/) configuration for the application tests. See [TypeScript Configuration](guide/typescript-configuration). |
|
| `tsconfig.spec.json` | [TypeScript](https://www.typescriptlang.org/) configuration for the application tests. See [TypeScript Configuration](guide/typescript-configuration). |
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -103,9 +103,8 @@ Version | Status | Released | Active Ends | LTS Ends
|
|||||||
------- | ------ | ------------ | ------------ | ------------
|
------- | ------ | ------------ | ------------ | ------------
|
||||||
^9.0.0 | Active | Feb 06, 2020 | Aug 06, 2020 | Aug 06, 2021
|
^9.0.0 | Active | Feb 06, 2020 | Aug 06, 2020 | Aug 06, 2021
|
||||||
^8.0.0 | LTS | May 28, 2019 | Nov 28, 2019 | Nov 28, 2020
|
^8.0.0 | LTS | May 28, 2019 | Nov 28, 2019 | Nov 28, 2020
|
||||||
^7.0.0 | LTS | Oct 18, 2018 | Apr 18, 2019 | Apr 18, 2020
|
|
||||||
|
|
||||||
Angular versions ^4.0.0, ^5.0.0 and ^6.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.
|
||||||
|
|
||||||
{@a deprecation}
|
{@a deprecation}
|
||||||
## Deprecation practices
|
## Deprecation practices
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1958,7 +1958,7 @@ for the `id` to change during its lifetime.
|
|||||||
|
|
||||||
<div class="alert is-helpful">
|
<div class="alert is-helpful">
|
||||||
|
|
||||||
The [Router](guide/router#route-parameters) guide covers `ActivatedRoute.paramMap` in more detail.
|
The [ActivatedRoute in action](guide/router#activated-route-in-action) section of the [Router](guide/router) guide covers `ActivatedRoute.paramMap` in more detail.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ The initial `tsconfig.json` for an Angular app typically looks like the followin
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"strictTemplates": true,
|
"fullTemplateTypeCheck": true,
|
||||||
"strictInjectionParameters": true
|
"strictInjectionParameters": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,6 +62,7 @@ The initial `tsconfig.json` for an Angular app typically looks like the followin
|
|||||||
|
|
||||||
{@a noImplicitAny}
|
{@a noImplicitAny}
|
||||||
|
|
||||||
|
|
||||||
### *noImplicitAny* and *suppressImplicitAnyIndexErrors*
|
### *noImplicitAny* and *suppressImplicitAnyIndexErrors*
|
||||||
|
|
||||||
TypeScript developers disagree about whether the `noImplicitAny` flag should be `true` or `false`.
|
TypeScript developers disagree about whether the `noImplicitAny` flag should be `true` or `false`.
|
||||||
@ -95,7 +96,6 @@ For more information about how the TypeScript configuration affects compilation,
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{@a typings}
|
{@a typings}
|
||||||
|
|
||||||
## TypeScript typings
|
## TypeScript typings
|
||||||
@ -146,6 +146,7 @@ For instance, to install typings for `jasmine` you run `npm install @types/jasmi
|
|||||||
|
|
||||||
{@a target}
|
{@a target}
|
||||||
|
|
||||||
|
|
||||||
### *target*
|
### *target*
|
||||||
|
|
||||||
By default, the target is `es2015`, which is supported only in modern browsers. You can configure the target to `es5` to specifically support legacy browsers. [Differential loading](guide/deployment#differential-loading) is also provided by the Angular CLI to support modern, and legacy browsers with separate bundles.
|
By default, the target is `es2015`, which is supported only in modern browsers. You can configure the target to `es5` to specifically support legacy browsers. [Differential loading](guide/deployment#differential-loading) is also provided by the Angular CLI to support modern, and legacy browsers with separate bundles.
|
||||||
|
@ -15,7 +15,7 @@ The CLI schematic `@nguniversal/express-engine` performs the required steps, as
|
|||||||
|
|
||||||
<div class="alert is-helpful">
|
<div class="alert is-helpful">
|
||||||
|
|
||||||
**Note:** [Download the finished sample code](generated/zips/universal/universal.zip),
|
**Note:** <live-example downloadOnly>Download the finished sample code</live-example>,
|
||||||
which runs in a [Node.js® Express](https://expressjs.com/) server.
|
which runs in a [Node.js® Express](https://expressjs.com/) server.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -27,7 +27,7 @@ The [Tour of Heroes tutorial](tutorial) is the foundation for this walkthrough.
|
|||||||
|
|
||||||
In this example, the Angular CLI compiles and bundles the Universal version of the app with the
|
In this example, the Angular CLI compiles and bundles the Universal version of the app with the
|
||||||
[Ahead-of-Time (AOT) compiler](guide/aot-compiler).
|
[Ahead-of-Time (AOT) compiler](guide/aot-compiler).
|
||||||
A Node Express web server compiles HTML pages with Universal based on client requests.
|
A Node.js Express web server compiles HTML pages with Universal based on client requests.
|
||||||
|
|
||||||
To create the server-side app module, `app.server.module.ts`, run the following CLI command.
|
To create the server-side app module, `app.server.module.ts`, run the following CLI command.
|
||||||
|
|
||||||
@ -62,10 +62,10 @@ The files marked with `*` are new and not in the original tutorial sample.
|
|||||||
To start rendering your app with Universal on your local system, use the following command.
|
To start rendering your app with Universal on your local system, use the following command.
|
||||||
|
|
||||||
<code-example language="bash">
|
<code-example language="bash">
|
||||||
npm run build:ssr && npm run serve:ssr
|
npm run dev:ssr
|
||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
Open a browser and navigate to http://localhost:4000/.
|
Open a browser and navigate to http://localhost:4200/.
|
||||||
You should see the familiar Tour of Heroes dashboard page.
|
You should see the familiar Tour of Heroes dashboard page.
|
||||||
|
|
||||||
Navigation via `routerLinks` works correctly because they use the native anchor (`<a>`) tags.
|
Navigation via `routerLinks` works correctly because they use the native anchor (`<a>`) tags.
|
||||||
@ -158,13 +158,12 @@ The sample web server for this guide is based on the popular [Express](https://e
|
|||||||
Universal applications use the Angular `platform-server` package (as opposed to `platform-browser`), which provides
|
Universal applications use the Angular `platform-server` package (as opposed to `platform-browser`), which provides
|
||||||
server implementations of the DOM, `XMLHttpRequest`, and other low-level features that don't rely on a browser.
|
server implementations of the DOM, `XMLHttpRequest`, and other low-level features that don't rely on a browser.
|
||||||
|
|
||||||
The server ([Node Express](https://expressjs.com/) in this guide's example)
|
The server ([Node.js Express](https://expressjs.com/) in this guide's example)
|
||||||
passes client requests for application pages to the NgUniversal `ngExpressEngine`. Under the hood, this
|
passes client requests for application pages to the NgUniversal `ngExpressEngine`. Under the hood, this
|
||||||
calls Universal's `renderModule()` function, while providing caching and other helpful utilities.
|
calls Universal's `renderModule()` function, while providing caching and other helpful utilities.
|
||||||
|
|
||||||
The `renderModule()` function takes as inputs a *template* HTML page (usually `index.html`),
|
The `renderModule()` function takes as inputs a *template* HTML page (usually `index.html`),
|
||||||
an Angular *module* containing components,
|
an Angular *module* containing components, and a *route* that determines which components to display.
|
||||||
and a *route* that determines which components to display.
|
|
||||||
The route comes from the client's request to the server.
|
The route comes from the client's request to the server.
|
||||||
|
|
||||||
Each request results in the appropriate view for the requested route.
|
Each request results in the appropriate view for the requested route.
|
||||||
@ -188,71 +187,6 @@ Similarly, without mouse or keyboard events, a server-side app can't rely on a u
|
|||||||
The app must determine what to render based solely on the incoming client request.
|
The app must determine what to render based solely on the incoming client request.
|
||||||
This is a good argument for making the app [routable](guide/router).
|
This is a good argument for making the app [routable](guide/router).
|
||||||
|
|
||||||
{@a http-urls}
|
|
||||||
### Using absolute URLs for server requests
|
|
||||||
|
|
||||||
The tutorial's `HeroService` and `HeroSearchService` delegate to the Angular `HttpClient` module to fetch application data.
|
|
||||||
These services send requests to _relative_ URLs such as `api/heroes`.
|
|
||||||
In a Universal app, HTTP URLs must be _absolute_ (for example, `https://my-server.com/api/heroes`).
|
|
||||||
This means you need to change your services to make requests with absolute URLs when running on the server and with relative
|
|
||||||
URLs when running in the browser.
|
|
||||||
|
|
||||||
One solution is to provide the full URL to your application on the server, and write an interceptor that can retrieve this
|
|
||||||
value and prepend it to the request URL. If you're using the `ngExpressEngine`, as shown in the example in this guide, half
|
|
||||||
the work is already done. We'll assume this is the case, but it's trivial to provide the same functionality.
|
|
||||||
|
|
||||||
Start by creating an [HttpInterceptor](api/common/http/HttpInterceptor).
|
|
||||||
|
|
||||||
<code-example language="typescript" header="universal-interceptor.ts">
|
|
||||||
|
|
||||||
import {Injectable, Inject, Optional} from '@angular/core';
|
|
||||||
import {HttpInterceptor, HttpHandler, HttpRequest, HttpHeaders} from '@angular/common/http';
|
|
||||||
import {Request} from 'express';
|
|
||||||
import {REQUEST} from '@nguniversal/express-engine/tokens';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class UniversalInterceptor implements HttpInterceptor {
|
|
||||||
|
|
||||||
constructor(@Optional() @Inject(REQUEST) protected request?: Request) {}
|
|
||||||
|
|
||||||
intercept(req: HttpRequest<any>, next: HttpHandler) {
|
|
||||||
let serverReq: HttpRequest<any> = req;
|
|
||||||
if (this.request) {
|
|
||||||
let newUrl = `${this.request.protocol}://${this.request.get('host')}`;
|
|
||||||
if (!req.url.startsWith('/')) {
|
|
||||||
newUrl += '/';
|
|
||||||
}
|
|
||||||
newUrl += req.url;
|
|
||||||
serverReq = req.clone({url: newUrl});
|
|
||||||
}
|
|
||||||
return next.handle(serverReq);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</code-example>
|
|
||||||
|
|
||||||
Next, provide the interceptor in the providers for the server `AppModule`.
|
|
||||||
|
|
||||||
<code-example language="typescript" header="app.server.module.ts">
|
|
||||||
|
|
||||||
import {HTTP_INTERCEPTORS} from '@angular/common/http';
|
|
||||||
import {UniversalInterceptor} from './universal-interceptor';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
...
|
|
||||||
providers: [{
|
|
||||||
provide: HTTP_INTERCEPTORS,
|
|
||||||
useClass: UniversalInterceptor,
|
|
||||||
multi: true
|
|
||||||
}],
|
|
||||||
})
|
|
||||||
export class AppServerModule {}
|
|
||||||
|
|
||||||
</code-example>
|
|
||||||
|
|
||||||
Now, on every HTTP request made on the server, this interceptor will fire and replace the request URL with the absolute
|
|
||||||
URL provided in the Express `Request` object.
|
|
||||||
|
|
||||||
{@a universal-engine}
|
{@a universal-engine}
|
||||||
### Universal template engine
|
### Universal template engine
|
||||||
|
|
||||||
@ -262,16 +196,10 @@ The important bit in the `server.ts` file is the `ngExpressEngine()` function.
|
|||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
The `ngExpressEngine()` function is a wrapper around Universal's `renderModule()` function which turns a client's
|
The `ngExpressEngine()` function is a wrapper around Universal's `renderModule()` function which turns a client's
|
||||||
requests into server-rendered HTML pages.
|
requests into server-rendered HTML pages. It accepts an object with the following properties:
|
||||||
|
|
||||||
* The first parameter is `AppServerModule`.
|
* `bootstrap`: The root `NgModule` or `NgModule` factory to use for bootstraping the app when rendering on the server. For the example app, it is `AppServerModule`. It's the bridge between the Universal server-side renderer and the Angular application.
|
||||||
It's the bridge between the Universal server-side renderer and the Angular application.
|
* `extraProviders`: This is optional and lets you specify dependency providers that apply only when rendering the app on the server. You can do this when your app needs information that can only be determined by the currently running server instance.
|
||||||
|
|
||||||
* The second parameter, `extraProviders`, is optional. It lets you specify dependency providers that apply only when
|
|
||||||
running on this server.
|
|
||||||
You can do this when your app needs information that can only be determined by the currently running server instance.
|
|
||||||
One example could be the running server's *origin*, which could be used to [calculate absolute HTTP URLs](#http-urls) if
|
|
||||||
not using the `Request` token as shown above.
|
|
||||||
|
|
||||||
The `ngExpressEngine()` function returns a `Promise` callback that resolves to the rendered page.
|
The `ngExpressEngine()` function returns a `Promise` callback that resolves to the rendered page.
|
||||||
It's up to the engine to decide what to do with that page.
|
It's up to the engine to decide what to do with that page.
|
||||||
@ -287,7 +215,7 @@ which then forwards it to the client in the HTTP response.
|
|||||||
|
|
||||||
### Filtering request URLs
|
### Filtering request URLs
|
||||||
|
|
||||||
NOTE: the basic behavior described below is handled automatically when using the NgUniversal Express schematic, this
|
NOTE: The basic behavior described below is handled automatically when using the NgUniversal Express schematic. This
|
||||||
is helpful when trying to understand the underlying behavior or replicate it without using the schematic.
|
is helpful when trying to understand the underlying behavior or replicate it without using the schematic.
|
||||||
|
|
||||||
The web server must distinguish _app page requests_ from other kinds of requests.
|
The web server must distinguish _app page requests_ from other kinds of requests.
|
||||||
@ -307,8 +235,8 @@ Because we use routing, we can easily recognize the three types of requests and
|
|||||||
1. **App navigation**: request URL with no file extension.
|
1. **App navigation**: request URL with no file extension.
|
||||||
1. **Static asset**: all other requests.
|
1. **Static asset**: all other requests.
|
||||||
|
|
||||||
A Node Express server is a pipeline of middleware that filters and processes requests one after the other.
|
A Node.js Express server is a pipeline of middleware that filters and processes requests one after the other.
|
||||||
You configure the Node Express server pipeline with calls to `app.get()` like this one for data requests.
|
You configure the Node.js Express server pipeline with calls to `server.get()` like this one for data requests.
|
||||||
|
|
||||||
<code-example path="universal/server.ts" header="server.ts (data URL)" region="data-request"></code-example>
|
<code-example path="universal/server.ts" header="server.ts (data URL)" region="data-request"></code-example>
|
||||||
|
|
||||||
@ -328,13 +256,32 @@ The following code filters for request URLs with no extensions and treats them a
|
|||||||
|
|
||||||
### Serving static files safely
|
### Serving static files safely
|
||||||
|
|
||||||
A single `app.use()` treats all other URLs as requests for static assets
|
A single `server.use()` treats all other URLs as requests for static assets
|
||||||
such as JavaScript, image, and style files.
|
such as JavaScript, image, and style files.
|
||||||
|
|
||||||
To ensure that clients can only download the files that they are permitted to see, put all client-facing asset files in
|
To ensure that clients can only download the files that they are permitted to see, put all client-facing asset files in
|
||||||
the `/dist` folder and only honor requests for files from the `/dist` folder.
|
the `/dist` folder and only honor requests for files from the `/dist` folder.
|
||||||
|
|
||||||
The following Node Express code routes all remaining requests to `/dist`, and returns a `404 - NOT FOUND` error if the
|
The following Node.js Express code routes all remaining requests to `/dist`, and returns a `404 - NOT FOUND` error if the
|
||||||
file isn't found.
|
file isn't found.
|
||||||
|
|
||||||
<code-example path="universal/server.ts" header="server.ts (static files)" region="static"></code-example>
|
<code-example path="universal/server.ts" header="server.ts (static files)" region="static"></code-example>
|
||||||
|
|
||||||
|
### Using absolute URLs for HTTP (data) requests on the server
|
||||||
|
|
||||||
|
The tutorial's `HeroService` and `HeroSearchService` delegate to the Angular `HttpClient` module to fetch application data.
|
||||||
|
These services send requests to _relative_ URLs such as `api/heroes`.
|
||||||
|
In a server-side rendered app, HTTP URLs must be _absolute_ (for example, `https://my-server.com/api/heroes`).
|
||||||
|
This means that the URLs must be somehow converted to absolute when running on the server and be left relative when running in the browser.
|
||||||
|
|
||||||
|
If you are using one of the `@nguniversal/*-engine` packages (such as `@nguniversal/express-engine`), this is taken care for you automatically.
|
||||||
|
You don't need to do anything to make relative URLs work on the server.
|
||||||
|
|
||||||
|
If, for some reason, you are not using an `@nguniversal/*-engine` package, you may need to handle it yourself.
|
||||||
|
|
||||||
|
The recommended solution is to pass the full request URL to the `options` argument of [renderModule()](api/platform-server/renderModule) or [renderModuleFactory()](api/platform-server/renderModuleFactory) (depending on what you use to render `AppServerModule` on the server).
|
||||||
|
This option is the least intrusive as it does not require any changes to the app.
|
||||||
|
Here, "request URL" refers to the URL of the request as a response to which the app is being rendered on the server.
|
||||||
|
For example, if the client requested `https://my-server.com/dashboard` and you are rendering the app on the server to respond to that request, `options.url` should be set to `https://my-server.com/dashboard`.
|
||||||
|
|
||||||
|
Now, on every HTTP request made as part of rendering the app on the server, Angular can correctly resolve the request URL to an absolute URL, using the provided `options.url`.
|
||||||
|
BIN
aio/content/images/bios/annieyw.jpg
Normal file
BIN
aio/content/images/bios/annieyw.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
Before Width: | Height: | Size: 7.0 KiB |
BIN
aio/content/images/bios/rockument69.jpg
Normal file
BIN
aio/content/images/bios/rockument69.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 132 KiB |
@ -44,15 +44,6 @@
|
|||||||
"groups": ["Angular"],
|
"groups": ["Angular"],
|
||||||
"lead": "juleskremer"
|
"lead": "juleskremer"
|
||||||
},
|
},
|
||||||
"robwormald": {
|
|
||||||
"name": "Rob Wormald",
|
|
||||||
"picture": "rob-wormald.jpg",
|
|
||||||
"twitter": "robwormald",
|
|
||||||
"website": "http://github.com/robwormald",
|
|
||||||
"bio": "Rob is a Developer Advocate on the Angular team at Google. He's the Angular team's resident reactive programming geek and founded the Reactive Extensions for Angular project, ngrx.",
|
|
||||||
"groups": ["Angular"],
|
|
||||||
"lead": "stephenfluin"
|
|
||||||
},
|
|
||||||
"alexeagle": {
|
"alexeagle": {
|
||||||
"name": "Alex Eagle",
|
"name": "Alex Eagle",
|
||||||
"picture": "alex-eagle.jpg",
|
"picture": "alex-eagle.jpg",
|
||||||
@ -667,6 +658,13 @@
|
|||||||
"groups": ["Angular"],
|
"groups": ["Angular"],
|
||||||
"lead": "dennispbrown"
|
"lead": "dennispbrown"
|
||||||
},
|
},
|
||||||
|
"rockument69": {
|
||||||
|
"name": "Tony Bove",
|
||||||
|
"picture": "rockument69.jpg",
|
||||||
|
"bio": "Tony is a technical writer with Expert Support. His lifelong passions are helping people use technology, writing fiction, and playing music. When he's not working or playing the harmonica with friends in a bluegrass band, he's swimming and snorkeling on a Kauai beach and playing ball with his Irish Wolfhound. He's worked at home for decades before it became a thing.",
|
||||||
|
"groups": ["Angular"],
|
||||||
|
"lead": "aikidave"
|
||||||
|
},
|
||||||
"kapunahelewong": {
|
"kapunahelewong": {
|
||||||
"name": "Kapunahele Wong",
|
"name": "Kapunahele Wong",
|
||||||
"picture": "kapunahele.jpg",
|
"picture": "kapunahele.jpg",
|
||||||
@ -835,5 +833,12 @@
|
|||||||
"bio": "Manu heads technical program management for Angular at Google. Manu keeps the big picture in focus and works with cross-functional teams to plan, execute and usher programs through the entire lifecycle.",
|
"bio": "Manu heads technical program management for Angular at Google. Manu keeps the big picture in focus and works with cross-functional teams to plan, execute and usher programs through the entire lifecycle.",
|
||||||
"groups": ["Angular"],
|
"groups": ["Angular"],
|
||||||
"lead": "juleskremer"
|
"lead": "juleskremer"
|
||||||
|
},
|
||||||
|
"anneiyw": {
|
||||||
|
"name": "Annie Wang",
|
||||||
|
"picture": "annieyw.jpg",
|
||||||
|
"bio": "Annie is an engineering resident on the Angular Components team at Google. She is passionate about the intersection between design and technology and enjoys drawing in her free time.",
|
||||||
|
"groups": ["Angular"],
|
||||||
|
"lead": "jelbourn"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -102,6 +102,12 @@ This section walks you through using the cart service to add a product to the ca
|
|||||||
<code-example header="src/app/product-details/product-details.component.html" path="getting-started/src/app/product-details/product-details.component.html">
|
<code-example header="src/app/product-details/product-details.component.html" path="getting-started/src/app/product-details/product-details.component.html">
|
||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
|
<div class="alert is-helpful">
|
||||||
|
|
||||||
|
The line, `<h4>{{ product.price | currency }}</h4>` uses the `currency` pipe to transform `product.price` from a number to a currency string. A pipe is a way you can transform data in your HTML template. For more information about Angular pipes, see [Pipes](guide/pipes "Pipes").
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
1. To see the new "Buy" button, refresh the application and click on a product's name to display its details.
|
1. To see the new "Buy" button, refresh the application and click on a product's name to display its details.
|
||||||
|
|
||||||
<div class="lightbox">
|
<div class="lightbox">
|
||||||
|
@ -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 56c648827",
|
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js 31ac61357",
|
||||||
"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",
|
||||||
|
@ -12,4 +12,4 @@ source ../scripts/ci/payload-size.sh
|
|||||||
# Provide node_modules from aio
|
# Provide node_modules from aio
|
||||||
NODE_MODULES_BIN=$PROJECT_ROOT/aio/node_modules/.bin/
|
NODE_MODULES_BIN=$PROJECT_ROOT/aio/node_modules/.bin/
|
||||||
|
|
||||||
trackPayloadSize "$target" "dist/*.js" true "${thisDir}/_payload-limits.json"
|
trackPayloadSize "$target" "dist/*.js" true "$PROJECT_ROOT/goldens/size-tracking/aio-payloads.json"
|
||||||
|
@ -18,7 +18,6 @@
|
|||||||
],
|
],
|
||||||
"devDependencies": [
|
"devDependencies": [
|
||||||
"@angular/compiler-cli",
|
"@angular/compiler-cli",
|
||||||
"@angular/platform-server",
|
|
||||||
"@types/jasmine",
|
"@types/jasmine",
|
||||||
"@types/node",
|
"@types/node",
|
||||||
"jasmine-core",
|
"jasmine-core",
|
||||||
|
@ -1,27 +1,29 @@
|
|||||||
{
|
{
|
||||||
"scripts": [
|
"scripts": [
|
||||||
{ "name": "ng", "command": "ng" },
|
{ "name": "ng", "command": "ng" },
|
||||||
|
{ "name": "build", "command": "ng build" },
|
||||||
{ "name": "start", "command": "ng serve" },
|
{ "name": "start", "command": "ng serve" },
|
||||||
{ "name": "test", "command": "ng test" },
|
{ "name": "test", "command": "ng test" },
|
||||||
{ "name": "lint", "command": "ng lint" },
|
{ "name": "lint", "command": "ng lint" },
|
||||||
{ "name": "e2e", "command": "ng e2e" },
|
{ "name": "e2e", "command": "ng e2e" },
|
||||||
{ "name": "build:ssr", "command": "npm run build:client-and-server-bundles && npm run webpack:server" },
|
{ "name": "dev:ssr", "command": "ng run angular.io-example:serve-ssr" },
|
||||||
{ "name": "serve:ssr", "command": "node dist/server.js" },
|
{ "name": "build:ssr", "command": "ng build --prod && ng run angular.io-example:server:production" },
|
||||||
{ "name": "build:client-and-server-bundles", "command": "ng build --prod && ng run angular.io-example:server" },
|
{ "name": "serve:ssr", "command": "node dist/server/main.js" },
|
||||||
{ "name": "webpack:server", "command": "webpack --config webpack.server.config.js --progress --colors" }
|
{ "name": "prerender", "command": "ng run angular.io-example:prerender" }
|
||||||
],
|
],
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
"@angular/platform-server",
|
||||||
"@nguniversal/express-engine",
|
"@nguniversal/express-engine",
|
||||||
"@nguniversal/module-map-ngfactory-loader"
|
"express"
|
||||||
],
|
],
|
||||||
"devDependencies": [
|
"devDependencies": [
|
||||||
"@angular-devkit/build-angular",
|
"@angular-devkit/build-angular",
|
||||||
"@angular/cli",
|
"@angular/cli",
|
||||||
|
"@nguniversal/builders",
|
||||||
|
"@types/express",
|
||||||
"@types/jasminewd2",
|
"@types/jasminewd2",
|
||||||
"jasmine-spec-reporter",
|
"jasmine-spec-reporter",
|
||||||
"karma-coverage-istanbul-reporter",
|
"karma-coverage-istanbul-reporter",
|
||||||
"ts-loader",
|
"ts-node"
|
||||||
"ts-node",
|
|
||||||
"webpack-cli"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -46,5 +46,5 @@ The specific changes to each project type are listed below:
|
|||||||
- Includes a `server` target in the `build` architect runners
|
- Includes a `server` target in the `build` architect runners
|
||||||
- package.json
|
- package.json
|
||||||
- Includes custom scripts for building the `server`
|
- Includes custom scripts for building the `server`
|
||||||
- Includes additional `dependencies` on `@nguniversal/common`, `@nguniversal/express-engine`, and `@nguniversal/module-map-ngfactory-loader`
|
- Includes additional `dependencies` on `@angular/platform-server`, `@nguniversal/express-engine`, and `express`
|
||||||
- Includes `devDependencies` on `@angular/platform-server`, and `ts-loader`
|
- Includes additional `devDependencies` on `@nguniversal/builders` and `@types/express`
|
||||||
|
@ -122,8 +122,47 @@
|
|||||||
"builder": "@angular-devkit/build-angular:server",
|
"builder": "@angular-devkit/build-angular:server",
|
||||||
"options": {
|
"options": {
|
||||||
"outputPath": "dist/server",
|
"outputPath": "dist/server",
|
||||||
"main": "src/main.server.ts",
|
"main": "server.ts",
|
||||||
"tsConfig": "tsconfig.server.json"
|
"tsConfig": "tsconfig.server.json"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"outputHashing": "media",
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sourceMap": false,
|
||||||
|
"optimization": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serve-ssr": {
|
||||||
|
"builder": "@nguniversal/builders:ssr-dev-server",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "angular.io-example:build",
|
||||||
|
"serverTarget": "angular.io-example:server"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"browserTarget": "angular.io-example:build:production",
|
||||||
|
"serverTarget": "angular.io-example:server:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prerender": {
|
||||||
|
"builder": "@nguniversal/builders:prerender",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "angular.io-example:build:production",
|
||||||
|
"serverTarget": "angular.io-example:server:production",
|
||||||
|
"routes": [
|
||||||
|
"/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,10 @@
|
|||||||
"test": "ng test",
|
"test": "ng test",
|
||||||
"lint": "ng lint",
|
"lint": "ng lint",
|
||||||
"e2e": "ng e2e",
|
"e2e": "ng e2e",
|
||||||
"build:ssr": "npm run build:client-and-server-bundles && npm run webpack:server",
|
"dev:ssr": "ng run angular.io-example:serve-ssr",
|
||||||
"serve:ssr": "node dist/server.js",
|
"serve:ssr": "node dist/server/main.js",
|
||||||
"build:client-and-server-bundles": "ng build --prod && ng run angular.io-example:server",
|
"build:ssr": "ng build --prod && ng run angular.io-example:server:production",
|
||||||
"webpack:server": "webpack --config webpack.server.config.js --progress --colors"
|
"prerender": "ng run angular.io-example:prerender"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -23,12 +23,11 @@
|
|||||||
"@angular/forms": "~9.0.6",
|
"@angular/forms": "~9.0.6",
|
||||||
"@angular/platform-browser": "~9.0.6",
|
"@angular/platform-browser": "~9.0.6",
|
||||||
"@angular/platform-browser-dynamic": "~9.0.6",
|
"@angular/platform-browser-dynamic": "~9.0.6",
|
||||||
|
"@angular/platform-server": "~9.0.6",
|
||||||
"@angular/router": "~9.0.6",
|
"@angular/router": "~9.0.6",
|
||||||
"@nguniversal/common": "~9.0.1",
|
|
||||||
"@nguniversal/express-engine": "~9.0.1",
|
"@nguniversal/express-engine": "~9.0.1",
|
||||||
"@nguniversal/module-map-ngfactory-loader": "~9.0.0-next.9",
|
|
||||||
"angular-in-memory-web-api": "~0.9.0",
|
"angular-in-memory-web-api": "~0.9.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.15.2",
|
||||||
"rxjs": "~6.5.4",
|
"rxjs": "~6.5.4",
|
||||||
"tslib": "^1.10.0",
|
"tslib": "^1.10.0",
|
||||||
"zone.js": "~0.10.3"
|
"zone.js": "~0.10.3"
|
||||||
@ -38,8 +37,8 @@
|
|||||||
"@angular/cli": "~9.0.6",
|
"@angular/cli": "~9.0.6",
|
||||||
"@angular/compiler-cli": "~9.0.6",
|
"@angular/compiler-cli": "~9.0.6",
|
||||||
"@angular/language-service": "~9.0.6",
|
"@angular/language-service": "~9.0.6",
|
||||||
"@angular/platform-server": "~9.0.6",
|
"@nguniversal/builders": "^9.0.2",
|
||||||
"@types/express": "^4.17.2",
|
"@types/express": "^4.17.0",
|
||||||
"@types/jasmine": "~3.5.0",
|
"@types/jasmine": "~3.5.0",
|
||||||
"@types/jasminewd2": "~2.0.3",
|
"@types/jasminewd2": "~2.0.3",
|
||||||
"@types/node": "^12.11.1",
|
"@types/node": "^12.11.1",
|
||||||
@ -53,10 +52,8 @@
|
|||||||
"karma-jasmine": "~2.0.1",
|
"karma-jasmine": "~2.0.1",
|
||||||
"karma-jasmine-html-reporter": "^1.4.2",
|
"karma-jasmine-html-reporter": "^1.4.2",
|
||||||
"protractor": "~5.4.3",
|
"protractor": "~5.4.3",
|
||||||
"ts-loader": "^6.2.1",
|
|
||||||
"ts-node": "~8.3.0",
|
"ts-node": "~8.3.0",
|
||||||
"tslint": "~5.18.0",
|
"tslint": "~5.18.0",
|
||||||
"typescript": "~3.7.5",
|
"typescript": "~3.7.5"
|
||||||
"webpack-cli": "^3.3.10"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,18 +28,17 @@
|
|||||||
"@angular/forms": "~9.0.6",
|
"@angular/forms": "~9.0.6",
|
||||||
"@angular/platform-browser": "~9.0.6",
|
"@angular/platform-browser": "~9.0.6",
|
||||||
"@angular/platform-browser-dynamic": "~9.0.6",
|
"@angular/platform-browser-dynamic": "~9.0.6",
|
||||||
|
"@angular/platform-server": "~9.0.6",
|
||||||
"@angular/router": "~9.0.6",
|
"@angular/router": "~9.0.6",
|
||||||
"@angular/service-worker": "~9.0.6",
|
"@angular/service-worker": "~9.0.6",
|
||||||
"@angular/upgrade": "~9.0.6",
|
"@angular/upgrade": "~9.0.6",
|
||||||
"@nguniversal/common": "~9.0.1",
|
|
||||||
"@nguniversal/express-engine": "~9.0.1",
|
"@nguniversal/express-engine": "~9.0.1",
|
||||||
"@nguniversal/module-map-ngfactory-loader": "~9.0.0-next.9",
|
|
||||||
"@webcomponents/custom-elements": "^1.4.1",
|
"@webcomponents/custom-elements": "^1.4.1",
|
||||||
"angular": "1.7.9",
|
"angular": "1.7.9",
|
||||||
"angular-in-memory-web-api": "~0.9.0",
|
"angular-in-memory-web-api": "~0.9.0",
|
||||||
"angular-route": "1.7.9",
|
"angular-route": "1.7.9",
|
||||||
"core-js": "^2.5.4",
|
"core-js": "^2.5.4",
|
||||||
"express": "^4.17.1",
|
"express": "^4.15.2",
|
||||||
"rxjs": "~6.5.4",
|
"rxjs": "~6.5.4",
|
||||||
"systemjs": "0.19.39",
|
"systemjs": "0.19.39",
|
||||||
"tslib": "^1.10.0",
|
"tslib": "^1.10.0",
|
||||||
@ -50,13 +49,13 @@
|
|||||||
"@angular/cli": "~9.0.6",
|
"@angular/cli": "~9.0.6",
|
||||||
"@angular/compiler-cli": "~9.0.6",
|
"@angular/compiler-cli": "~9.0.6",
|
||||||
"@angular/language-service": "~9.0.6",
|
"@angular/language-service": "~9.0.6",
|
||||||
"@angular/platform-server": "~9.0.6",
|
"@nguniversal/builders": "^9.0.2",
|
||||||
"@types/angular": "1.6.47",
|
"@types/angular": "1.6.47",
|
||||||
"@types/angular-animate": "1.5.10",
|
"@types/angular-animate": "1.5.10",
|
||||||
"@types/angular-mocks": "1.6.0",
|
"@types/angular-mocks": "1.6.0",
|
||||||
"@types/angular-resource": "1.5.14",
|
"@types/angular-resource": "1.5.14",
|
||||||
"@types/angular-route": "1.3.5",
|
"@types/angular-route": "1.3.5",
|
||||||
"@types/express": "4.0.35",
|
"@types/express": "^4.17.0",
|
||||||
"@types/jasmine": "~3.5.0",
|
"@types/jasmine": "~3.5.0",
|
||||||
"@types/jasminewd2": "~2.0.3",
|
"@types/jasminewd2": "~2.0.3",
|
||||||
"@types/jquery": "3.3.28",
|
"@types/jquery": "3.3.28",
|
||||||
@ -83,10 +82,8 @@
|
|||||||
"rollup-plugin-node-resolve": "^4.0.0",
|
"rollup-plugin-node-resolve": "^4.0.0",
|
||||||
"rollup-plugin-uglify": "^1.0.1",
|
"rollup-plugin-uglify": "^1.0.1",
|
||||||
"source-map-explorer": "^1.3.2",
|
"source-map-explorer": "^1.3.2",
|
||||||
"ts-loader": "^6.2.1",
|
|
||||||
"ts-node": "~8.3.0",
|
"ts-node": "~8.3.0",
|
||||||
"tslint": "~5.18.0",
|
"tslint": "~5.18.0",
|
||||||
"typescript": "~3.7.5",
|
"typescript": "~3.7.5"
|
||||||
"webpack-cli": "^3.3.10"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -28,56 +28,100 @@ module.exports = function autoLinkCode(getDocFromAlias) {
|
|||||||
return autoLinkCodeImpl;
|
return autoLinkCodeImpl;
|
||||||
|
|
||||||
function autoLinkCodeImpl() {
|
function autoLinkCodeImpl() {
|
||||||
return (ast) => {
|
return (ast, file) => {
|
||||||
visit(ast, 'element', (node, ancestors) => {
|
visit(ast, 'element', (node, ancestors) => {
|
||||||
// Only interested in code elements that:
|
if (!isValidCodeElement(node, ancestors)) {
|
||||||
// * do not have `no-auto-link` class
|
return;
|
||||||
// * do not have an ignored language
|
}
|
||||||
// * are not inside links
|
|
||||||
if (autoLinkCodeImpl.codeElements.some(elementType => is(node, elementType)) &&
|
|
||||||
(!node.properties.className || !node.properties.className.includes('no-auto-link')) &&
|
|
||||||
!autoLinkCodeImpl.ignoredLanguages.includes(node.properties.language) &&
|
|
||||||
ancestors.every(ancestor => !is(ancestor, 'a'))) {
|
|
||||||
visit(node, 'text', (node, ancestors) => {
|
visit(node, 'text', (node, ancestors) => {
|
||||||
// Only interested in text nodes that are not inside links
|
const isInLink = isInsideLink(ancestors);
|
||||||
if (ancestors.every(ancestor => !is(ancestor, 'a'))) {
|
if (isInLink) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const parent = ancestors[ancestors.length - 1];
|
const parent = ancestors[ancestors.length - 1];
|
||||||
const index = parent.children.indexOf(node);
|
const index = parent.children.indexOf(node);
|
||||||
|
|
||||||
// Can we convert the whole text node into a doc link?
|
// Can we convert the whole text node into a doc link?
|
||||||
const docs = getDocFromAlias(node.value);
|
const docs = getDocFromAlias(node.value);
|
||||||
if (foundValidDoc(docs)) {
|
if (foundValidDoc(docs, node.value, file)) {
|
||||||
parent.children.splice(index, 1, createLinkNode(docs[0], node.value));
|
parent.children.splice(index, 1, createLinkNode(docs[0], node.value));
|
||||||
} else {
|
} else {
|
||||||
// Parse the text for words that we can convert to links
|
// Parse the text for words that we can convert to links
|
||||||
const nodes =
|
const nodes = getNodes(node, file);
|
||||||
textContent(node)
|
// Replace the text node with the links and leftover text nodes
|
||||||
|
Array.prototype.splice.apply(parent.children, [index, 1].concat(nodes));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidCodeElement(node, ancestors) {
|
||||||
|
// Only interested in code elements that:
|
||||||
|
// * do not have `no-auto-link` class
|
||||||
|
// * do not have an ignored language
|
||||||
|
// * are not inside links
|
||||||
|
const isCodeElement = autoLinkCodeImpl.codeElements.some(elementType => is(node, elementType));
|
||||||
|
const hasNoAutoLink = node.properties.className && node.properties.className.includes('no-auto-link');
|
||||||
|
const isLanguageSupported = !autoLinkCodeImpl.ignoredLanguages.includes(node.properties.language);
|
||||||
|
const isInLink = isInsideLink(ancestors);
|
||||||
|
return isCodeElement && !hasNoAutoLink && isLanguageSupported && !isInLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInsideLink(ancestors) {
|
||||||
|
return ancestors.some(ancestor => is(ancestor, 'a'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodes(node, file) {
|
||||||
|
return textContent(node)
|
||||||
.split(/([A-Za-z0-9_.-]+)/)
|
.split(/([A-Za-z0-9_.-]+)/)
|
||||||
.filter(word => word.length)
|
.filter(word => word.length)
|
||||||
.map((word, index, words) => {
|
.map((word, index, words) => {
|
||||||
// remove docs that fail the custom filter tests
|
// remove docs that fail the custom filter tests
|
||||||
const filteredDocs = autoLinkCodeImpl.customFilters.reduce(
|
const filteredDocs = autoLinkCodeImpl.customFilters.reduce(
|
||||||
(docs, filter) => filter(docs, words, index), getDocFromAlias(word));
|
(docs, filter) => filter(docs, words, index), getDocFromAlias(word));
|
||||||
return foundValidDoc(filteredDocs) ?
|
|
||||||
|
return foundValidDoc(filteredDocs, word, file) ?
|
||||||
// Create a link wrapping the text node.
|
// Create a link wrapping the text node.
|
||||||
createLinkNode(filteredDocs[0], word) :
|
createLinkNode(filteredDocs[0], word) :
|
||||||
// this is just text so push a new text node
|
// this is just text so push a new text node
|
||||||
{type: 'text', value: word};
|
{type: 'text', value: word};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replace the text node with the links and leftover text nodes
|
|
||||||
Array.prototype.splice.apply(parent.children, [index, 1].concat(nodes));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function foundValidDoc(docs) {
|
/**
|
||||||
return docs.length === 1 && !docs[0].internal &&
|
* Validates the docs to be used to generate the links. The validation ensures
|
||||||
autoLinkCodeImpl.docTypes.indexOf(docs[0].docType) !== -1;
|
* that the docs are not `internal` and that the `docType` is supported. The `path`
|
||||||
|
* can be empty when the `API` is not public.
|
||||||
|
*
|
||||||
|
* @param {Array<Object>} docs An array of objects containing the doc details
|
||||||
|
*
|
||||||
|
* @param {string} keyword The keyword the doc applies to
|
||||||
|
*/
|
||||||
|
function foundValidDoc(docs, keyword, file) {
|
||||||
|
if (docs.length !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var doc = docs[0];
|
||||||
|
|
||||||
|
const isInvalidDoc = doc.docType === 'member' && !keyword.includes('.');
|
||||||
|
if (isInvalidDoc) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.path === '') {
|
||||||
|
var message = `
|
||||||
|
autoLinkCode: Doc path is empty for "${doc.id}" - link will not be generated for "${keyword}".
|
||||||
|
Please make sure if the doc should be public. If not, it should probably not be referenced in the docs.`;
|
||||||
|
|
||||||
|
file.message(message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !doc.internal && autoLinkCodeImpl.docTypes.includes(doc.docType);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLinkNode(doc, text) {
|
function createLinkNode(doc, text) {
|
||||||
|
@ -126,6 +126,24 @@ describe('autoLinkCode post-processor', () => {
|
|||||||
expect(doc.renderedContent).toEqual('<code>MyClass</code>');
|
expect(doc.renderedContent).toEqual('<code>MyClass</code>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should ignore code items that match an API doc but have no path set',
|
||||||
|
() => {
|
||||||
|
aliasMap.addDoc(
|
||||||
|
{docType: 'class', id: 'MyClass', aliases: ['MyClass'], path: ''});
|
||||||
|
const doc = {docType: 'test-doc', renderedContent: '<code>MyClass</code>'};
|
||||||
|
processor.$process([doc]);
|
||||||
|
expect(doc.renderedContent).toEqual('<code>MyClass</code>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore documents when the `docType` is set to `member` and the keyword doesn\'t include `.`',
|
||||||
|
() => {
|
||||||
|
aliasMap.addDoc(
|
||||||
|
{docType: 'member', id: 'MyEnum', aliases: ['MyEnum'], path: 'a/b/c'});
|
||||||
|
const doc = {docType: 'test-doc', renderedContent: '<code>MyEnum</code>'};
|
||||||
|
processor.$process([doc]);
|
||||||
|
expect(doc.renderedContent).toEqual('<code>MyEnum</code>');
|
||||||
|
});
|
||||||
|
|
||||||
it('should insert anchors for individual text nodes within a code block', () => {
|
it('should insert anchors for individual text nodes within a code block', () => {
|
||||||
aliasMap.addDoc({docType: 'class', id: 'MyClass', aliases: ['MyClass'], path: 'a/b/myclass'});
|
aliasMap.addDoc({docType: 'class', id: 'MyClass', aliases: ['MyClass'], path: 'a/b/myclass'});
|
||||||
const doc = {
|
const doc = {
|
||||||
|
@ -5,6 +5,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = function ignoreGenericWords() {
|
module.exports = function ignoreGenericWords() {
|
||||||
const ignoredWords = new Set(['a', 'classes', 'create', 'error', 'group', 'request', 'target', 'value']);
|
const ignoredWords = new Set(['a', 'classes', 'create', 'error', 'group', 'request', 'target', 'value', '_']);
|
||||||
return (docs, words, index) => ignoredWords.has(words[index].toLowerCase()) ? [] : docs;
|
return (docs, words, index) => ignoredWords.has(words[index].toLowerCase()) ? [] : docs;
|
||||||
};
|
};
|
||||||
|
@ -9,9 +9,10 @@ ts_library(
|
|||||||
module_name = "@angular/dev-infra-private",
|
module_name = "@angular/dev-infra-private",
|
||||||
deps = [
|
deps = [
|
||||||
"//dev-infra/commit-message",
|
"//dev-infra/commit-message",
|
||||||
|
"//dev-infra/format",
|
||||||
"//dev-infra/pullapprove",
|
"//dev-infra/pullapprove",
|
||||||
"//dev-infra/ts-circular-dependencies",
|
"//dev-infra/ts-circular-dependencies",
|
||||||
"//dev-infra/utils:config",
|
"//dev-infra/utils",
|
||||||
"@npm//@types/node",
|
"@npm//@types/node",
|
||||||
"@npm//@types/yargs",
|
"@npm//@types/yargs",
|
||||||
"@npm//yargs",
|
"@npm//yargs",
|
||||||
|
@ -10,6 +10,7 @@ import * as yargs from 'yargs';
|
|||||||
import {tsCircularDependenciesBuilder} from './ts-circular-dependencies/index';
|
import {tsCircularDependenciesBuilder} from './ts-circular-dependencies/index';
|
||||||
import {buildPullapproveParser} from './pullapprove/cli';
|
import {buildPullapproveParser} from './pullapprove/cli';
|
||||||
import {buildCommitMessageParser} from './commit-message/cli';
|
import {buildCommitMessageParser} from './commit-message/cli';
|
||||||
|
import {buildFormatParser} from './format/cli';
|
||||||
|
|
||||||
yargs.scriptName('ng-dev')
|
yargs.scriptName('ng-dev')
|
||||||
.demandCommand()
|
.demandCommand()
|
||||||
@ -17,6 +18,7 @@ yargs.scriptName('ng-dev')
|
|||||||
.command('ts-circular-deps <command>', '', tsCircularDependenciesBuilder)
|
.command('ts-circular-deps <command>', '', tsCircularDependenciesBuilder)
|
||||||
.command('pullapprove <command>', '', buildPullapproveParser)
|
.command('pullapprove <command>', '', buildPullapproveParser)
|
||||||
.command('commit-message <command>', '', buildCommitMessageParser)
|
.command('commit-message <command>', '', buildCommitMessageParser)
|
||||||
|
.command('format <command>', '', buildFormatParser)
|
||||||
.wrap(120)
|
.wrap(120)
|
||||||
.strict()
|
.strict()
|
||||||
.parse();
|
.parse();
|
||||||
|
@ -13,7 +13,7 @@ ts_library(
|
|||||||
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:config",
|
"//dev-infra/utils",
|
||||||
"@npm//@types/node",
|
"@npm//@types/node",
|
||||||
"@npm//@types/shelljs",
|
"@npm//@types/shelljs",
|
||||||
"@npm//@types/yargs",
|
"@npm//@types/yargs",
|
||||||
@ -29,7 +29,7 @@ ts_library(
|
|||||||
srcs = ["validate.spec.ts"],
|
srcs = ["validate.spec.ts"],
|
||||||
deps = [
|
deps = [
|
||||||
":commit-message",
|
":commit-message",
|
||||||
"//dev-infra/utils:config",
|
"//dev-infra/utils",
|
||||||
"@npm//@types/events",
|
"@npm//@types/events",
|
||||||
"@npm//@types/jasmine",
|
"@npm//@types/jasmine",
|
||||||
"@npm//@types/node",
|
"@npm//@types/node",
|
||||||
|
@ -11,6 +11,9 @@ import {parseCommitMessage, validateCommitMessage, ValidateCommitMessageOptions}
|
|||||||
// Whether the provided commit is a fixup commit.
|
// Whether the provided commit is a fixup commit.
|
||||||
const isNonFixup = (m: string) => !parseCommitMessage(m).isFixup;
|
const isNonFixup = (m: string) => !parseCommitMessage(m).isFixup;
|
||||||
|
|
||||||
|
// Extracts commit header (first line of commit message).
|
||||||
|
const extractCommitHeader = (m: string) => parseCommitMessage(m).header;
|
||||||
|
|
||||||
/** Validate all commits in a provided git commit range. */
|
/** Validate all commits in a provided git commit range. */
|
||||||
export function validateCommitRange(range: string) {
|
export function validateCommitRange(range: string) {
|
||||||
// A random value is used as a string to allow for a definite split point in the git log result.
|
// A random value is used as a string to allow for a definite split point in the git log result.
|
||||||
@ -35,11 +38,18 @@ export function validateCommitRange(range: string) {
|
|||||||
const allCommitsInRangeValid = commits.every((m, i) => {
|
const allCommitsInRangeValid = commits.every((m, i) => {
|
||||||
const options: ValidateCommitMessageOptions = {
|
const options: ValidateCommitMessageOptions = {
|
||||||
disallowSquash: true,
|
disallowSquash: true,
|
||||||
nonFixupCommitHeaders: isNonFixup(m) ? undefined : commits.slice(0, i).filter(isNonFixup)
|
nonFixupCommitHeaders: isNonFixup(m) ?
|
||||||
|
undefined :
|
||||||
|
commits.slice(0, i).filter(isNonFixup).map(extractCommitHeader)
|
||||||
};
|
};
|
||||||
return validateCommitMessage(m, options);
|
return validateCommitMessage(m, options);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (allCommitsInRangeValid) {
|
if (allCommitsInRangeValid) {
|
||||||
console.info('√ All commit messages in range valid.');
|
console.info('√ All commit messages in range valid.');
|
||||||
|
} else {
|
||||||
|
// Exit with a non-zero exit code if invalid commit messages have
|
||||||
|
// been discovered.
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -160,21 +160,12 @@ describe('validate-commit-message.js', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('(squash)', () => {
|
describe('(squash)', () => {
|
||||||
it('should strip the `squash! ` prefix and validate the rest', () => {
|
describe('without `disallowSquash`', () => {
|
||||||
const errorMessage = `The commit message header does not match the expected format.`;
|
it('should return commits as valid', () => {
|
||||||
|
|
||||||
// Valid messages.
|
|
||||||
expect(validateCommitMessage('squash! feat(core): add feature')).toBe(VALID);
|
expect(validateCommitMessage('squash! feat(core): add feature')).toBe(VALID);
|
||||||
expect(validateCommitMessage('squash! fix: a bug', {disallowSquash: false})).toBe(VALID);
|
expect(validateCommitMessage('squash! fix: a bug')).toBe(VALID);
|
||||||
|
expect(validateCommitMessage('squash! fix a typo')).toBe(VALID);
|
||||||
// Invalid messages.
|
});
|
||||||
expect(validateCommitMessage('squash! fix a typo', {disallowSquash: false})).toBe(INVALID);
|
|
||||||
expect(lastError).toContain('squash! fix a typo');
|
|
||||||
expect(lastError).toContain(errorMessage);
|
|
||||||
|
|
||||||
expect(validateCommitMessage('squash! squash! fix: a bug')).toBe(INVALID);
|
|
||||||
expect(lastError).toContain('squash! squash! fix: a bug');
|
|
||||||
expect(lastError).toContain(errorMessage);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with `disallowSquash`', () => {
|
describe('with `disallowSquash`', () => {
|
||||||
@ -191,21 +182,10 @@ describe('validate-commit-message.js', () => {
|
|||||||
|
|
||||||
describe('(fixup)', () => {
|
describe('(fixup)', () => {
|
||||||
describe('without `nonFixupCommitHeaders`', () => {
|
describe('without `nonFixupCommitHeaders`', () => {
|
||||||
it('should strip the `fixup! ` prefix and validate the rest', () => {
|
it('should return commits as valid', () => {
|
||||||
const errorMessage = `The commit message header does not match the expected format.`;
|
|
||||||
|
|
||||||
// Valid messages.
|
|
||||||
expect(validateCommitMessage('fixup! feat(core): add feature')).toBe(VALID);
|
expect(validateCommitMessage('fixup! feat(core): add feature')).toBe(VALID);
|
||||||
expect(validateCommitMessage('fixup! fix: a bug')).toBe(VALID);
|
expect(validateCommitMessage('fixup! fix: a bug')).toBe(VALID);
|
||||||
|
expect(validateCommitMessage('fixup! fixup! fix: a bug')).toBe(VALID);
|
||||||
// Invalid messages.
|
|
||||||
expect(validateCommitMessage('fixup! fix a typo')).toBe(INVALID);
|
|
||||||
expect(lastError).toContain('fixup! fix a typo');
|
|
||||||
expect(lastError).toContain(errorMessage);
|
|
||||||
|
|
||||||
expect(validateCommitMessage('fixup! fixup! fix: a bug')).toBe(INVALID);
|
|
||||||
expect(lastError).toContain('fixup! fixup! fix: a bug');
|
|
||||||
expect(lastError).toContain(errorMessage);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ const SQUASH_PREFIX_RE = /^squash! /i;
|
|||||||
const REVERT_PREFIX_RE = /^revert:? /i;
|
const REVERT_PREFIX_RE = /^revert:? /i;
|
||||||
const TYPE_SCOPE_RE = /^(\w+)(?:\(([^)]+)\))?\:\s(.+)$/;
|
const TYPE_SCOPE_RE = /^(\w+)(?:\(([^)]+)\))?\:\s(.+)$/;
|
||||||
const COMMIT_HEADER_RE = /^(.*)/i;
|
const COMMIT_HEADER_RE = /^(.*)/i;
|
||||||
const COMMIT_BODY_RE = /^.*\n\n(.*)/i;
|
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) {
|
export function parseCommitMessage(commitMsg: string) {
|
||||||
@ -79,20 +79,32 @@ export function validateCommitMessage(
|
|||||||
const config = getAngularDevConfig<'commitMessage', CommitMessageConfig>().commitMessage;
|
const config = getAngularDevConfig<'commitMessage', CommitMessageConfig>().commitMessage;
|
||||||
const commit = parseCommitMessage(commitMsg);
|
const commit = parseCommitMessage(commitMsg);
|
||||||
|
|
||||||
|
////////////////////////////////////
|
||||||
|
// Checking revert, squash, fixup //
|
||||||
|
////////////////////////////////////
|
||||||
|
|
||||||
|
// All revert commits are considered valid.
|
||||||
if (commit.isRevert) {
|
if (commit.isRevert) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commit.isSquash && options.disallowSquash) {
|
// All squashes are considered valid, as the commit will be squashed into another in
|
||||||
|
// the git history anyway, unless the options provided to not allow squash commits.
|
||||||
|
if (commit.isSquash) {
|
||||||
|
if (options.disallowSquash) {
|
||||||
error('The commit must be manually squashed into the target commit');
|
error('The commit must be manually squashed into the target commit');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// If it is a fixup commit and `nonFixupCommitHeaders` is not empty, we only care to check whether
|
// Fixups commits are considered valid, unless nonFixupCommitHeaders are provided to check
|
||||||
// there is a corresponding non-fixup commit (i.e. a commit whose header is identical to this
|
// against. If `nonFixupCommitHeaders` is not empty, we check whether there is a corresponding
|
||||||
// commit's header after stripping the `fixup! ` prefix).
|
// non-fixup commit (i.e. a commit whose header is identical to this commit's header after
|
||||||
if (commit.isFixup && options.nonFixupCommitHeaders) {
|
// stripping the `fixup! ` prefix), otherwise we assume this verification will happen in another
|
||||||
if (!options.nonFixupCommitHeaders.includes(commit.header)) {
|
// check.
|
||||||
|
if (commit.isFixup) {
|
||||||
|
if (options.nonFixupCommitHeaders && !options.nonFixupCommitHeaders.includes(commit.header)) {
|
||||||
error(
|
error(
|
||||||
'Unable to find match for fixup commit among prior commits: ' +
|
'Unable to find match for fixup commit among prior commits: ' +
|
||||||
(options.nonFixupCommitHeaders.map(x => `\n ${x}`).join('') || '-'));
|
(options.nonFixupCommitHeaders.map(x => `\n ${x}`).join('') || '-'));
|
||||||
@ -102,6 +114,9 @@ export function validateCommitMessage(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////
|
||||||
|
// Checking commit header //
|
||||||
|
////////////////////////////
|
||||||
if (commit.header.length > config.maxLineLength) {
|
if (commit.header.length > config.maxLineLength) {
|
||||||
error(`The commit message header is longer than ${config.maxLineLength} characters`);
|
error(`The commit message header is longer than ${config.maxLineLength} characters`);
|
||||||
return false;
|
return false;
|
||||||
@ -122,6 +137,10 @@ export function validateCommitMessage(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//////////////////////////
|
||||||
|
// Checking commit body //
|
||||||
|
//////////////////////////
|
||||||
|
|
||||||
if (commit.bodyWithoutLinking.trim().length < config.minBodyLength) {
|
if (commit.bodyWithoutLinking.trim().length < config.minBodyLength) {
|
||||||
error(`The commit message body does not meet the minimum length of ${
|
error(`The commit message body does not meet the minimum length of ${
|
||||||
config.minBodyLength} characters`);
|
config.minBodyLength} characters`);
|
||||||
|
27
dev-infra/format/BUILD.bazel
Normal file
27
dev-infra/format/BUILD.bazel
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "format",
|
||||||
|
srcs = [
|
||||||
|
"cli.ts",
|
||||||
|
"config.ts",
|
||||||
|
"format.ts",
|
||||||
|
"run-commands-parallel.ts",
|
||||||
|
],
|
||||||
|
module_name = "@angular/dev-infra-private/format",
|
||||||
|
visibility = ["//dev-infra:__subpackages__"],
|
||||||
|
deps = [
|
||||||
|
"//dev-infra/utils",
|
||||||
|
"@npm//@types/cli-progress",
|
||||||
|
"@npm//@types/inquirer",
|
||||||
|
"@npm//@types/node",
|
||||||
|
"@npm//@types/shelljs",
|
||||||
|
"@npm//@types/yargs",
|
||||||
|
"@npm//cli-progress",
|
||||||
|
"@npm//inquirer",
|
||||||
|
"@npm//multimatch",
|
||||||
|
"@npm//shelljs",
|
||||||
|
"@npm//tslib",
|
||||||
|
"@npm//yargs",
|
||||||
|
],
|
||||||
|
)
|
45
dev-infra/format/cli.ts
Normal file
45
dev-infra/format/cli.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. 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 yargs from 'yargs';
|
||||||
|
|
||||||
|
import {allChangedFilesSince, allFiles} from '../utils/repo-files';
|
||||||
|
|
||||||
|
import {checkFiles, formatFiles} from './format';
|
||||||
|
|
||||||
|
/** Build the parser for the format commands. */
|
||||||
|
export function buildFormatParser(localYargs: yargs.Argv) {
|
||||||
|
return localYargs.help()
|
||||||
|
.strict()
|
||||||
|
.demandCommand()
|
||||||
|
.option('check', {
|
||||||
|
type: 'boolean',
|
||||||
|
default: process.env['CI'] ? true : false,
|
||||||
|
description: 'Run the formatter to check formatting rather than updating code format'
|
||||||
|
})
|
||||||
|
.command(
|
||||||
|
'all', 'Run the formatter on all files in the repository', {},
|
||||||
|
({check}) => {
|
||||||
|
const executionCmd = check ? checkFiles : formatFiles;
|
||||||
|
executionCmd(allFiles());
|
||||||
|
})
|
||||||
|
.command(
|
||||||
|
'changed [shaOrRef]', 'Run the formatter on files changed since the provided sha/ref', {},
|
||||||
|
({shaOrRef, check}) => {
|
||||||
|
const sha = shaOrRef || 'master';
|
||||||
|
const executionCmd = check ? checkFiles : formatFiles;
|
||||||
|
executionCmd(allChangedFilesSince(sha));
|
||||||
|
})
|
||||||
|
.command('files <files..>', 'Run the formatter on provided files', {}, ({check, files}) => {
|
||||||
|
const executionCmd = check ? checkFiles : formatFiles;
|
||||||
|
executionCmd(files);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
buildFormatParser(yargs).parse();
|
||||||
|
}
|
11
dev-infra/format/config.ts
Normal file
11
dev-infra/format/config.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface FormatConfig {
|
||||||
|
matchers: string[];
|
||||||
|
}
|
130
dev-infra/format/format.ts
Normal file
130
dev-infra/format/format.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. 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 {prompt} from 'inquirer';
|
||||||
|
import * as multimatch from 'multimatch';
|
||||||
|
import {join} from 'path';
|
||||||
|
|
||||||
|
import {getAngularDevConfig, getRepoBaseDir} from '../utils/config';
|
||||||
|
|
||||||
|
import {FormatConfig} from './config';
|
||||||
|
import {runInParallel} from './run-commands-parallel';
|
||||||
|
|
||||||
|
/** By default, run the formatter on all javascript and typescript files. */
|
||||||
|
const DEFAULT_MATCHERS = ['**/*.{t,j}s'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format provided files in place.
|
||||||
|
*/
|
||||||
|
export async function formatFiles(unfilteredFiles: string[]) {
|
||||||
|
// Whether any files failed to format.
|
||||||
|
let formatFailed = false;
|
||||||
|
// All files which formatting should be applied to.
|
||||||
|
const files = filterFilesByMatchers(unfilteredFiles);
|
||||||
|
|
||||||
|
console.info(`Formatting ${files.length} file(s)`);
|
||||||
|
|
||||||
|
|
||||||
|
// Run the formatter to format the files in place, split across (number of available
|
||||||
|
// cpu threads - 1) processess. The task is done in multiple processess to speed up
|
||||||
|
// the overall time of the task, as running across entire repositories takes a large
|
||||||
|
// amount of time.
|
||||||
|
// As a data point for illustration, using 8 process rather than 1 cut the execution
|
||||||
|
// time from 276 seconds to 39 seconds for the same 2700 files
|
||||||
|
await runInParallel(files, `${getFormatterBinary()} -i -style=file`, (file, code, _, stderr) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
formatFailed = true;
|
||||||
|
console.error(`Error running clang-format on: ${file}`);
|
||||||
|
console.error(stderr);
|
||||||
|
console.error();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// The process should exit as a failure if any of the files failed to format.
|
||||||
|
if (formatFailed) {
|
||||||
|
console.error(`Formatting failed, see errors above for more information.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.info(`√ Formatting complete.`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check provided files for formatting correctness.
|
||||||
|
*/
|
||||||
|
export async function checkFiles(unfilteredFiles: string[]) {
|
||||||
|
// All files which formatting should be applied to.
|
||||||
|
const files = filterFilesByMatchers(unfilteredFiles);
|
||||||
|
// Files which are currently not formatted correctly.
|
||||||
|
const failures: string[] = [];
|
||||||
|
|
||||||
|
console.info(`Checking format of ${files.length} file(s)`);
|
||||||
|
|
||||||
|
// Run the formatter to check the format of files, split across (number of available
|
||||||
|
// cpu threads - 1) processess. The task is done in multiple processess to speed up
|
||||||
|
// the overall time of the task, as running across entire repositories takes a large
|
||||||
|
// amount of time.
|
||||||
|
// As a data point for illustration, using 8 process rather than 1 cut the execution
|
||||||
|
// time from 276 seconds to 39 seconds for the same 2700 files.
|
||||||
|
await runInParallel(files, `${getFormatterBinary()} --Werror -n -style=file`, (file, code) => {
|
||||||
|
// Add any files failing format checks to the list.
|
||||||
|
if (code !== 0) {
|
||||||
|
failures.push(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (failures.length) {
|
||||||
|
// Provide output expressing which files are failing formatting.
|
||||||
|
console.group('\nThe following files are out of format:');
|
||||||
|
for (const file of failures) {
|
||||||
|
console.info(` - ${file}`);
|
||||||
|
}
|
||||||
|
console.groupEnd();
|
||||||
|
console.info();
|
||||||
|
|
||||||
|
// If the command is run in a non-CI environment, prompt to format the files immediately.
|
||||||
|
let runFormatter = false;
|
||||||
|
if (!process.env['CI']) {
|
||||||
|
runFormatter = (await prompt({
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'runFormatter',
|
||||||
|
message: 'Format the files now?',
|
||||||
|
})).runFormatter;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runFormatter) {
|
||||||
|
// Format the failing files as requested.
|
||||||
|
await formatFiles(failures);
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
// Inform user how to format files in the future.
|
||||||
|
console.info();
|
||||||
|
console.info(`To format the failing file run the following command:`);
|
||||||
|
console.info(` yarn ng-dev format files ${failures.join(' ')}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.info('√ All files correctly formatted.');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the full path of the formatter binary to execute. */
|
||||||
|
function getFormatterBinary() {
|
||||||
|
return join(getRepoBaseDir(), 'node_modules/.bin/clang-format');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Filter a list of files to only contain files which are expected to be formatted. */
|
||||||
|
function filterFilesByMatchers(allFiles: string[]) {
|
||||||
|
const matchers =
|
||||||
|
getAngularDevConfig<'format', FormatConfig>().format.matchers || DEFAULT_MATCHERS;
|
||||||
|
const files = multimatch(allFiles, matchers, {dot: true});
|
||||||
|
|
||||||
|
console.info(`Formatting enforced on ${files.length} of ${allFiles.length} file(s)`);
|
||||||
|
return files;
|
||||||
|
}
|
77
dev-infra/format/run-commands-parallel.ts
Normal file
77
dev-infra/format/run-commands-parallel.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. 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 {Bar} from 'cli-progress';
|
||||||
|
import {cpus} from 'os';
|
||||||
|
import {exec} from 'shelljs';
|
||||||
|
|
||||||
|
const AVAILABLE_THREADS = Math.max(cpus().length - 1, 1);
|
||||||
|
|
||||||
|
type CallbackFunction = (file: string, code?: number, stdout?: string, stderr?: string) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the provided commands in parallel for each provided file.
|
||||||
|
*
|
||||||
|
* A promise is returned, completed when the command has completed running for each file.
|
||||||
|
*/
|
||||||
|
export function runInParallel(providedFiles: string[], cmd: string, callback: CallbackFunction) {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
if (providedFiles.length === 0) {
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
// The progress bar instance to use for progress tracking.
|
||||||
|
const progressBar =
|
||||||
|
new Bar({format: `[{bar}] ETA: {eta}s | {value}/{total} files`, clearOnComplete: true});
|
||||||
|
// A local copy of the files to run the command on.
|
||||||
|
const files = providedFiles.slice();
|
||||||
|
// An array to represent the current usage state of each of the threads for parallelization.
|
||||||
|
const threads = new Array<boolean>(AVAILABLE_THREADS).fill(false);
|
||||||
|
|
||||||
|
// Recursively run the command on the next available file from the list using the provided
|
||||||
|
// thread.
|
||||||
|
function runCommandInThread(thread: number) {
|
||||||
|
// Get the next file.
|
||||||
|
const file = files.pop();
|
||||||
|
// If no file was pulled from the array, return as there are no more files to run against.
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
exec(
|
||||||
|
`${cmd} ${file}`,
|
||||||
|
{async: true, silent: true},
|
||||||
|
(code, stdout, stderr) => {
|
||||||
|
// Run the provided callback function.
|
||||||
|
callback(file, code, stdout, stderr);
|
||||||
|
// Note in the progress bar another file being completed.
|
||||||
|
progressBar.increment(1);
|
||||||
|
// If more files exist in the list, run again to work on the next file,
|
||||||
|
// using the same slot.
|
||||||
|
if (files.length) {
|
||||||
|
return runCommandInThread(thread);
|
||||||
|
}
|
||||||
|
// If not more files are available, mark the thread as unused.
|
||||||
|
threads[thread] = false;
|
||||||
|
// If all of the threads are false, as they are unused, mark the progress bar
|
||||||
|
// completed and resolve the promise.
|
||||||
|
if (threads.every(active => !active)) {
|
||||||
|
progressBar.stop();
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Mark the thread as in use as the command execution has been started.
|
||||||
|
threads[thread] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the progress bar
|
||||||
|
progressBar.start(files.length, 0);
|
||||||
|
// Start running the command on files from the least in each available thread.
|
||||||
|
threads.forEach((_, idx) => runCommandInThread(idx));
|
||||||
|
});
|
||||||
|
}
|
@ -4,6 +4,7 @@ ts_library(
|
|||||||
name = "pullapprove",
|
name = "pullapprove",
|
||||||
srcs = [
|
srcs = [
|
||||||
"cli.ts",
|
"cli.ts",
|
||||||
|
"condition_evaluator.ts",
|
||||||
"group.ts",
|
"group.ts",
|
||||||
"logging.ts",
|
"logging.ts",
|
||||||
"parse-yaml.ts",
|
"parse-yaml.ts",
|
||||||
@ -12,7 +13,7 @@ ts_library(
|
|||||||
module_name = "@angular/dev-infra-private/pullapprove",
|
module_name = "@angular/dev-infra-private/pullapprove",
|
||||||
visibility = ["//dev-infra:__subpackages__"],
|
visibility = ["//dev-infra:__subpackages__"],
|
||||||
deps = [
|
deps = [
|
||||||
"//dev-infra/utils:config",
|
"//dev-infra/utils",
|
||||||
"@npm//@types/minimatch",
|
"@npm//@types/minimatch",
|
||||||
"@npm//@types/node",
|
"@npm//@types/node",
|
||||||
"@npm//@types/shelljs",
|
"@npm//@types/shelljs",
|
||||||
|
@ -10,8 +10,11 @@ import {verify} from './verify';
|
|||||||
|
|
||||||
/** Build the parser for the pullapprove commands. */
|
/** Build the parser for the pullapprove commands. */
|
||||||
export function buildPullapproveParser(localYargs: yargs.Argv) {
|
export function buildPullapproveParser(localYargs: yargs.Argv) {
|
||||||
return localYargs.help().strict().demandCommand().command(
|
return localYargs.help()
|
||||||
'verify', 'Verify the pullapprove config', {}, () => verify());
|
.strict()
|
||||||
|
.option('verbose', {alias: ['v'], description: 'Enable verbose logging'})
|
||||||
|
.demandCommand()
|
||||||
|
.command('verify', 'Verify the pullapprove config', {}, ({verbose}) => verify(verbose));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
|
99
dev-infra/pullapprove/condition_evaluator.ts
Normal file
99
dev-infra/pullapprove/condition_evaluator.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. 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 {IMinimatch, Minimatch} from 'minimatch';
|
||||||
|
|
||||||
|
/** Map that holds patterns and their corresponding Minimatch globs. */
|
||||||
|
const patternCache = new Map<string, IMinimatch>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context that is provided to conditions. Conditions can use various helpers
|
||||||
|
* that PullApprove provides. We try to mock them here. Consult the official
|
||||||
|
* docs for more details: https://docs.pullapprove.com/config/conditions.
|
||||||
|
*/
|
||||||
|
const conditionContext = {
|
||||||
|
'len': (value: any[]) => value.length,
|
||||||
|
'contains_any_globs': (files: PullApproveArray, patterns: string[]) => {
|
||||||
|
// Note: Do not always create globs for the same pattern again. This method
|
||||||
|
// could be called for each source file. Creating glob's is expensive.
|
||||||
|
return files.some(f => patterns.some(pattern => getOrCreateGlob(pattern).match(f)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a given condition to a function that accepts a set of files. The returned
|
||||||
|
* function can be called to check if the set of files matches the condition.
|
||||||
|
*/
|
||||||
|
export function convertConditionToFunction(expr: string): (files: string[]) => boolean {
|
||||||
|
// Creates a dynamic function with the specified expression. The first parameter will
|
||||||
|
// be `files` as that corresponds to the supported `files` variable that can be accessed
|
||||||
|
// in PullApprove condition expressions. The followed parameters correspond to other
|
||||||
|
// context variables provided by PullApprove for conditions.
|
||||||
|
const evaluateFn = new Function('files', ...Object.keys(conditionContext), `
|
||||||
|
return (${transformExpressionToJs(expr)});
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create a function that calls the dynamically constructed function which mimics
|
||||||
|
// the condition expression that is usually evaluated with Python in PullApprove.
|
||||||
|
return files => {
|
||||||
|
const result = evaluateFn(new PullApproveArray(...files), ...Object.values(conditionContext));
|
||||||
|
// If an array is returned, we consider the condition as active if the array is not
|
||||||
|
// empty. This matches PullApprove's condition evaluation that is based on Python.
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
return result.length !== 0;
|
||||||
|
}
|
||||||
|
return !!result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms a condition expression from PullApprove that is based on python
|
||||||
|
* so that it can be run inside JavaScript. Current transformations:
|
||||||
|
* 1. `not <..>` -> `!<..>`
|
||||||
|
*/
|
||||||
|
function transformExpressionToJs(expression: string): string {
|
||||||
|
return expression.replace(/not\s+/g, '!');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Superset of a native array. The superset provides methods which mimic the
|
||||||
|
* list data structure used in PullApprove for files in conditions.
|
||||||
|
*/
|
||||||
|
class PullApproveArray extends Array<string> {
|
||||||
|
constructor(...elements: string[]) {
|
||||||
|
super(...elements);
|
||||||
|
|
||||||
|
// Set the prototype explicitly because in ES5, the prototype is accidentally
|
||||||
|
// lost due to a limitation in down-leveling.
|
||||||
|
// https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work.
|
||||||
|
Object.setPrototypeOf(this, PullApproveArray.prototype);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a new array which only includes files that match the given pattern. */
|
||||||
|
include(pattern: string): PullApproveArray {
|
||||||
|
return new PullApproveArray(...this.filter(s => getOrCreateGlob(pattern).match(s)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a new array which only includes files that did not match the given pattern. */
|
||||||
|
exclude(pattern: string): PullApproveArray {
|
||||||
|
return new PullApproveArray(...this.filter(s => !getOrCreateGlob(pattern).match(s)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a glob for the given pattern. The cached glob will be returned
|
||||||
|
* if available. Otherwise a new glob will be created and cached.
|
||||||
|
*/
|
||||||
|
function getOrCreateGlob(pattern: string) {
|
||||||
|
if (patternCache.has(pattern)) {
|
||||||
|
return patternCache.get(pattern)!;
|
||||||
|
}
|
||||||
|
const glob = new Minimatch(pattern, {dot: true});
|
||||||
|
patternCache.set(pattern, glob);
|
||||||
|
return glob;
|
||||||
|
}
|
@ -5,162 +5,101 @@
|
|||||||
* Use of this source code is governed by an MIT-style license that can be
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import {IMinimatch, Minimatch} from 'minimatch';
|
import {convertConditionToFunction} from './condition_evaluator';
|
||||||
|
|
||||||
import {PullApproveGroupConfig} from './parse-yaml';
|
import {PullApproveGroupConfig} from './parse-yaml';
|
||||||
|
|
||||||
/** A condition for a group. */
|
/** A condition for a group. */
|
||||||
interface GroupCondition {
|
interface GroupCondition {
|
||||||
glob: string;
|
expression: string;
|
||||||
matcher: IMinimatch;
|
checkFn: (files: string[]) => boolean;
|
||||||
matchedFiles: Set<string>;
|
matchedFiles: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Result of testing files against the group. */
|
/** Result of testing files against the group. */
|
||||||
export interface PullApproveGroupResult {
|
export interface PullApproveGroupResult {
|
||||||
groupName: string;
|
groupName: string;
|
||||||
matchedIncludes: GroupCondition[];
|
matchedConditions: GroupCondition[];
|
||||||
matchedExcludes: GroupCondition[];
|
|
||||||
matchedCount: number;
|
matchedCount: number;
|
||||||
unmatchedIncludes: GroupCondition[];
|
unmatchedConditions: GroupCondition[];
|
||||||
unmatchedExcludes: GroupCondition[];
|
|
||||||
unmatchedCount: number;
|
unmatchedCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regex Matcher for contains_any_globs conditions
|
// Regular expression that matches conditions for the global approval.
|
||||||
const CONTAINS_ANY_GLOBS_REGEX = /^'([^']+)',?$/;
|
const GLOBAL_APPROVAL_CONDITION_REGEX = /^"global-(docs-)?approvers" not in groups.approved$/;
|
||||||
|
|
||||||
const CONDITION_TYPES = {
|
// Name of the PullApprove group that serves as fallback. This group should never capture
|
||||||
INCLUDE_GLOBS: /^contains_any_globs/,
|
// any conditions as it would always match specified files. This is not desired as we want
|
||||||
EXCLUDE_GLOBS: /^not contains_any_globs/,
|
// to figure out as part of this tool, whether there actually are unmatched files.
|
||||||
ATTR_LENGTH: /^len\(.*\)/,
|
const FALLBACK_GROUP_NAME = 'fallback';
|
||||||
GLOBAL_APPROVAL: /^"global-(docs-)?approvers" not in groups.approved$/,
|
|
||||||
};
|
|
||||||
|
|
||||||
/** A PullApprove group to be able to test files against. */
|
/** A PullApprove group to be able to test files against. */
|
||||||
export class PullApproveGroup {
|
export class PullApproveGroup {
|
||||||
// Lines which were not able to be parsed as expected.
|
/** List of conditions for the group. */
|
||||||
private misconfiguredLines: string[] = [];
|
conditions: GroupCondition[] = [];
|
||||||
// Conditions for the group for including files.
|
|
||||||
private includeConditions: GroupCondition[] = [];
|
|
||||||
// Conditions for the group for excluding files.
|
|
||||||
private excludeConditions: GroupCondition[] = [];
|
|
||||||
// Whether the group has file matchers.
|
|
||||||
public hasMatchers = false;
|
|
||||||
|
|
||||||
constructor(public groupName: string, group: PullApproveGroupConfig) {
|
constructor(public groupName: string, config: PullApproveGroupConfig) {
|
||||||
if (group.conditions) {
|
this._captureConditions(config);
|
||||||
for (let condition of group.conditions) {
|
}
|
||||||
condition = condition.trim();
|
|
||||||
|
|
||||||
if (condition.match(CONDITION_TYPES.INCLUDE_GLOBS)) {
|
private _captureConditions(config: PullApproveGroupConfig) {
|
||||||
const [conditions, misconfiguredLines] = getLinesForContainsAnyGlobs(condition);
|
if (config.conditions && this.groupName !== FALLBACK_GROUP_NAME) {
|
||||||
conditions.forEach(globString => this.includeConditions.push({
|
return config.conditions.forEach(condition => {
|
||||||
glob: globString,
|
const expression = condition.trim();
|
||||||
matcher: new Minimatch(globString, {dot: true}),
|
|
||||||
matchedFiles: new Set<string>(),
|
if (expression.match(GLOBAL_APPROVAL_CONDITION_REGEX)) {
|
||||||
}));
|
|
||||||
this.misconfiguredLines.push(...misconfiguredLines);
|
|
||||||
this.hasMatchers = true;
|
|
||||||
} else if (condition.match(CONDITION_TYPES.EXCLUDE_GLOBS)) {
|
|
||||||
const [conditions, misconfiguredLines] = getLinesForContainsAnyGlobs(condition);
|
|
||||||
conditions.forEach(globString => this.excludeConditions.push({
|
|
||||||
glob: globString,
|
|
||||||
matcher: new Minimatch(globString, {dot: true}),
|
|
||||||
matchedFiles: new Set<string>(),
|
|
||||||
}));
|
|
||||||
this.misconfiguredLines.push(...misconfiguredLines);
|
|
||||||
this.hasMatchers = true;
|
|
||||||
} else if (condition.match(CONDITION_TYPES.ATTR_LENGTH)) {
|
|
||||||
// Currently a noop as we do not take any action on this condition type.
|
|
||||||
} else if (condition.match(CONDITION_TYPES.GLOBAL_APPROVAL)) {
|
|
||||||
// Currently a noop as we don't take any action for global approval conditions.
|
// Currently a noop as we don't take any action for global approval conditions.
|
||||||
} else {
|
return;
|
||||||
const errMessage =
|
}
|
||||||
`Unrecognized condition found, unable to parse the following condition: \n\n` +
|
|
||||||
`From the [${groupName}] group:\n` +
|
try {
|
||||||
` - ${condition}` +
|
this.conditions.push({
|
||||||
`\n\n` +
|
expression,
|
||||||
`Known condition regexs:\n` +
|
checkFn: convertConditionToFunction(expression),
|
||||||
`${Object.entries(CONDITION_TYPES).map(([k, v]) => ` ${k} - ${v}`).join('\n')}` +
|
matchedFiles: new Set(),
|
||||||
`\n\n`;
|
});
|
||||||
console.error(errMessage);
|
} catch (e) {
|
||||||
|
console.error(`Could not parse condition in group: ${this.groupName}`);
|
||||||
|
console.error(` - ${expression}`);
|
||||||
|
console.error(`Error:`, e.message, e.stack);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/** Retrieve all of the lines which were not able to be parsed. */
|
|
||||||
getBadLines(): string[] {
|
|
||||||
return this.misconfiguredLines;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Retrieve the results for the Group, all matched and unmatched conditions. */
|
|
||||||
getResults(): PullApproveGroupResult {
|
|
||||||
const matchedIncludes = this.includeConditions.filter(c => !!c.matchedFiles.size);
|
|
||||||
const matchedExcludes = this.excludeConditions.filter(c => !!c.matchedFiles.size);
|
|
||||||
const unmatchedIncludes = this.includeConditions.filter(c => !c.matchedFiles.size);
|
|
||||||
const unmatchedExcludes = this.excludeConditions.filter(c => !c.matchedFiles.size);
|
|
||||||
const unmatchedCount = unmatchedIncludes.length + unmatchedExcludes.length;
|
|
||||||
const matchedCount = matchedIncludes.length + matchedExcludes.length;
|
|
||||||
return {
|
|
||||||
matchedIncludes,
|
|
||||||
matchedExcludes,
|
|
||||||
matchedCount,
|
|
||||||
unmatchedIncludes,
|
|
||||||
unmatchedExcludes,
|
|
||||||
unmatchedCount,
|
|
||||||
groupName: this.groupName,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests a provided file path to determine if it would be considered matched by
|
* Tests a provided file path to determine if it would be considered matched by
|
||||||
* the pull approve group's conditions.
|
* the pull approve group's conditions.
|
||||||
*/
|
*/
|
||||||
testFile(file: string) {
|
testFile(filePath: string): boolean {
|
||||||
let matched = false;
|
return this.conditions.every(({matchedFiles, checkFn, expression}) => {
|
||||||
this.includeConditions.forEach((includeCondition: GroupCondition) => {
|
try {
|
||||||
if (includeCondition.matcher.match(file)) {
|
const matchesFile = checkFn([filePath]);
|
||||||
let matchedExclude = false;
|
if (matchesFile) {
|
||||||
this.excludeConditions.forEach((excludeCondition: GroupCondition) => {
|
matchedFiles.add(filePath);
|
||||||
if (excludeCondition.matcher.match(file)) {
|
}
|
||||||
// Add file as a discovered exclude as it is negating a matched
|
return matchesFile;
|
||||||
// include condition.
|
} catch (e) {
|
||||||
excludeCondition.matchedFiles.add(file);
|
const errMessage = `Condition could not be evaluated: \n\n` +
|
||||||
matchedExclude = true;
|
`From the [${this.groupName}] group:\n` +
|
||||||
|
` - ${expression}` +
|
||||||
|
`\n\n${e.message} ${e.stack}\n\n`;
|
||||||
|
console.error(errMessage);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// An include condition is only considered matched if no exclude
|
|
||||||
// conditions are found to matched the file.
|
|
||||||
if (!matchedExclude) {
|
|
||||||
includeCondition.matchedFiles.add(file);
|
|
||||||
matched = true;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
return matched;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/** Retrieve the results for the Group, all matched and unmatched conditions. */
|
||||||
* Extract all of the individual globs from a group condition,
|
getResults(): PullApproveGroupResult {
|
||||||
* providing both the valid and invalid lines.
|
const matchedConditions = this.conditions.filter(c => !!c.matchedFiles.size);
|
||||||
*/
|
const unmatchedConditions = this.conditions.filter(c => !c.matchedFiles.size);
|
||||||
function getLinesForContainsAnyGlobs(lines: string) {
|
return {
|
||||||
const invalidLines: string[] = [];
|
matchedConditions,
|
||||||
const validLines = lines.split('\n')
|
matchedCount: matchedConditions.length,
|
||||||
.slice(1, -1)
|
unmatchedConditions,
|
||||||
.map((glob: string) => {
|
unmatchedCount: unmatchedConditions.length,
|
||||||
const trimmedGlob = glob.trim();
|
groupName: this.groupName,
|
||||||
const match = trimmedGlob.match(CONTAINS_ANY_GLOBS_REGEX);
|
};
|
||||||
if (!match) {
|
|
||||||
invalidLines.push(trimmedGlob);
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
return match[1];
|
|
||||||
})
|
|
||||||
.filter(globString => !!globString);
|
|
||||||
return [validLines, invalidLines];
|
|
||||||
}
|
}
|
||||||
|
@ -5,26 +5,20 @@
|
|||||||
* Use of this source code is governed by an MIT-style license that can be
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {PullApproveGroupResult} from './group';
|
import {PullApproveGroupResult} from './group';
|
||||||
|
|
||||||
/** Create logs for each pullapprove group result. */
|
/** Create logs for each pullapprove group result. */
|
||||||
export function logGroup(group: PullApproveGroupResult, matched = true) {
|
export function logGroup(group: PullApproveGroupResult, matched = true) {
|
||||||
const includeConditions = matched ? group.matchedIncludes : group.unmatchedIncludes;
|
const conditions = matched ? group.matchedConditions : group.unmatchedConditions;
|
||||||
const excludeConditions = matched ? group.matchedExcludes : group.unmatchedExcludes;
|
|
||||||
console.groupCollapsed(`[${group.groupName}]`);
|
console.groupCollapsed(`[${group.groupName}]`);
|
||||||
if (includeConditions.length) {
|
if (conditions.length) {
|
||||||
console.group('includes');
|
conditions.forEach(matcher => {
|
||||||
includeConditions.forEach(
|
const count = matcher.matchedFiles.size;
|
||||||
matcher => console.info(`${matcher.glob} - ${matcher.matchedFiles.size}`));
|
console.info(`${count} ${count === 1 ? 'match' : 'matches'} - ${matcher.expression}`)
|
||||||
|
});
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
}
|
}
|
||||||
if (excludeConditions.length) {
|
|
||||||
console.group('excludes');
|
|
||||||
excludeConditions.forEach(
|
|
||||||
matcher => console.info(`${matcher.glob} - ${matcher.matchedFiles.size}`));
|
|
||||||
console.groupEnd();
|
|
||||||
}
|
|
||||||
console.groupEnd();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Logs a header within a text drawn box. */
|
/** Logs a header within a text drawn box. */
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
import {parse as parseYaml} from 'yaml';
|
import {parse as parseYaml} from 'yaml';
|
||||||
|
|
||||||
export interface PullApproveGroupConfig {
|
export interface PullApproveGroupConfig {
|
||||||
conditions?: string;
|
conditions?: string[];
|
||||||
reviewers: {
|
reviewers: {
|
||||||
users: string[],
|
users: string[],
|
||||||
teams?: string[],
|
teams?: string[],
|
||||||
|
@ -15,11 +15,9 @@ import {PullApproveGroup} from './group';
|
|||||||
import {logGroup, logHeader} from './logging';
|
import {logGroup, logHeader} from './logging';
|
||||||
import {parsePullApproveYaml} from './parse-yaml';
|
import {parsePullApproveYaml} from './parse-yaml';
|
||||||
|
|
||||||
export function verify() {
|
export function verify(verbose = false) {
|
||||||
// Exit early on shelljs errors
|
// Exit early on shelljs errors
|
||||||
set('-e');
|
set('-e');
|
||||||
// Whether to log verbosely
|
|
||||||
const VERBOSE_MODE = process.argv.includes('-v');
|
|
||||||
// Full path of the angular project directory
|
// Full path of the angular project directory
|
||||||
const PROJECT_DIR = getRepoBaseDir();
|
const PROJECT_DIR = getRepoBaseDir();
|
||||||
// Change to the Angular project directory
|
// Change to the Angular project directory
|
||||||
@ -39,24 +37,11 @@ export function verify() {
|
|||||||
const groups = Object.entries(pullApprove.groups).map(([groupName, group]) => {
|
const groups = Object.entries(pullApprove.groups).map(([groupName, group]) => {
|
||||||
return new PullApproveGroup(groupName, group);
|
return new PullApproveGroup(groupName, group);
|
||||||
});
|
});
|
||||||
// PullApprove groups without matchers.
|
// PullApprove groups without conditions. These are skipped in the verification
|
||||||
const groupsWithoutMatchers = groups.filter(group => !group.hasMatchers);
|
// as those would always be active and cause zero unmatched files.
|
||||||
// PullApprove groups with matchers.
|
const groupsSkipped = groups.filter(group => !group.conditions.length);
|
||||||
const groupsWithMatchers = groups.filter(group => group.hasMatchers);
|
// PullApprove groups with conditions.
|
||||||
// All lines from group conditions which are not parsable.
|
const groupsWithConditions = groups.filter(group => !!group.conditions.length);
|
||||||
const groupsWithBadLines = groups.filter(g => !!g.getBadLines().length);
|
|
||||||
// If any groups contains bad lines, log bad lines and exit failing.
|
|
||||||
if (groupsWithBadLines.length) {
|
|
||||||
logHeader('PullApprove config file parsing failure');
|
|
||||||
console.info(`Discovered errors in ${groupsWithBadLines.length} groups`);
|
|
||||||
groupsWithBadLines.forEach(group => {
|
|
||||||
console.info(` - [${group.groupName}]`);
|
|
||||||
group.getBadLines().forEach(line => console.info(` ${line}`));
|
|
||||||
});
|
|
||||||
console.info(
|
|
||||||
`Correct the invalid conditions, before PullApprove verification can be completed`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
// Files which are matched by at least one group.
|
// Files which are matched by at least one group.
|
||||||
const matchedFiles: string[] = [];
|
const matchedFiles: string[] = [];
|
||||||
// Files which are not matched by at least one group.
|
// Files which are not matched by at least one group.
|
||||||
@ -64,14 +49,14 @@ export function verify() {
|
|||||||
|
|
||||||
// Test each file in the repo against each group for being matched.
|
// Test each file in the repo against each group for being matched.
|
||||||
REPO_FILES.forEach((file: string) => {
|
REPO_FILES.forEach((file: string) => {
|
||||||
if (groupsWithMatchers.filter(group => group.testFile(file)).length) {
|
if (groupsWithConditions.filter(group => group.testFile(file)).length) {
|
||||||
matchedFiles.push(file);
|
matchedFiles.push(file);
|
||||||
} else {
|
} else {
|
||||||
unmatchedFiles.push(file);
|
unmatchedFiles.push(file);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Results for each group
|
// Results for each group
|
||||||
const resultsByGroup = groupsWithMatchers.map(group => group.getResults());
|
const resultsByGroup = groupsWithConditions.map(group => group.getResults());
|
||||||
// Whether all group condition lines match at least one file and all files
|
// Whether all group condition lines match at least one file and all files
|
||||||
// are matched by at least one group.
|
// are matched by at least one group.
|
||||||
const verificationSucceeded =
|
const verificationSucceeded =
|
||||||
@ -94,7 +79,7 @@ export function verify() {
|
|||||||
*/
|
*/
|
||||||
logHeader('PullApprove results by file');
|
logHeader('PullApprove results by file');
|
||||||
console.groupCollapsed(`Matched Files (${matchedFiles.length} files)`);
|
console.groupCollapsed(`Matched Files (${matchedFiles.length} files)`);
|
||||||
VERBOSE_MODE && matchedFiles.forEach(file => console.info(file));
|
verbose && matchedFiles.forEach(file => console.info(file));
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
console.groupCollapsed(`Unmatched Files (${unmatchedFiles.length} files)`);
|
console.groupCollapsed(`Unmatched Files (${unmatchedFiles.length} files)`);
|
||||||
unmatchedFiles.forEach(file => console.info(file));
|
unmatchedFiles.forEach(file => console.info(file));
|
||||||
@ -103,12 +88,12 @@ export function verify() {
|
|||||||
* Group by group Summary
|
* Group by group Summary
|
||||||
*/
|
*/
|
||||||
logHeader('PullApprove results by group');
|
logHeader('PullApprove results by group');
|
||||||
console.groupCollapsed(`Groups without matchers (${groupsWithoutMatchers.length} groups)`);
|
console.groupCollapsed(`Groups skipped (${groupsSkipped.length} groups)`);
|
||||||
VERBOSE_MODE && groupsWithoutMatchers.forEach(group => console.info(`${group.groupName}`));
|
verbose && groupsSkipped.forEach(group => console.info(`${group.groupName}`));
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
const matchedGroups = resultsByGroup.filter(group => !group.unmatchedCount);
|
const matchedGroups = resultsByGroup.filter(group => !group.unmatchedCount);
|
||||||
console.groupCollapsed(`Matched conditions by Group (${matchedGroups.length} groups)`);
|
console.groupCollapsed(`Matched conditions by Group (${matchedGroups.length} groups)`);
|
||||||
VERBOSE_MODE && matchedGroups.forEach(group => logGroup(group));
|
verbose && matchedGroups.forEach(group => logGroup(group));
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
const unmatchedGroups = resultsByGroup.filter(group => group.unmatchedCount);
|
const unmatchedGroups = resultsByGroup.filter(group => group.unmatchedCount);
|
||||||
console.groupCollapsed(`Unmatched conditions by Group (${unmatchedGroups.length} groups)`);
|
console.groupCollapsed(`Unmatched conditions by Group (${unmatchedGroups.length} groups)`);
|
||||||
|
@ -10,8 +10,12 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"chalk": "<from-root>",
|
"chalk": "<from-root>",
|
||||||
|
"clang-format": "<from-root>",
|
||||||
|
"cli-progress": "<from-root>",
|
||||||
"glob": "<from-root>",
|
"glob": "<from-root>",
|
||||||
|
"inquirer": "<from-root>",
|
||||||
"minimatch": "<from-root>",
|
"minimatch": "<from-root>",
|
||||||
|
"multimatch": "<from-root>",
|
||||||
"shelljs": "<from-root>",
|
"shelljs": "<from-root>",
|
||||||
"typescript": "<from-root>",
|
"typescript": "<from-root>",
|
||||||
"yaml": "<from-root>",
|
"yaml": "<from-root>",
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user