Compare commits
584 Commits
7.0.0-beta
...
7.0.4
Author | SHA1 | Date | |
---|---|---|---|
0e95a20576 | |||
9c7428e7ad | |||
d3044271dc | |||
4348c472d6 | |||
2ec05415e2 | |||
188e9ceb1f | |||
de7b554a14 | |||
1d252a559b | |||
bff5c24ed2 | |||
53197de80c | |||
12757474e6 | |||
057423e3e7 | |||
7a1b8763b2 | |||
cbd420656d | |||
f33307badb | |||
b00588a240 | |||
fb0d93d2c1 | |||
cc0fd6036d | |||
682034fcc0 | |||
0475b8d686 | |||
6be6125c5c | |||
1b7b64ae18 | |||
d284fa6bd2 | |||
8e4dca0ddf | |||
006a43f951 | |||
5469b6f64b | |||
926367150a | |||
14ff340e93 | |||
c8e750745a | |||
6b8f2da93e | |||
352219eafb | |||
e8a8770eee | |||
f536261639 | |||
2d06693560 | |||
315d95c747 | |||
5c5b62cc86 | |||
479f773bb3 | |||
629c83588d | |||
d96ba0ffd9 | |||
30700035b3 | |||
8c6d6b85db | |||
0c10b57749 | |||
fe1c0d7a0c | |||
dc053859ee | |||
4d532df52b | |||
5eb540cf34 | |||
7697650edc | |||
4517b8e618 | |||
3abc4c9c54 | |||
1d6333891c | |||
4d4e85b708 | |||
28104828d5 | |||
77f531b6f7 | |||
3041240cbc | |||
257ac83095 | |||
bc93d47129 | |||
3f61071638 | |||
ea571aa9d1 | |||
40cd0371d2 | |||
8356577670 | |||
45d3c8093b | |||
e7eec2e44b | |||
c44b93253c | |||
cd444bc2ba | |||
e9ae26a272 | |||
f9856974e3 | |||
b3c6409763 | |||
4c3e9a32d2 | |||
2700952040 | |||
6d92c99c49 | |||
7cefb3efd4 | |||
1c0ab3468d | |||
b5ede01c09 | |||
98b1a9dcfe | |||
c277c198d2 | |||
b7d7609872 | |||
415e3e06ed | |||
0f7a850699 | |||
1e2525b8a7 | |||
bc9f534bf4 | |||
ea2a3ca705 | |||
802e996e64 | |||
aa7dd1267b | |||
91916e7113 | |||
0e92f1eee5 | |||
4ac3b2e2bd | |||
20ce134961 | |||
c01f3409f3 | |||
71580c8e37 | |||
e58f8b7be0 | |||
046c96a3f1 | |||
00e6569e0f | |||
5dbf9f721b | |||
c63bc02846 | |||
2f3308e4f0 | |||
cc5e4fb93f | |||
3ab9947c7f | |||
b1f92db22b | |||
bda1e52b50 | |||
1084ebc091 | |||
5a3af00398 | |||
7a088af3ff | |||
3e8d8ec2f5 | |||
d4ea7f1f7e | |||
23096501e2 | |||
023874426e | |||
aeffcdd417 | |||
6d7d7c77e3 | |||
8da6c369e5 | |||
d0f2bae62b | |||
062af7a725 | |||
9d1d70a563 | |||
5f908eedbf | |||
a3eac19831 | |||
cace4dea64 | |||
a012ffbd5b | |||
2065149d73 | |||
44e604b131 | |||
4503b28295 | |||
f8fcea333f | |||
a09782b8ac | |||
bfeceb3d3e | |||
1a5735d5c0 | |||
43d4446ec0 | |||
91edfb8be0 | |||
9d0a6554a3 | |||
8fff438767 | |||
b2ae08f272 | |||
c78542f124 | |||
b52c97cf70 | |||
929739c736 | |||
85bde4ff90 | |||
5b5b8254b1 | |||
ac8871fb7f | |||
6b2b1eae9d | |||
b3f096219f | |||
224d9dcd4a | |||
31a0c2a6c2 | |||
c251a5a4d1 | |||
81e571b908 | |||
80a9de0a57 | |||
d1e2ac86fd | |||
9a0d326f4f | |||
ce3024017a | |||
c8adef2dab | |||
778e1c2602 | |||
35bfa1437f | |||
4d83078cd2 | |||
64d005de05 | |||
0c822b75f9 | |||
e0a9cfb5e2 | |||
f8dff83d4b | |||
e411cd124a | |||
df4e85907c | |||
1498dc2dca | |||
abfde44afb | |||
eb9d431487 | |||
9060fb7031 | |||
7fbe547f78 | |||
cda6a215f2 | |||
a8430db45d | |||
5e2bf292e5 | |||
efded24527 | |||
30ac643d1d | |||
d9725016ad | |||
3f94759b14 | |||
9a46420aaf | |||
6a11d7e2a0 | |||
4fd9988251 | |||
a4fcd07792 | |||
078a928fab | |||
5d90aede4e | |||
658a18cdab | |||
6d240cf687 | |||
dca176e232 | |||
6c4845515b | |||
c2ce832f65 | |||
c726d27331 | |||
c4b7862e1b | |||
39472e102b | |||
1fafc5ca18 | |||
07a26647ac | |||
817821e553 | |||
398db3e9f0 | |||
d5fa4dc146 | |||
0ae3518fa9 | |||
e0f9a1b3f3 | |||
2604f429c7 | |||
ecada17ad4 | |||
fa3751ec9e | |||
f85a969219 | |||
d0a1e42e50 | |||
4f14a09895 | |||
8d28fe9df9 | |||
6715f056b0 | |||
32c018cbfe | |||
a403c4f7f5 | |||
bf656d64b8 | |||
3b183ce9b5 | |||
f635c3ecec | |||
dd6e8424aa | |||
1035c6a3ee | |||
b49b274a16 | |||
a4ebf3fb6d | |||
9997ab5ef9 | |||
c109aada2c | |||
57ce13aa1c | |||
2fd127d000 | |||
2f28e6a62d | |||
bf1a13e5e1 | |||
ab9e114fee | |||
448ab9c465 | |||
d769b441c0 | |||
50ccbe744d | |||
b29e709208 | |||
017a087f9b | |||
4b522572e6 | |||
3808434eec | |||
34aff0b3a1 | |||
7cb4183f58 | |||
960d32dd4f | |||
be0382b50c | |||
7ac4b76336 | |||
00b5c7b49b | |||
96f38562bd | |||
94b98aa819 | |||
1c9b06504b | |||
dd8a85158e | |||
164f79a7b0 | |||
327c614799 | |||
1aa8cfbf74 | |||
95d0626a1e | |||
5183bbffbe | |||
e76a570908 | |||
9afc9a7464 | |||
931e603f80 | |||
45732e5b91 | |||
b2db32b715 | |||
9e32dc7c95 | |||
a19b690338 | |||
bab5b68910 | |||
3daeadd235 | |||
ff15043e48 | |||
ea20ae63d0 | |||
7777a99fe5 | |||
9ebb4c02a2 | |||
7fbeb04b7c | |||
42ee50fc22 | |||
163b7c94a9 | |||
071934e92a | |||
735dfd3b1a | |||
70cd112872 | |||
989555352d | |||
99736750fc | |||
0a3f8173f0 | |||
6a64ac4151 | |||
062fe5c2cf | |||
4b494f23f5 | |||
bd186c7ef9 | |||
1657c997cd | |||
1e69d601fb | |||
34b6d5fff9 | |||
632f66a461 | |||
f7b17a4784 | |||
9562324ea4 | |||
880c0add56 | |||
64c96186da | |||
26209fca49 | |||
7f03528dbc | |||
d17602f31d | |||
7d0e17530b | |||
053bf27fb3 | |||
39f42bad1c | |||
3f8ac238f1 | |||
e0e2038718 | |||
67608a907e | |||
7acdad6921 | |||
7466a99dda | |||
912f3d186f | |||
e26cb21e4e | |||
be4edf15ee | |||
d5e9405d4f | |||
d1b7bb52e7 | |||
1312693f88 | |||
72ff9c880c | |||
3060b3e29b | |||
ee28b64d74 | |||
1246ba53c7 | |||
c29ff722a0 | |||
67ad9468d3 | |||
f922808b8d | |||
67435d456c | |||
fbfce79b93 | |||
2a14dfa4ba | |||
8e71ad6027 | |||
25289664ea | |||
e5644204dc | |||
69b9758ab8 | |||
7ea5161d4d | |||
b0879046b7 | |||
456f23f76a | |||
9623e7c639 | |||
83302d193e | |||
807070fe83 | |||
3ac8a63499 | |||
6a24db2bc6 | |||
50d1cba174 | |||
44c05c05af | |||
7d08722e80 | |||
13cdd13511 | |||
9320ec0f43 | |||
5dd225cb43 | |||
1b1c8ee545 | |||
10e414f617 | |||
b46fa92ae5 | |||
0bdea1f69c | |||
5a31bde649 | |||
decc0b840d | |||
55d54c7e97 | |||
ccceff5ecc | |||
6c6bc95ac0 | |||
c9cfcfa728 | |||
f543d71cc3 | |||
3a5cb1cb11 | |||
ab6f055479 | |||
3683c6a188 | |||
4006c9b6e6 | |||
245b85f72a | |||
bfeed842ee | |||
77942cc690 | |||
7aed64d3a1 | |||
9953fe7c00 | |||
3cce4afa0d | |||
8f25321787 | |||
51dfdd5dd1 | |||
fdaf573073 | |||
35bf95281f | |||
7a78889994 | |||
19c4e705ff | |||
36d6e6076e | |||
868047e87f | |||
5f1273ba2e | |||
355a7cae3c | |||
4c615f7de7 | |||
79466baef8 | |||
b0070dfb9a | |||
9ed4e3df60 | |||
a2da485d90 | |||
9cb17ecc39 | |||
35936864bc | |||
532e53678d | |||
f859d83298 | |||
ac68c75e26 | |||
fe45b9cebd | |||
aaaa34021c | |||
730679964f | |||
cb59d87489 | |||
d216a46412 | |||
a2878b0b1d | |||
5977b72e9c | |||
7373da9b11 | |||
783a682a7d | |||
d22418d417 | |||
4b1fd98093 | |||
935ef13096 | |||
f4b60588fb | |||
15dadb92ef | |||
ce06a75ebf | |||
9889276b15 | |||
d0f7eadc09 | |||
4b132c9848 | |||
46729c76a0 | |||
f22deb2e2d | |||
57de9fc41a | |||
31034f5146 | |||
c5899f4cd4 | |||
ab379ab72a | |||
32e479ffec | |||
391c708d7e | |||
c51331689f | |||
68fadd9b97 | |||
2ad1bb4eb9 | |||
794c3595d4 | |||
b807106f54 | |||
86e6a2099a | |||
9993c72335 | |||
f455518d80 | |||
7cf5807100 | |||
9523991a9b | |||
9acd04c192 | |||
c091d40fb0 | |||
b7baf632c0 | |||
4c0d4fc649 | |||
5b3c08b237 | |||
68f2e0c391 | |||
9c1c945489 | |||
ef5338663d | |||
380b3d7653 | |||
4decc8521d | |||
4d544bcb46 | |||
4c819f79b2 | |||
ac3252a73b | |||
a08af77b70 | |||
aac08e0438 | |||
63b795ae4a | |||
5f6900ecc0 | |||
325e8010e9 | |||
632b19d5c2 | |||
add1198b88 | |||
0ed2df2a36 | |||
bc1f2d6411 | |||
d7326d81ba | |||
c683f74225 | |||
b286abeabe | |||
eeebe28c0f | |||
ffc6e199bf | |||
a01acec7fe | |||
021f4344b1 | |||
f113b49909 | |||
d8d276c245 | |||
e42bd012f9 | |||
6d6b0ff1ad | |||
f378454c92 | |||
c8c8436e58 | |||
b31c8b6063 | |||
897261efdc | |||
35d70ff265 | |||
fc0a7959a4 | |||
182c08bee1 | |||
e2bc0ad6c2 | |||
73333ee3e5 | |||
c819859598 | |||
79aefa7659 | |||
e1990a5a80 | |||
4cff5b2964 | |||
459758231b | |||
f29b218060 | |||
39a67548ac | |||
bc88f318f6 | |||
a5b7008c8e | |||
0aafbac99b | |||
ac5aa8f46d | |||
1fb3c4ffee | |||
3c8aa0b301 | |||
15a2b8f622 | |||
d19108531c | |||
354d1944bb | |||
eaccd03ed7 | |||
343df337f4 | |||
9b14483824 | |||
bd42caf1c7 | |||
7db8111973 | |||
74fef157e6 | |||
9661bed3ba | |||
95168e4de0 | |||
13c3e241c8 | |||
8d098d389a | |||
79a2567aa3 | |||
5649acd03f | |||
ebd01e8e79 | |||
e08955b557 | |||
04dfca41f4 | |||
129f69c3bc | |||
c7e2930f25 | |||
6a62ed2245 | |||
482e12c940 | |||
0c344715e5 | |||
cf095d982d | |||
23ec88ef23 | |||
2bd767c4a6 | |||
1e02cd9961 | |||
bc02e19831 | |||
47eb2122c0 | |||
b8422b41bb | |||
8ac4dd6447 | |||
206ae7a233 | |||
79b6256789 | |||
72dce34f42 | |||
7d39bc68fb | |||
32ad2438ca | |||
fc4b993d98 | |||
ff028f0b39 | |||
fef9cebed0 | |||
c08549ae38 | |||
3808416479 | |||
cf8ad24dcf | |||
cee7448efc | |||
7f1cace2a2 | |||
56c86c7e79 | |||
82a14dc107 | |||
a880686081 | |||
026b60cd70 | |||
cea2e0477c | |||
96f9f03d25 | |||
9931bd7576 | |||
48094835bf | |||
e42c1b0da8 | |||
e7ade38731 | |||
d5f47d6b71 | |||
64aa6701f6 | |||
12ccf57340 | |||
c634176035 | |||
4bb10d224c | |||
5d689469f6 | |||
855ad8804e | |||
29d3f3f6dd | |||
33101359c6 | |||
44eef5c343 | |||
68b7847b4c | |||
2b2e841e5b | |||
549de1e21a | |||
48e73c1558 | |||
41ac58ab7d | |||
f37cf52b4c | |||
927323f24e | |||
b94436d86c | |||
bc5cb8153e | |||
34b848ad51 | |||
d7e5bbf2d0 | |||
a9a81f91cf | |||
07c10e2844 | |||
df5999a739 | |||
3fb0da2de5 | |||
8f0fcc3f71 | |||
ca1e56dc8b | |||
d0e710d472 | |||
bc7f962039 | |||
78d42a9503 | |||
dd5e35ee67 | |||
f91b0455c0 | |||
e8bab1349f | |||
e952c65759 | |||
5241ea086d | |||
cbbad1b791 | |||
96ee898cee | |||
2c40a86b61 | |||
a53a559f5a | |||
6de393b2b8 | |||
56283ed594 | |||
ddd3bf83c7 | |||
9b1bb370a3 | |||
976389836e | |||
f76a9ad156 | |||
6f1100a7e9 | |||
b99d7ed5bf | |||
f47f2628e1 | |||
5653874683 | |||
21e566d9bc | |||
bdbb2f9bfa | |||
2e32d4ee17 | |||
8f81dba367 | |||
aedebaf025 | |||
47f4412650 | |||
a09c3923db | |||
10a656fc38 | |||
8dc2b119fb | |||
f2ba55f2fb | |||
00d3666d95 | |||
21009b06a1 | |||
379e8c5e19 | |||
f3b552f51f | |||
c5b594e351 | |||
86a3be8610 | |||
d5bd86ae5d | |||
a9099e8f70 | |||
96d6b79ada | |||
13ccdfd89d | |||
a0c4b2d8f0 | |||
7ba0cb7c93 | |||
d83f9d432a | |||
e3633888ed | |||
ed266daf2c | |||
89af5291de | |||
91d79939be | |||
96eb79b1c7 | |||
83a1334876 | |||
2a21ca09d2 | |||
ddc13352e9 | |||
62be8c2e2f | |||
d2dfd48be0 | |||
d6cd041cbd | |||
694b8ae779 | |||
152de20774 | |||
34ec9244a6 |
3
.bazelignore
Normal file
3
.bazelignore
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
dist
|
||||
aio/node_modules
|
@ -24,6 +24,10 @@ build --symlink_prefix=/
|
||||
# Performance: avoid stat'ing input files
|
||||
build --watchfs
|
||||
|
||||
# Turn off legacy external runfiles
|
||||
run --nolegacy_external_runfiles
|
||||
test --nolegacy_external_runfiles
|
||||
|
||||
###############################
|
||||
# Release support #
|
||||
###############################
|
@ -13,7 +13,7 @@ a GitHub token that enables publishing snapshots.
|
||||
|
||||
To create the github_token file, we take this approach:
|
||||
- Find the angular-builds:token in http://valentine
|
||||
- Go inside the ngcontainer docker image so you use the same version of openssl as we will at runtime: `docker run --rm -it angular/ngcontainer`
|
||||
- Go inside the CircleCI default docker image so you use the same version of openssl as we will at runtime: `docker run --rm -it circleci/node:10.12`
|
||||
- echo "https://[token]:@github.com" > credentials
|
||||
- openssl aes-256-cbc -e -in credentials -out .circleci/github_token -k $KEY
|
||||
- If needed, base64-encode the result so you can copy-paste it out of docker: `base64 github_token`
|
@ -28,3 +28,6 @@ build --local_resources=14336,8.0,1.0
|
||||
|
||||
# Retry in the event of flakes, eg. https://circleci.com/gh/angular/angular/31309
|
||||
test --flaky_test_attempts=2
|
||||
|
||||
# More details on failures
|
||||
build --verbose_failures=true
|
||||
|
@ -7,85 +7,88 @@
|
||||
# To validate changes, use an online parser, eg.
|
||||
# http://yaml-online-parser.appspot.com/
|
||||
|
||||
# Variables
|
||||
|
||||
## IMPORTANT
|
||||
# If you change the `docker_image` version, also change the `cache_key` suffix and the version of
|
||||
# `com_github_bazelbuild_buildtools` in the `/WORKSPACE` file.
|
||||
var_1: &docker_image angular/ngcontainer:0.4.0
|
||||
var_2: &cache_key v2-angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-bust1-0.4.0
|
||||
# Note that the browser docker image comes with Chrome and Firefox preinstalled. This is just
|
||||
# needed for jobs that run tests without Bazel. Bazel runs tests with browsers that will be
|
||||
# fetched by the Webtesting rules. Therefore for jobs that run tests with Bazel, we don't need a
|
||||
# docker image with browsers pre-installed.
|
||||
# **NOTE**: If you change the version of the docker images, also change the `cache_key` suffix.
|
||||
var_1: &default_docker_image circleci/node:10.12
|
||||
var_2: &browsers_docker_image circleci/node:10.12-browsers
|
||||
var_3: &cache_key v2-angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-node-10.12
|
||||
|
||||
# Define common ENV vars
|
||||
var_3: &define_env_vars
|
||||
run: echo "export PROJECT_ROOT=$(pwd)" >> $BASH_ENV
|
||||
|
||||
# See remote cache documentation in /docs/BAZEL.md
|
||||
var_4: &setup-bazel-remote-cache
|
||||
var_4: &define_env_vars
|
||||
run:
|
||||
name: Start up bazel remote cache proxy
|
||||
command: ~/bazel-remote-proxy -backend circleci://
|
||||
background: true
|
||||
name: Define environment variables
|
||||
command: ./.circleci/env.sh
|
||||
|
||||
var_5: &setup_bazel_remote_execution
|
||||
run:
|
||||
name: "Setup bazel RBE remote execution"
|
||||
command: openssl aes-256-cbc -d -in .circleci/gcp_token -k "$CI_REPO_NAME" -out /home/circleci/.gcp_credentials && echo "export GOOGLE_APPLICATION_CREDENTIALS=/home/circleci/.gcp_credentials" >> $BASH_ENV && sudo bash -c "cat .circleci/rbe-bazel.rc >> /etc/bazel.bazelrc"
|
||||
|
||||
# Settings common to each job
|
||||
anchor_1: &job_defaults
|
||||
var_6: &job_defaults
|
||||
working_directory: ~/ng
|
||||
docker:
|
||||
- image: *docker_image
|
||||
- image: *default_docker_image
|
||||
|
||||
# After checkout, rebase on top of master.
|
||||
# Similar to travis behavior, but not quite the same.
|
||||
# See https://discuss.circleci.com/t/1662
|
||||
anchor_2: &post_checkout
|
||||
post: git pull --ff-only origin "refs/pull/${CIRCLE_PULL_REQUEST//*pull\//}/merge"
|
||||
var_7: &post_checkout
|
||||
post: git pull --ff-only origin "refs/pull/${CI_PULL_REQUEST//*pull\//}/merge"
|
||||
|
||||
var_8: &yarn_install
|
||||
run:
|
||||
name: Running Yarn install
|
||||
command: yarn install --frozen-lockfile --non-interactive
|
||||
|
||||
var_9: &setup_circleci_bazel_config
|
||||
run:
|
||||
name: Setting up CircleCI bazel configuration
|
||||
command: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
|
||||
|
||||
version: 2
|
||||
jobs:
|
||||
lint:
|
||||
<<: *job_defaults
|
||||
resource_class: xlarge
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
|
||||
# Check BUILD.bazel formatting before we have a node_modules directory
|
||||
# Then we don't need any exclude pattern to avoid checking those files
|
||||
- run: 'buildifier -mode=check $(find . -type f \( -name "*.bzl" -or -name BUILD.bazel -or -name BUILD \)) ||
|
||||
(echo "BUILD files not formatted. Please run ''yarn buildifier''" ; exit 1)'
|
||||
# Run the skylark linter to check our Bazel rules
|
||||
# deprecated-api is disabled because we use actions.new_file(genfiles_dir)
|
||||
# which has no replacement, see https://github.com/bazelbuild/bazel/issues/4858
|
||||
- run: 'find . -type f -name "*.bzl" |
|
||||
xargs java -jar /usr/local/bin/Skylint_deploy.jar --disable-checks=deprecated-api ||
|
||||
(echo -e "\n.bzl files have lint errors. Please run ''yarn skylint''"; exit 1)'
|
||||
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- *define_env_vars
|
||||
- *setup_circleci_bazel_config
|
||||
- *yarn_install
|
||||
|
||||
- run: 'yarn buildifier -mode=check ||
|
||||
(echo "BUILD files not formatted. Please run ''yarn buildifier''" ; exit 1)'
|
||||
# Run the skylark linter to check our Bazel rules
|
||||
- run: 'yarn skylint ||
|
||||
(echo -e "\n.bzl files have lint errors. Please run ''yarn skylint''"; exit 1)'
|
||||
|
||||
- run: yarn install --frozen-lockfile --non-interactive
|
||||
- run: ./node_modules/.bin/gulp lint
|
||||
|
||||
test:
|
||||
<<: *job_defaults
|
||||
resource_class: xlarge
|
||||
steps:
|
||||
- *define_env_vars
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
# See remote cache documentation in /docs/BAZEL.md
|
||||
- run: .circleci/setup_cache.sh
|
||||
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
|
||||
- *setup-bazel-remote-cache
|
||||
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- *define_env_vars
|
||||
- *setup_circleci_bazel_config
|
||||
- *yarn_install
|
||||
|
||||
- run: ls /home/circleci/bazel_repository_cache || true
|
||||
- run: bazel info release
|
||||
- run: bazel run @nodejs//:yarn
|
||||
# Use bazel query so that we explicitly ask for all buildable targets to be built as well
|
||||
# This avoids waiting for the slowest build target to finish before running the first test
|
||||
# See https://github.com/bazelbuild/bazel/issues/4257
|
||||
# NOTE: Angular developers should typically just bazel build //packages/... or bazel test //packages/...
|
||||
- run: bazel query --output=label //... | xargs bazel test --build_tag_filters=-ivy-only --test_tag_filters=-manual,-ivy-only
|
||||
# Setup remote execution and run RBE-compatible tests.
|
||||
- *setup_bazel_remote_execution
|
||||
- run: yarn bazel test //... --build_tag_filters=-ivy-only --test_tag_filters=-ivy-only,-local
|
||||
# Now run RBE incompatible tests locally.
|
||||
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
|
||||
- run: yarn bazel test //... --build_tag_filters=-ivy-only,local --test_tag_filters=-ivy-only,local
|
||||
|
||||
# CircleCI will allow us to go back and view/download these artifacts from past builds.
|
||||
# Also we can use a service like https://buildsize.org/ to automatically track binary size of these artifacts.
|
||||
@ -111,43 +114,166 @@ jobs:
|
||||
paths:
|
||||
- "node_modules"
|
||||
- "~/bazel_repository_cache"
|
||||
|
||||
# Temporary job to test what will happen when we flip the Ivy flag to true
|
||||
test_ivy_jit:
|
||||
<<: *job_defaults
|
||||
resource_class: xlarge
|
||||
steps:
|
||||
- *define_env_vars
|
||||
# don't run this job on the patch branch (to preserve resources)
|
||||
- run: circleci step halt
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
# See remote cache documentation in /docs/BAZEL.md
|
||||
- run: .circleci/setup_cache.sh
|
||||
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
|
||||
- *setup-bazel-remote-cache
|
||||
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- *define_env_vars
|
||||
- *setup_circleci_bazel_config
|
||||
- *yarn_install
|
||||
- *setup_bazel_remote_execution
|
||||
|
||||
- run: bazel run @yarn//:yarn
|
||||
- run: bazel query --output=label //... | xargs bazel test --define=compile=jit --build_tag_filters=ivy-jit --test_tag_filters=-manual,ivy-jit
|
||||
- run: yarn test-ivy-jit //...
|
||||
|
||||
test_ivy_aot:
|
||||
<<: *job_defaults
|
||||
resource_class: xlarge
|
||||
steps:
|
||||
- *define_env_vars
|
||||
# don't run this job on the patch branch (to preserve resources)
|
||||
- run: circleci step halt
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
# See remote cache documentation in /docs/BAZEL.md
|
||||
- run: .circleci/setup_cache.sh
|
||||
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
|
||||
- *setup-bazel-remote-cache
|
||||
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- *define_env_vars
|
||||
- *setup_circleci_bazel_config
|
||||
- *yarn_install
|
||||
- *setup_bazel_remote_execution
|
||||
|
||||
- run: bazel run @yarn//:yarn
|
||||
- run: bazel query --output=label //... | xargs bazel test --define=compile=local --build_tag_filters=ivy-local --test_tag_filters=-manual,ivy-local
|
||||
- run: yarn test-ivy-aot //...
|
||||
|
||||
test_aio:
|
||||
<<: *job_defaults
|
||||
docker:
|
||||
# Needed because the AIO tests and the PWA score test depend on Chrome being available.
|
||||
- image: *browsers_docker_image
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- *define_env_vars
|
||||
# Build aio
|
||||
- run: yarn --cwd aio build --progress=false
|
||||
# Lint the code
|
||||
- run: yarn --cwd aio lint
|
||||
# Run PWA-score tests
|
||||
# (Run before unit and e2e tests, which destroy the `dist/` directory.)
|
||||
- run: yarn --cwd aio test-pwa-score-localhost $CI_AIO_MIN_PWA_SCORE
|
||||
# Check the bundle sizes.
|
||||
# (Run before unit and e2e tests, which destroy the `dist/` directory.)
|
||||
- run: yarn --cwd aio payload-size
|
||||
# Run unit tests
|
||||
- run: yarn --cwd aio test --watch=false
|
||||
# Run e2e tests
|
||||
- run: yarn --cwd aio e2e
|
||||
# Run unit tests for Firebase redirects
|
||||
- run: yarn --cwd aio redirects-test
|
||||
|
||||
deploy_aio:
|
||||
<<: *job_defaults
|
||||
docker:
|
||||
# Needed because before deploying the deploy-production script runs the PWA score tests.
|
||||
- image: *browsers_docker_image
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- *define_env_vars
|
||||
# Deploy angular.io to production (if necessary)
|
||||
- run: setPublicVar CI_STABLE_BRANCH "$(npm info @angular/core dist-tags.latest | sed -r 's/^\s*([0-9]+\.[0-9]+)\.[0-9]+.*$/\1.x/')"
|
||||
- run: yarn --cwd aio deploy-production
|
||||
|
||||
test_aio_local:
|
||||
<<: *job_defaults
|
||||
docker:
|
||||
# Needed because the AIO tests and the PWA score test depend on Chrome being available.
|
||||
- image: *browsers_docker_image
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- attach_workspace:
|
||||
at: dist
|
||||
- *define_env_vars
|
||||
# Build aio (with local Angular packages)
|
||||
- run: yarn --cwd aio build-local --progress=false
|
||||
# Run PWA-score tests
|
||||
# (Run before unit and e2e tests, which destroy the `dist/` directory.)
|
||||
- run: yarn --cwd aio test-pwa-score-localhost $CI_AIO_MIN_PWA_SCORE
|
||||
# Run unit tests
|
||||
- run: yarn --cwd aio test --watch=false
|
||||
# Run e2e tests
|
||||
- run: yarn --cwd aio e2e
|
||||
|
||||
test_aio_tools:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- attach_workspace:
|
||||
at: dist
|
||||
- *define_env_vars
|
||||
# Install
|
||||
- run: yarn --cwd aio install --frozen-lockfile --non-interactive
|
||||
- run: yarn --cwd aio extract-cli-command-docs
|
||||
# Run tools tests
|
||||
- run: yarn --cwd aio tools-test
|
||||
- run: ./aio/aio-builds-setup/scripts/test.sh
|
||||
|
||||
test_docs_examples_0:
|
||||
<<: *job_defaults
|
||||
docker:
|
||||
# Needed because the example e2e tests depend on Chrome.
|
||||
- image: *browsers_docker_image
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- attach_workspace:
|
||||
at: dist
|
||||
- *define_env_vars
|
||||
# Install root
|
||||
- *yarn_install
|
||||
# Install aio
|
||||
- run: yarn --cwd aio install --frozen-lockfile --non-interactive
|
||||
# Run examples tests
|
||||
- run: yarn --cwd aio example-e2e --setup --local --shard=0/2
|
||||
|
||||
test_docs_examples_1:
|
||||
<<: *job_defaults
|
||||
docker:
|
||||
# Needed because the example e2e tests depend on Chrome.
|
||||
- image: *browsers_docker_image
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- attach_workspace:
|
||||
at: dist
|
||||
- *define_env_vars
|
||||
# Install root
|
||||
- *yarn_install
|
||||
# Install aio
|
||||
- run: yarn --cwd aio install --frozen-lockfile --non-interactive
|
||||
# Run examples tests
|
||||
- run: yarn --cwd aio example-e2e --setup --local --shard=1/2
|
||||
|
||||
# This job should only be run on PR builds, where `CI_PULL_REQUEST` is not `false`.
|
||||
aio_preview:
|
||||
<<: *job_defaults
|
||||
environment:
|
||||
@ -157,14 +283,32 @@ jobs:
|
||||
<<: *post_checkout
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- run: yarn install --frozen-lockfile --non-interactive
|
||||
- run: ./aio/scripts/build-artifacts.sh $AIO_SNAPSHOT_ARTIFACT_PATH
|
||||
- *define_env_vars
|
||||
- *yarn_install
|
||||
- run: ./aio/scripts/build-artifacts.sh $AIO_SNAPSHOT_ARTIFACT_PATH $CI_PULL_REQUEST $CI_COMMIT
|
||||
- store_artifacts:
|
||||
path: *aio_preview_artifact_path
|
||||
# The `destination` needs to be kept in synch with the value of
|
||||
# `AIO_ARTIFACT_PATH` in `aio/aio-builds-setup/Dockerfile`
|
||||
destination: aio/dist/aio-snapshot.tgz
|
||||
|
||||
# This job should only be run on PR builds, where `CI_PULL_REQUEST` is not `false`.
|
||||
test_aio_preview:
|
||||
<<: *job_defaults
|
||||
docker:
|
||||
# Needed because the test-preview script runs e2e tests and the PWA score test with Chrome.
|
||||
- image: *browsers_docker_image
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- *define_env_vars
|
||||
- run: yarn install --cwd aio --frozen-lockfile --non-interactive
|
||||
- run:
|
||||
name: Wait for preview and run tests
|
||||
command: node aio/scripts/test-preview.js $CI_PULL_REQUEST $CI_COMMIT $CI_AIO_MIN_PWA_SCORE
|
||||
|
||||
# This job exists only for backwards-compatibility with old scripts and tests
|
||||
# that rely on the pre-Bazel dist/packages-dist layout.
|
||||
# It duplicates some work with the job above: we build the bazel packages
|
||||
@ -176,24 +320,16 @@ jobs:
|
||||
<<: *job_defaults
|
||||
resource_class: xlarge
|
||||
steps:
|
||||
- *define_env_vars
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
# See remote cache documentation in /docs/BAZEL.md
|
||||
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
|
||||
- run: bazel run @nodejs//:yarn
|
||||
- run:
|
||||
# RBE is enabled by appending rbe-bazel.rc.
|
||||
name: Enable RBE
|
||||
command: 'sudo bash -c "cat .circleci/rbe-bazel.rc >> /etc/bazel.bazelrc"'
|
||||
- run:
|
||||
name: "Setup GCP environment"
|
||||
command: 'openssl aes-256-cbc -d -in .circleci/gcp_token -k "${CIRCLE_PROJECT_REPONAME}" -out /home/circleci/.gcp_credentials'
|
||||
- run:
|
||||
name: build-packages-dist
|
||||
command: scripts/build-packages-dist.sh
|
||||
environment:
|
||||
GOOGLE_APPLICATION_CREDENTIALS: /home/circleci/.gcp_credentials
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- *define_env_vars
|
||||
- *setup_circleci_bazel_config
|
||||
- *yarn_install
|
||||
- *setup_bazel_remote_execution
|
||||
|
||||
- run: scripts/build-packages-dist.sh
|
||||
|
||||
# Save the npm packages from //packages/... for other workflow jobs to read
|
||||
# https://circleci.com/docs/2.0/workflows/#using-workspaces-to-share-data-among-jobs
|
||||
@ -202,7 +338,7 @@ jobs:
|
||||
paths:
|
||||
- packages-dist
|
||||
- packages-dist-ivy-jit
|
||||
- packages-dist-ivy-local
|
||||
- packages-dist-ivy-aot
|
||||
|
||||
# We run the integration tests outside of Bazel for now.
|
||||
# They are a separate workflow job so that they can be easily re-run.
|
||||
@ -212,35 +348,41 @@ jobs:
|
||||
# See comments inside the integration/run_tests.sh script.
|
||||
integration_test:
|
||||
<<: *job_defaults
|
||||
docker:
|
||||
# Needed because the integration tests expect Chrome to be installed (e.g cli-hello-world)
|
||||
- image: *browsers_docker_image
|
||||
# Note: we run Bazel in one of the integration tests, and it can consume >2G
|
||||
# of memory. Together with the system under test, this can exhaust the RAM
|
||||
# on a 4G worker so we use a larger machine here too.
|
||||
resource_class: xlarge
|
||||
steps:
|
||||
- *define_env_vars
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- attach_workspace:
|
||||
at: dist
|
||||
- run: xvfb-run --auto-servernum ./integration/run_tests.sh
|
||||
- *define_env_vars
|
||||
- run: ./integration/run_tests.sh
|
||||
|
||||
# This job updates the content of repos like github.com/angular/core-builds
|
||||
# for every green build on angular/angular.
|
||||
publish_snapshot:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- *define_env_vars
|
||||
# See below - ideally this job should not trigger for non-upstream builds.
|
||||
# But since it does, we have to check this condition.
|
||||
- run:
|
||||
name: Skip this job for Pull Requests and Fork builds
|
||||
# Note, `|| true` on the end makes this step always exit 0
|
||||
command: '[[
|
||||
-v CIRCLE_PR_NUMBER
|
||||
|| "$CIRCLE_PROJECT_USERNAME" != "angular"
|
||||
|| "$CIRCLE_PROJECT_REPONAME" != "angular"
|
||||
"$CI_PULL_REQUEST" != "false"
|
||||
|| "$CI_REPO_OWNER" != "angular"
|
||||
|| "$CI_REPO_NAME" != "angular"
|
||||
]] && circleci step halt || true'
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- attach_workspace:
|
||||
at: dist
|
||||
# CircleCI has a config setting to force SSH for all github connections
|
||||
@ -254,12 +396,23 @@ jobs:
|
||||
|
||||
aio_monitoring:
|
||||
<<: *job_defaults
|
||||
docker:
|
||||
# This job needs Chrome to be globally installed because the tests run with Protractor
|
||||
# which does not load the browser through the Bazel webtesting rules.
|
||||
- image: *browsers_docker_image
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- run: xvfb-run --auto-servernum ./aio/scripts/test-production.sh
|
||||
- *define_env_vars
|
||||
- run:
|
||||
name: Run tests against the deployed apps
|
||||
command: ./aio/scripts/test-production.sh $CI_AIO_MIN_PWA_SCORE
|
||||
- run:
|
||||
name: Notify caretaker about failure
|
||||
command: 'curl --request POST --header "Content-Type: application/json" --data "{\"text\":\":x: \`$CIRCLE_JOB\` job failed on build $CIRCLE_BUILD_NUM: $CIRCLE_BUILD_URL :scream:\"}" $CI_SECRET_SLACK_CARETAKER_WEBHOOK_URL'
|
||||
when: on_fail
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
@ -270,6 +423,29 @@ workflows:
|
||||
- test_ivy_jit
|
||||
- test_ivy_aot
|
||||
- build-packages-dist
|
||||
- test_aio
|
||||
- deploy_aio:
|
||||
requires:
|
||||
- test_aio
|
||||
- test_aio_local:
|
||||
requires:
|
||||
- build-packages-dist
|
||||
- test_aio_tools:
|
||||
requires:
|
||||
- build-packages-dist
|
||||
- test_docs_examples_0:
|
||||
requires:
|
||||
- build-packages-dist
|
||||
- test_docs_examples_1:
|
||||
requires:
|
||||
- build-packages-dist
|
||||
- aio_preview:
|
||||
# Only run on PR builds. (There can be no previews for non-PR builds.)
|
||||
filters:
|
||||
branches:
|
||||
only: /pull\/\d+/
|
||||
- test_aio_preview:
|
||||
requires:
|
||||
- aio_preview
|
||||
- integration_test:
|
||||
requires:
|
||||
@ -285,6 +461,10 @@ workflows:
|
||||
- test_ivy_jit
|
||||
- test_ivy_aot
|
||||
- integration_test
|
||||
# Only publish if `aio`/`docs` tests using the locally built Angular packages pass
|
||||
- test_aio_local
|
||||
- test_docs_examples_0
|
||||
- test_docs_examples_1
|
||||
# Get the artifacts to publish from the build-packages-dist job
|
||||
# since the publishing script expects the legacy outputs layout.
|
||||
- build-packages-dist
|
||||
@ -299,6 +479,7 @@ workflows:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
|
||||
notify:
|
||||
webhooks:
|
||||
- url: https://ngbuilds.io/circle-build
|
||||
|
38
.circleci/env-helpers.inc.sh
Normal file
38
.circleci/env-helpers.inc.sh
Normal file
@ -0,0 +1,38 @@
|
||||
####################################################################################################
|
||||
# Helpers for defining environment variables for CircleCI.
|
||||
#
|
||||
# In CircleCI, each step runs in a new shell. The way to share ENV variables across steps is to
|
||||
# export them from `$BASH_ENV`, which is automatically sourced at the beginning of every step (for
|
||||
# the default `bash` shell).
|
||||
#
|
||||
# See also https://circleci.com/docs/2.0/env-vars/#using-bash_env-to-set-environment-variables.
|
||||
####################################################################################################
|
||||
|
||||
# Set and print an environment variable.
|
||||
#
|
||||
# Use this function for setting environment variables that are public, i.e. it is OK for them to be
|
||||
# visible to anyone through the CI logs.
|
||||
#
|
||||
# Usage: `setPublicVar <name> <value>`
|
||||
function setPublicVar() {
|
||||
setSecretVar $1 $2;
|
||||
echo "$1=$2";
|
||||
}
|
||||
|
||||
# Set (without printing) an environment variable.
|
||||
#
|
||||
# Use this function for setting environment variables that are secret, i.e. should not be visible to
|
||||
# everyone through the CI logs.
|
||||
#
|
||||
# Usage: `setSecretVar <name> <value>`
|
||||
function setSecretVar() {
|
||||
# WARNING: Secrets (e.g. passwords, access tokens) should NOT be printed.
|
||||
# (Keep original shell options to restore at the end.)
|
||||
local -r originalShellOptions=$(set +o);
|
||||
set +x -eu -o pipefail;
|
||||
|
||||
echo "export $1=\"${2:-}\";" >> $BASH_ENV;
|
||||
|
||||
# Restore original shell options.
|
||||
eval "$originalShellOptions";
|
||||
}
|
35
.circleci/env.sh
Executable file
35
.circleci/env.sh
Executable file
@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Load helpers and make them available everywhere (through `$BASH_ENV`).
|
||||
readonly envHelpersPath="`dirname $0`/env-helpers.inc.sh";
|
||||
source $envHelpersPath;
|
||||
echo "source $envHelpersPath;" >> $BASH_ENV;
|
||||
|
||||
|
||||
####################################################################################################
|
||||
# Define PUBLIC environment variables for CircleCI.
|
||||
####################################################################################################
|
||||
setPublicVar PROJECT_ROOT "$(pwd)";
|
||||
setPublicVar CI_AIO_MIN_PWA_SCORE "95";
|
||||
# This is the branch being built; e.g. `pull/12345` for PR builds.
|
||||
setPublicVar CI_BRANCH "$CIRCLE_BRANCH";
|
||||
setPublicVar CI_COMMIT "$CIRCLE_SHA1";
|
||||
# `CI_COMMIT_RANGE` will only be available when `CIRCLE_COMPARE_URL` is also available,
|
||||
# i.e. on push builds (a.k.a. non-PR builds). That is fine, since we only need it in push builds.
|
||||
setPublicVar CI_COMMIT_RANGE "$(sed -r 's|^.*/([0-9a-f]+\.\.\.[0-9a-f]+)$|\1|i' <<< ${CIRCLE_COMPARE_URL:-})";
|
||||
setPublicVar CI_PULL_REQUEST "${CIRCLE_PR_NUMBER:-false}";
|
||||
setPublicVar CI_REPO_NAME "$CIRCLE_PROJECT_REPONAME";
|
||||
setPublicVar CI_REPO_OWNER "$CIRCLE_PROJECT_USERNAME";
|
||||
|
||||
|
||||
####################################################################################################
|
||||
# Define SECRET environment variables for CircleCI.
|
||||
####################################################################################################
|
||||
setSecretVar CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN "$AIO_DEPLOY_TOKEN";
|
||||
setSecretVar CI_SECRET_PAYLOAD_FIREBASE_TOKEN "$ANGULAR_PAYLOAD_TOKEN";
|
||||
# Defined in https://angular-team.slack.com/apps/A0F7VRE7N-circleci.
|
||||
setSecretVar CI_SECRET_SLACK_CARETAKER_WEBHOOK_URL "$SLACK_CARETAKER_WEBHOOK_URL";
|
||||
|
||||
|
||||
# Source `$BASH_ENV` to make the variables available immediately.
|
||||
source $BASH_ENV;
|
Binary file not shown.
@ -34,9 +34,9 @@ build --action_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1
|
||||
# "exec_compatible_with").
|
||||
# More about platforms: https://docs.bazel.build/versions/master/platforms.html
|
||||
build --extra_toolchains=@bazel_toolchains//configs/ubuntu16_04_clang/1.0/bazel_0.15.0/cpp:cc-toolchain-clang-x86_64-default
|
||||
build --extra_execution_platforms=@bazel_toolchains//configs/ubuntu16_04_clang/1.0:rbe_ubuntu1604
|
||||
build --host_platform=@bazel_toolchains//configs/ubuntu16_04_clang/1.0:rbe_ubuntu1604
|
||||
build --platforms=@bazel_toolchains//configs/ubuntu16_04_clang/1.0:rbe_ubuntu1604
|
||||
build --extra_execution_platforms=//tools:rbe_ubuntu1604-angular
|
||||
build --host_platform=//tools:rbe_ubuntu1604-angular
|
||||
build --platforms=//tools:rbe_ubuntu1604-angular
|
||||
|
||||
# Set various strategies so that all actions execute remotely. Mixing remote
|
||||
# and local execution will lead to errors unless the toolchain and remote
|
||||
|
61
.github/ISSUE_TEMPLATE.md
vendored
61
.github/ISSUE_TEMPLATE.md
vendored
@ -1,59 +1,10 @@
|
||||
<!--
|
||||
PLEASE HELP US PROCESS GITHUB ISSUES FASTER BY PROVIDING THE FOLLOWING INFORMATION.
|
||||
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
||||
|
||||
ISSUES MISSING IMPORTANT INFORMATION MAY BE CLOSED WITHOUT INVESTIGATION.
|
||||
-->
|
||||
Please help us process issues more efficiently by filing an
|
||||
issue using one of the following templates:
|
||||
|
||||
## I'm submitting a...
|
||||
<!-- Check one of the following options with "x" -->
|
||||
<pre><code>
|
||||
[ ] Regression (a behavior that used to work and stopped working in a new release)
|
||||
[ ] Bug report <!-- Please search GitHub for a similar issue or PR before submitting -->
|
||||
[ ] Performance issue
|
||||
[ ] Feature request
|
||||
[ ] Documentation issue or request
|
||||
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question
|
||||
[ ] Other... Please describe:
|
||||
</code></pre>
|
||||
https://github.com/angular/angular/issues/new/choose
|
||||
|
||||
## Current behavior
|
||||
<!-- Describe how the issue manifests. -->
|
||||
Thank you!
|
||||
|
||||
|
||||
## Expected behavior
|
||||
<!-- Describe what the desired behavior would be. -->
|
||||
|
||||
|
||||
## Minimal reproduction of the problem with instructions
|
||||
<!--
|
||||
For bug reports please provide the *STEPS TO REPRODUCE* and if possible a *MINIMAL DEMO* of the problem via
|
||||
https://stackblitz.com or similar (you can use this template as a starting point: https://stackblitz.com/fork/angular-gitter).
|
||||
-->
|
||||
|
||||
## What is the motivation / use case for changing the behavior?
|
||||
<!-- Describe the motivation or the concrete use case. -->
|
||||
|
||||
|
||||
## Environment
|
||||
|
||||
<pre><code>
|
||||
Angular version: X.Y.Z
|
||||
<!-- Check whether this is still an issue in the most recent Angular version -->
|
||||
|
||||
Browser:
|
||||
- [ ] Chrome (desktop) version XX
|
||||
- [ ] Chrome (Android) version XX
|
||||
- [ ] Chrome (iOS) version XX
|
||||
- [ ] Firefox version XX
|
||||
- [ ] Safari (desktop) version XX
|
||||
- [ ] Safari (iOS) version XX
|
||||
- [ ] IE version XX
|
||||
- [ ] Edge version XX
|
||||
|
||||
For Tooling issues:
|
||||
- Node version: XX <!-- run `node --version` -->
|
||||
- Platform: <!-- Mac, Linux, Windows -->
|
||||
|
||||
Others:
|
||||
<!-- Anything else relevant? Operating system version, IDE, package manager, HTTP server, ... -->
|
||||
</code></pre>
|
||||
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
||||
|
63
.github/ISSUE_TEMPLATE/1-bug-report.md
vendored
Normal file
63
.github/ISSUE_TEMPLATE/1-bug-report.md
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
---
|
||||
name: "\U0001F41EBug report"
|
||||
about: Report a bug in the Angular Framework
|
||||
---
|
||||
<!--🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅
|
||||
|
||||
Oh hi there! 😄
|
||||
|
||||
To expedite issue processing please search open and closed issues before submitting a new one.
|
||||
Existing issues often contain information about workarounds, resolution, or progress updates.
|
||||
|
||||
🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅-->
|
||||
|
||||
|
||||
# 🐞 bug report
|
||||
|
||||
### Affected Package
|
||||
<!-- Can you pin-point one or more @angular/* packages as the source of the bug? -->
|
||||
<!-- ✍️edit: --> The issue is caused by package @angular/....
|
||||
|
||||
|
||||
### Is this a regression?
|
||||
|
||||
<!-- Did this behavior use to work in the previous version? -->
|
||||
<!-- ✍️--> Yes, the previous version in which this bug was not present was: ....
|
||||
|
||||
|
||||
### Description
|
||||
|
||||
<!-- ✍️--> A clear and concise description of the problem...
|
||||
|
||||
|
||||
## 🔬 Minimal Reproduction
|
||||
<!--
|
||||
Please create and share minimal reproduction of the issue starting with this template: https://stackblitz.com/fork/angular-issue-repro2
|
||||
-->
|
||||
<!-- ✍️--> https://stackblitz.com/...
|
||||
|
||||
<!--
|
||||
If StackBlitz is not suitable for reproduction of your issue, please create a minimal GitHub repository with the reproduction of the issue. Share the link to the repo below along with step-by-step instructions to reproduce the problem, as well as expected and actual behavior.
|
||||
-->
|
||||
|
||||
## 🔥 Exception or Error
|
||||
<pre><code>
|
||||
<!-- If the issue is accompanied by an exception or an error, please share it below: -->
|
||||
<!-- ✍️-->
|
||||
|
||||
</code></pre>
|
||||
|
||||
|
||||
## 🌍 Your Environment
|
||||
|
||||
**Angular Version:**
|
||||
<pre><code>
|
||||
<!-- run `ng version` and paste output below -->
|
||||
<!-- ✍️-->
|
||||
|
||||
</code></pre>
|
||||
|
||||
**Anything else relevant?**
|
||||
<!-- ✍️Is this a browser specific issue? If so, please specify the browser and version. -->
|
||||
|
||||
<!-- ✍️Do any of these matter: operating system, IDE, package manager, HTTP server, ...? If so, please mention it below. -->
|
32
.github/ISSUE_TEMPLATE/2-feature-request.md
vendored
Normal file
32
.github/ISSUE_TEMPLATE/2-feature-request.md
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
name: "\U0001F680Feature request"
|
||||
about: Suggest a feature for Angular Framework
|
||||
|
||||
---
|
||||
<!--🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅
|
||||
|
||||
Oh hi there! 😄
|
||||
|
||||
To expedite issue processing please search open and closed issues before submitting a new one.
|
||||
Existing issues often contain information about workarounds, resolution, or progress updates.
|
||||
|
||||
🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅-->
|
||||
|
||||
|
||||
# 🚀 feature request
|
||||
|
||||
### Releavant Package
|
||||
<!-- Can you pin-point one or more @angular/* packages the are relevant for this feature request? -->
|
||||
<!-- ✍️edit: --> This feature request is for @angular/....
|
||||
|
||||
|
||||
### Description
|
||||
<!-- ✍️--> A clear and concise description of the problem or missing capability...
|
||||
|
||||
|
||||
### Describe the solution you'd like
|
||||
<!-- ✍️--> If you have a solution in mind, please describe it.
|
||||
|
||||
|
||||
### Describe alternatives you've considered
|
||||
<!-- ✍️--> Have you considered any alternative solutions or workarounds?
|
55
.github/ISSUE_TEMPLATE/3-docs-bug.md
vendored
Normal file
55
.github/ISSUE_TEMPLATE/3-docs-bug.md
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
---
|
||||
name: "📚 Docs or angular.io issue report"
|
||||
about: Report an issue in Angular's documentation or angular.io application
|
||||
|
||||
---
|
||||
<!--🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅
|
||||
|
||||
Oh hi there! 😄
|
||||
|
||||
To expedite issue processing please search open and closed issues before submitting a new one.
|
||||
Existing issues often contain information about workarounds, resolution, or progress updates.
|
||||
|
||||
🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅-->
|
||||
|
||||
# 📚 Docs or angular.io bug report
|
||||
|
||||
### Description
|
||||
|
||||
<!-- ✍️edit:--> A clear and concise description of the problem...
|
||||
|
||||
|
||||
## 🔬 Minimal Reproduction
|
||||
|
||||
### What's the affected URL?**
|
||||
<!-- ✍️edit:--> https://angular.io/...
|
||||
|
||||
### Reproduction Steps**
|
||||
<!-- If applicable please list the steps to take to reproduce the issue -->
|
||||
<!-- ✍️edit:-->
|
||||
|
||||
### Expected vs Actual Behavior**
|
||||
<!-- If applicable please describe the difference between the expected and actual behavior after following the repro steps. -->
|
||||
<!-- ✍️edit:-->
|
||||
|
||||
|
||||
## 📷Screenshot
|
||||
<!-- Often a screenshot can help to capture the issue better than a long description. -->
|
||||
<!-- ✍️upload a screenshot:-->
|
||||
|
||||
|
||||
## 🔥 Exception or Error
|
||||
<pre><code>
|
||||
<!-- If the issue is accompanied by an exception or an error, please share it below: -->
|
||||
<!-- ✍️-->
|
||||
|
||||
</code></pre>
|
||||
|
||||
|
||||
## 🌍 Your Environment
|
||||
|
||||
### Browser info
|
||||
<!-- ✍️Is this a browser specific issue? If so, please specify the device, browser, and version. -->
|
||||
|
||||
### Anything else relevant?
|
||||
<!-- ✍️Please provide additional info if necessary. -->
|
11
.github/ISSUE_TEMPLATE/4-security-issue-disclosure.md
vendored
Normal file
11
.github/ISSUE_TEMPLATE/4-security-issue-disclosure.md
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
name: ⚠️ Security issue disclosure
|
||||
about: Report a security issue in Angular Framework, Material, or CLI
|
||||
|
||||
---
|
||||
|
||||
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
||||
|
||||
Please read https://angular.io/guide/security#report-issues on how to disclose security related issues.
|
||||
|
||||
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
16
.github/ISSUE_TEMPLATE/5-support-request.md
vendored
Normal file
16
.github/ISSUE_TEMPLATE/5-support-request.md
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
name: "❓Support request"
|
||||
about: Questions and requests for support
|
||||
|
||||
---
|
||||
|
||||
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
||||
|
||||
Please do not file questions or support requests on the GitHub issues tracker.
|
||||
|
||||
You can get your questions answered using other communication channels. Please see:
|
||||
https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question
|
||||
|
||||
Thank you!
|
||||
|
||||
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
13
.github/ISSUE_TEMPLATE/6-angular-cli.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE/6-angular-cli.md
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
name: "\U0001F6E0️Angular CLI"
|
||||
about: Issues and feature requests for Angular CLI
|
||||
|
||||
---
|
||||
|
||||
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
||||
|
||||
Please file any Angular CLI issues at: https://github.com/angular/angular-cli/issues/new
|
||||
|
||||
For the time being, we keep Angular CLI issues in a separate repository.
|
||||
|
||||
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
13
.github/ISSUE_TEMPLATE/7-angular-material.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE/7-angular-material.md
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
name: "\U0001F48EAngular Material"
|
||||
about: Issues and feature requests for Angular Material
|
||||
|
||||
---
|
||||
|
||||
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
||||
|
||||
Please file any Angular Material issues at: https://github.com/angular/material2/issues/new
|
||||
|
||||
For the time being, we keep Angular Material issues in a separate repository.
|
||||
|
||||
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
30
.github/PULL_REQUEST_TEMPLATE.md
vendored
30
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -10,17 +10,17 @@ Please check if your PR fulfills the following requirements:
|
||||
What kind of change does this PR introduce?
|
||||
|
||||
<!-- Please check the one that applies to this PR using "x". -->
|
||||
```
|
||||
[ ] Bugfix
|
||||
[ ] Feature
|
||||
[ ] Code style update (formatting, local variables)
|
||||
[ ] Refactoring (no functional changes, no api changes)
|
||||
[ ] Build related changes
|
||||
[ ] CI related changes
|
||||
[ ] Documentation content changes
|
||||
[ ] angular.io application / infrastructure changes
|
||||
[ ] Other... Please describe:
|
||||
```
|
||||
|
||||
- [ ] Bugfix
|
||||
- [ ] Feature
|
||||
- [ ] Code style update (formatting, local variables)
|
||||
- [ ] Refactoring (no functional changes, no api changes)
|
||||
- [ ] Build related changes
|
||||
- [ ] CI related changes
|
||||
- [ ] Documentation content changes
|
||||
- [ ] angular.io application / infrastructure changes
|
||||
- [ ] Other... Please describe:
|
||||
|
||||
|
||||
## What is the current behavior?
|
||||
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
|
||||
@ -32,10 +32,10 @@ Issue Number: N/A
|
||||
|
||||
|
||||
## Does this PR introduce a breaking change?
|
||||
```
|
||||
[ ] Yes
|
||||
[ ] No
|
||||
```
|
||||
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
|
||||
|
||||
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
|
||||
|
||||
|
28
.github/angular-robot.yml
vendored
28
.github/angular-robot.yml
vendored
@ -3,11 +3,8 @@
|
||||
#options for the size plugin
|
||||
size:
|
||||
disabled: false
|
||||
maxSizeIncrease: 1000
|
||||
circleCiStatusName: "ci/circleci: build-packages-dist"
|
||||
status:
|
||||
disabled: false
|
||||
context: "ci/angular: size"
|
||||
maxSizeIncrease: 2000
|
||||
circleCiStatusName: "ci/circleci: test"
|
||||
|
||||
# options for the merge plugin
|
||||
merge:
|
||||
@ -42,6 +39,7 @@ merge:
|
||||
- "packages/**"
|
||||
# list of patterns to ignore for the files changed by the PR
|
||||
exclude:
|
||||
- "packages/bazel/*.bzl"
|
||||
- "packages/language-service/**"
|
||||
- "**/.gitignore"
|
||||
- "**/.gitkeep"
|
||||
@ -127,3 +125,23 @@ triage:
|
||||
-
|
||||
- "type: RFC / Discussion / question"
|
||||
- "comp: *"
|
||||
|
||||
# options for the triage PR plugin
|
||||
triagePR:
|
||||
# set to true to disable
|
||||
disabled: false
|
||||
# number of the milestone to apply when the PR has not been triaged yet
|
||||
needsTriageMilestone: 83,
|
||||
# number of the milestone to apply when the PR is triaged
|
||||
defaultMilestone: 82,
|
||||
# arrays of labels that determine if a PR has been triaged by the caretaker
|
||||
l1TriageLabels:
|
||||
-
|
||||
- "comp: *"
|
||||
# arrays of labels that determine if a PR has been fully triaged
|
||||
l2TriageLabels:
|
||||
-
|
||||
- "type: *"
|
||||
- "effort*"
|
||||
- "risk*"
|
||||
- "comp: *"
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -14,7 +14,6 @@ pubspec.lock
|
||||
.settings/
|
||||
*.swo
|
||||
modules/.settings
|
||||
.bazelrc
|
||||
.vscode
|
||||
modules/.vscode
|
||||
|
||||
|
@ -87,10 +87,10 @@ groups:
|
||||
files:
|
||||
include:
|
||||
- "WORKSPACE"
|
||||
- ".bazel*"
|
||||
- "*.bazel"
|
||||
- "*.bzl"
|
||||
- "packages/bazel/*"
|
||||
- "tools/bazel.rc"
|
||||
- "/docs/BAZEL.md"
|
||||
users:
|
||||
- alexeagle #primary
|
||||
@ -108,9 +108,9 @@ groups:
|
||||
- "*.lock"
|
||||
- "tools/*"
|
||||
exclude:
|
||||
- "tools/bazel.rc"
|
||||
- "tools/public_api_guard/*"
|
||||
- "aio/*"
|
||||
- "packages/core/test/bundling/*"
|
||||
- "tools/public_api_guard/*"
|
||||
users:
|
||||
- IgorMinar #primary
|
||||
- alexeagle
|
||||
@ -277,6 +277,9 @@ groups:
|
||||
- "aio/content/guide/forms.md"
|
||||
- "aio/content/examples/forms/*"
|
||||
- "aio/content/images/guide/forms/*"
|
||||
- "aio/content/guide/forms-overview.md"
|
||||
- "aio/content/examples/forms-overview/*"
|
||||
- "aio/content/images/guide/forms-overview/*"
|
||||
- "aio/content/guide/form-validation.md"
|
||||
- "aio/content/examples/form-validation/*"
|
||||
- "aio/content/images/guide/form-validation/*"
|
||||
|
16
.travis.yml
16
.travis.yml
@ -2,7 +2,7 @@ language: node_js
|
||||
sudo: false
|
||||
dist: trusty
|
||||
node_js:
|
||||
- '8.9.1'
|
||||
- '10.9.0'
|
||||
|
||||
addons:
|
||||
# firefox: "38.0"
|
||||
@ -30,14 +30,6 @@ env:
|
||||
# GITHUB_TOKEN_ANGULAR=<github token, a personal access token of the angular-builds account, account access in valentine>
|
||||
# This is needed for the e2e Travis matrix task to publish packages to github for continuous packages delivery.
|
||||
- secure: "aCdHveZuY8AT4Jr1JoJB4LxZsnGWRe/KseZh1YXYe5UtufFCtTVHvUcLn0j2aLBF0KpdyS+hWf0i4np9jthKu2xPKriefoPgCMpisYeC0MFkwbmv+XlgkUbgkgVZMGiVyX7DCYXVahxIoOUjVMEDCbNiHTIrfEuyq24U3ok2tHc="
|
||||
# FIREBASE_TOKEN
|
||||
# This is needed for publishing builds to the "aio-staging" and "angular-io" firebase projects.
|
||||
# This token was generated using the aio-deploy@angular.io account using `firebase login:ci` and password from valentine
|
||||
- secure: "L5CyQmpwWtoR4Qi4xlWQh/cL1M6ZeJL4W4QAr4HdKFMgYt9h+Whqkymyh2NxwmCbPvWa7yUd+OiLQUDCY7L2VIg16hTwoe2CgYDyQA0BEwLzxtRrJXl93TfwMlrUx5JSIzAccD6D4sjtz8kSFMomK2Nls33xOXOukwyhVMjd0Cg="
|
||||
# ANGULAR_PAYLOAD_FIREBASE_TOKEN
|
||||
# This is for payload size data to "angular-payload-size" firebase project
|
||||
# This token was generated using the payload@angular.io account using `firebase login:ci` and password from valentine
|
||||
- secure: "SxotP/ymNy6uWAVbfwM9BlwETPEBpkRvU/F7fCtQDDic99WfQHzzUSQqHTk8eKk3GrGAOSL09vT0WfStQYEIGEoS5UHWNgOnelxhw+d5EnaoB8vQ0dKQBTK092hQg4feFprr+B/tCasyMV6mVwpUzZMbIJNn/Rx7H5g1bp+Gkfg="
|
||||
matrix:
|
||||
# Order: a slower build first, so that we don't occupy an idle travis worker waiting for others to complete.
|
||||
- CI_MODE=e2e
|
||||
@ -47,10 +39,6 @@ env:
|
||||
# - CI_MODE=browserstack_required
|
||||
- CI_MODE=saucelabs_optional
|
||||
- CI_MODE=browserstack_optional
|
||||
- CI_MODE=aio_tools_test
|
||||
- CI_MODE=aio
|
||||
- CI_MODE=aio_e2e AIO_SHARD=0
|
||||
- CI_MODE=aio_e2e AIO_SHARD=1
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
@ -68,8 +56,6 @@ install:
|
||||
script:
|
||||
- ./scripts/ci/build.sh
|
||||
- ./scripts/ci/test.sh
|
||||
# deploy is part of 'script' and not 'after_success' so that we fail the build if the deployment fails
|
||||
- ./scripts/ci/deploy.sh
|
||||
- ./scripts/ci/angular.sh
|
||||
# all the scripts under this line will not quickly abort in case ${TRAVIS_TEST_RESULT} is 1 (job failure)
|
||||
- ./scripts/ci/cleanup.sh
|
||||
|
50
BUILD.bazel
50
BUILD.bazel
@ -8,26 +8,14 @@ exports_files([
|
||||
"protractor-perf.conf.js",
|
||||
])
|
||||
|
||||
# Developers should always run `bazel run :install`
|
||||
# This ensures that package.json in subdirectories get installed as well.
|
||||
alias(
|
||||
name = "install",
|
||||
actual = "@nodejs//:yarn",
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "node_modules",
|
||||
actual = "@angular_deps//:node_modules",
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "web_test_bootstrap_scripts",
|
||||
# do not sort
|
||||
srcs = [
|
||||
"@angular_deps//:node_modules/reflect-metadata/Reflect.js",
|
||||
"@angular_deps//:node_modules/zone.js/dist/zone.js",
|
||||
"@angular_deps//:node_modules/zone.js/dist/zone-testing.js",
|
||||
"@angular_deps//:node_modules/zone.js/dist/task-tracking.js",
|
||||
"@ngdeps//node_modules/reflect-metadata:Reflect.js",
|
||||
"@ngdeps//node_modules/zone.js:dist/zone.js",
|
||||
"@ngdeps//node_modules/zone.js:dist/zone-testing.js",
|
||||
"@ngdeps//node_modules/zone.js:dist/task-tracking.js",
|
||||
"//:test-events.js",
|
||||
],
|
||||
)
|
||||
@ -35,11 +23,29 @@ filegroup(
|
||||
filegroup(
|
||||
name = "angularjs_scripts",
|
||||
srcs = [
|
||||
"@angular_deps//:node_modules/angular-1.5/angular.js",
|
||||
"@angular_deps//:node_modules/angular-1.6/angular.js",
|
||||
"@angular_deps//:node_modules/angular-mocks-1.5/angular-mocks.js",
|
||||
"@angular_deps//:node_modules/angular-mocks-1.6/angular-mocks.js",
|
||||
"@angular_deps//:node_modules/angular-mocks/angular-mocks.js",
|
||||
"@angular_deps//:node_modules/angular/angular.js",
|
||||
"@ngdeps//node_modules/angular:angular.js",
|
||||
"@ngdeps//node_modules/angular-1.5:angular.js",
|
||||
"@ngdeps//node_modules/angular-1.6:angular.js",
|
||||
"@ngdeps//node_modules/angular-mocks:angular-mocks.js",
|
||||
"@ngdeps//node_modules/angular-mocks-1.5:angular-mocks.js",
|
||||
"@ngdeps//node_modules/angular-mocks-1.6:angular-mocks.js",
|
||||
],
|
||||
)
|
||||
|
||||
load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary")
|
||||
|
||||
# A nodejs_binary for @angular/bazel/ngc-wrapped to use by default in
|
||||
# ng_module that depends on @npm//@angular/bazel instead of the
|
||||
# output of the //packages/bazel/src/ngc-wrapped ts_library rule. This
|
||||
# default is for downstream users that depend on the @angular/bazel npm
|
||||
# package. The generated @npm//@angular/bazel/ngc-wrapped target
|
||||
# does not work because it does not have the node `--expose-gc` flag
|
||||
# set which is required to support the call to `global.gc()`.
|
||||
nodejs_binary(
|
||||
name = "@angular/bazel/ngc-wrapped",
|
||||
configuration_env_vars = ["compile"],
|
||||
data = ["@npm//@angular/bazel"],
|
||||
entry_point = "@angular/bazel/src/ngc-wrapped/index.js",
|
||||
install_source_map_support = False,
|
||||
templated_args = ["--node_options=--expose-gc"],
|
||||
)
|
||||
|
193
CHANGELOG.md
193
CHANGELOG.md
@ -1,24 +1,126 @@
|
||||
<a name="7.0.0-beta.5"></a>
|
||||
# [7.0.0-beta.5](https://github.com/angular/angular/compare/7.0.0-beta.4...7.0.0-beta.5) (2018-09-06)
|
||||
<a name="7.0.4"></a>
|
||||
## [7.0.4](https://github.com/angular/angular/compare/7.0.3...7.0.4) (2018-11-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bazel:** protractor rule should include *.e2e-spec.js ([#25701](https://github.com/angular/angular/issues/25701)) ([3809e0f](https://github.com/angular/angular/commit/3809e0f))
|
||||
* **benchpress:** Use performance.mark() instead of console.time() ([#24114](https://github.com/angular/angular/issues/24114)) ([06d0400](https://github.com/angular/angular/commit/06d0400))
|
||||
* **compiler:** add hostVars and support pure functions in host bindings ([#25626](https://github.com/angular/angular/issues/25626)) ([b424b31](https://github.com/angular/angular/commit/b424b31))
|
||||
* **core:** do not clear element content when using shadow dom ([#24861](https://github.com/angular/angular/issues/24861)) ([6e828bb](https://github.com/angular/angular/commit/6e828bb))
|
||||
* **core:** size regression with closure compiler ([#25531](https://github.com/angular/angular/issues/25531)) ([1f59f2f](https://github.com/angular/angular/commit/1f59f2f))
|
||||
* **elements:** add compiler dependency ([#24861](https://github.com/angular/angular/issues/24861)) ([6143da6](https://github.com/angular/angular/commit/6143da6))
|
||||
* **elements:** add compiler to integration ([#24861](https://github.com/angular/angular/issues/24861)) ([a080ffc](https://github.com/angular/angular/commit/a080ffc))
|
||||
* **elements:** strict null checks ([#24861](https://github.com/angular/angular/issues/24861)) ([a8210d0](https://github.com/angular/angular/commit/a8210d0))
|
||||
* **upgrade:** trigger `$destroy` event on upgraded component element ([#25357](https://github.com/angular/angular/issues/25357)) ([2a672a9](https://github.com/angular/angular/commit/2a672a9)), closes [#25334](https://github.com/angular/angular/issues/25334)
|
||||
* **compiler-cli:** add missing tslib dependency ([#27063](https://github.com/angular/angular/issues/27063)) ([4348c47](https://github.com/angular/angular/commit/4348c47))
|
||||
* **compiler-cli:** only pass canonical genfile paths to compiler host ([#27062](https://github.com/angular/angular/issues/27062)) ([188e9ce](https://github.com/angular/angular/commit/188e9ce))
|
||||
* **router:** add `relativeLinkResolution` to `recognize` operator ([#26990](https://github.com/angular/angular/issues/26990)) ([d304427](https://github.com/angular/angular/commit/d304427)), closes [#26983](https://github.com/angular/angular/issues/26983)
|
||||
|
||||
|
||||
|
||||
<a name="7.0.3"></a>
|
||||
## [7.0.3](https://github.com/angular/angular/compare/7.0.2...7.0.3) (2018-11-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bazel:** unknown replay compiler error in windows ([#26711](https://github.com/angular/angular/issues/26711)) ([4d532df](https://github.com/angular/angular/commit/4d532df))
|
||||
* **router:** remove type bludgeoning of context and outlet when running CanDeactivate ([#26496](https://github.com/angular/angular/issues/26496)) ([dc05385](https://github.com/angular/angular/commit/dc05385)), closes [#18253](https://github.com/angular/angular/issues/18253)
|
||||
* **upgrade:** make typings compatible with older AngularJS typings ([#26880](https://github.com/angular/angular/issues/26880)) ([315d95c](https://github.com/angular/angular/commit/315d95c)), closes [#26420](https://github.com/angular/angular/issues/26420)
|
||||
|
||||
|
||||
|
||||
<a name="7.0.2"></a>
|
||||
## [7.0.2](https://github.com/angular/angular/compare/7.0.1...7.0.2) (2018-10-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler:** generate relative paths only in summary file errors ([#26759](https://github.com/angular/angular/issues/26759)) ([c01f340](https://github.com/angular/angular/commit/c01f340))
|
||||
* **core:** Remove static dependency from [@angular](https://github.com/angular)/core to [@angular](https://github.com/angular)/compiler ([#26734](https://github.com/angular/angular/issues/26734)) ([#26879](https://github.com/angular/angular/issues/26879)) ([257ac83](https://github.com/angular/angular/commit/257ac83))
|
||||
* **core:** support computed base class in metadata inheritance ([#24014](https://github.com/angular/angular/issues/24014)) ([b3c6409](https://github.com/angular/angular/commit/b3c6409))
|
||||
|
||||
|
||||
|
||||
<a name="7.0.1"></a>
|
||||
## [7.0.1](https://github.com/angular/angular/compare/7.0.0...7.0.1) (2018-10-24)
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="7.0.0"></a>
|
||||
# [7.0.0](https://github.com/angular/angular/compare/7.0.0-rc.1...7.0.0) (2018-10-18)
|
||||
|
||||
|
||||
### Release Highlights & Update instructions
|
||||
|
||||
To learn about the release highlights and our new CLI-powered update workflow for your projects please check out the [v7 release announcement](https://blog.angular.io/version-7-of-angular-cli-prompts-virtual-scroll-drag-and-drop-and-more-c594e22e7b8c).
|
||||
|
||||
|
||||
### Dependency updates
|
||||
|
||||
* @angular/core now depends on
|
||||
* TypeScript 3.1
|
||||
* RxJS 6.3
|
||||
* @angular/platform-server now depends on Domino 2.1
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **core:** add DoBootstrap interface. ([#24558](https://github.com/angular/angular/issues/24558)) ([732026c](https://github.com/angular/angular/commit/732026c)), closes [#24557](https://github.com/angular/angular/issues/24557)
|
||||
* **compiler:** add "original" placeholder value on extracted XMB ([#25079](https://github.com/angular/angular/issues/25079)) ([e99d860](https://github.com/angular/angular/commit/e99d860))
|
||||
* **compiler-cli:** add support to extend `angularCompilerOptions` ([#22717](https://github.com/angular/angular/issues/22717)) ([d7e5bbf](https://github.com/angular/angular/commit/d7e5bbf)), closes [#22684](https://github.com/angular/angular/issues/22684)
|
||||
* **bazel:** add additional parameters to `ts_api_guardian_test` def ([#25694](https://github.com/angular/angular/issues/25694)) ([2a21ca0](https://github.com/angular/angular/commit/2a21ca0))
|
||||
* **elements:** enable Shadow DOM v1 and slots ([#24861](https://github.com/angular/angular/issues/24861)) ([c9844a2](https://github.com/angular/angular/commit/c9844a2))
|
||||
* **platform-server:** update domino to v2.1.0 ([#25564](https://github.com/angular/angular/issues/25564)) ([3fb0da2](https://github.com/angular/angular/commit/3fb0da2))
|
||||
* **router:** warn if navigation triggered outside Angular zone ([#24959](https://github.com/angular/angular/issues/24959)) ([010e35d](https://github.com/angular/angular/commit/010e35d)), closes [#15770](https://github.com/angular/angular/issues/15770) [#15946](https://github.com/angular/angular/issues/15946) [#24728](https://github.com/angular/angular/issues/24728)
|
||||
* **router:** add UrlSegment[] to CanLoad interface ([#13127](https://github.com/angular/angular/issues/13127)) ([07d8d39](https://github.com/angular/angular/commit/07d8d39)), closes [#12411](https://github.com/angular/angular/issues/12411)
|
||||
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add mappings for ngfactory & ngsummary files to their module names in aot summary resolver ([#25335](https://github.com/angular/angular/issues/25335)) ([02e201a](https://github.com/angular/angular/commit/02e201a))
|
||||
* **bazel:** Cache fileNameToModuleName lookups ([#25731](https://github.com/angular/angular/issues/25731)) ([f394ba0](https://github.com/angular/angular/commit/f394ba0))
|
||||
* **bazel:** allow compile_strategy to be (privately) imported ([#25080](https://github.com/angular/angular/issues/25080)) ([0d1d589](https://github.com/angular/angular/commit/0d1d589))
|
||||
* **bazel:** correct type concatenated to devmode_js ([#25467](https://github.com/angular/angular/issues/25467)) ([fb2c524](https://github.com/angular/angular/commit/fb2c524))
|
||||
* **bazel:** move bazel managed runtime deps for downstream usage ([#25690](https://github.com/angular/angular/issues/25690)) ([6ed7993](https://github.com/angular/angular/commit/6ed7993))
|
||||
* **bazel:** only lookup amd module-name tags in .d.ts files ([#25710](https://github.com/angular/angular/issues/25710)) ([42072c4](https://github.com/angular/angular/commit/42072c4))
|
||||
* **bazel:** protractor rule should include *.e2e-spec.js ([#25701](https://github.com/angular/angular/issues/25701)) ([3809e0f](https://github.com/angular/angular/commit/3809e0f))
|
||||
* **bazel:** specify the package and lock files using the workspace ([#25694](https://github.com/angular/angular/issues/25694)) ([ddc1335](https://github.com/angular/angular/commit/ddc1335))
|
||||
* **benchpress:** Use performance.mark() instead of console.time() ([#24114](https://github.com/angular/angular/issues/24114)) ([06d0400](https://github.com/angular/angular/commit/06d0400))
|
||||
* **common:** register locale data for all equivalent closure locales ([#25867](https://github.com/angular/angular/issues/25867)) ([d83f9d4](https://github.com/angular/angular/commit/d83f9d4))
|
||||
* **compiler-cli:** correct realPath to realpath. ([#25023](https://github.com/angular/angular/issues/25023)) ([01e6dab](https://github.com/angular/angular/commit/01e6dab))
|
||||
* **compiler-cli:** use the oldProgram option in watch mode ([#21364](https://github.com/angular/angular/issues/21364)) ([c6e5b97](https://github.com/angular/angular/commit/c6e5b97)), closes [#21361](https://github.com/angular/angular/issues/21361)
|
||||
* **compiler:** Fix look up of entryComponents in AOT Summaries ([#24892](https://github.com/angular/angular/issues/24892)) ([00d3666](https://github.com/angular/angular/commit/00d3666))
|
||||
* **compiler:** add hostVars and support pure functions in host bindings ([#25626](https://github.com/angular/angular/issues/25626)) ([b424b31](https://github.com/angular/angular/commit/b424b31))
|
||||
* **compiler:** update compiler to flatten nested template fns ([#24943](https://github.com/angular/angular/issues/24943)) ([fe14f18](https://github.com/angular/angular/commit/fe14f18))
|
||||
* **compiler:** update compiler to generate new slot allocations ([#25607](https://github.com/angular/angular/issues/25607)) ([27e2039](https://github.com/angular/angular/commit/27e2039))
|
||||
* **core:** In Testability.whenStable update callback, pass more complete ([#25010](https://github.com/angular/angular/issues/25010)) ([16c03c0](https://github.com/angular/angular/commit/16c03c0))
|
||||
* **core:** add missing `peerDependency ` to `[@angular](https://github.com/angular)/compiler` ([#26033](https://github.com/angular/angular/issues/26033)) ([549de1e](https://github.com/angular/angular/commit/549de1e)), closes [/github.com/angular/angular/commit/919f42fea1df4b9e38b7d688aef5f2de668e9d3e#diff-58563046c4439699f2e6a89187099a54](https://github.com//github.com/angular/angular/commit/919f42fea1df4b9e38b7d688aef5f2de668e9d3e/issues/diff-58563046c4439699f2e6a89187099a54)
|
||||
* **core:** allow null value for renderer setElement(…) ([#17065](https://github.com/angular/angular/issues/17065)) ([ff15043](https://github.com/angular/angular/commit/ff15043)), closes [#13686](https://github.com/angular/angular/issues/13686)
|
||||
* **core:** do not clear element content when using shadow dom ([#24861](https://github.com/angular/angular/issues/24861)) ([6e828bb](https://github.com/angular/angular/commit/6e828bb))
|
||||
* **core:** size regression with closure compiler ([#25531](https://github.com/angular/angular/issues/25531)) ([1f59f2f](https://github.com/angular/angular/commit/1f59f2f))
|
||||
* **core:** throw error message when @Output not initialized ([#19116](https://github.com/angular/angular/issues/19116)) ([adf510f](https://github.com/angular/angular/commit/adf510f)), closes [#3664](https://github.com/angular/angular/issues/3664)
|
||||
* **elements:** add compiler dependency ([#24861](https://github.com/angular/angular/issues/24861)) ([6143da6](https://github.com/angular/angular/commit/6143da6))
|
||||
* **elements:** add compiler to integration ([#24861](https://github.com/angular/angular/issues/24861)) ([a080ffc](https://github.com/angular/angular/commit/a080ffc))
|
||||
* **elements:** strict null checks ([#24861](https://github.com/angular/angular/issues/24861)) ([a8210d0](https://github.com/angular/angular/commit/a8210d0))
|
||||
* **router:** fix regression where navigateByUrl promise didn't resolve on CanLoad failure ([#26455](https://github.com/angular/angular/issues/26455)) ([1c9b065](https://github.com/angular/angular/commit/1c9b065)), closes [#26284](https://github.com/angular/angular/issues/26284)
|
||||
* **router:** mount correct component if router outlet was not instantiated and if using a route reuse strategy ([#25313](https://github.com/angular/angular/issues/25313)) ([#25314](https://github.com/angular/angular/issues/25314)) ([8dc2b11](https://github.com/angular/angular/commit/8dc2b11))
|
||||
* **router:** take base uri into account in `setUpLocationSync()` ([#20244](https://github.com/angular/angular/issues/20244)) ([ba1e25f](https://github.com/angular/angular/commit/ba1e25f)), closes [#20061](https://github.com/angular/angular/issues/20061)
|
||||
* **service-worker:** clean up caches from old SW versions ([#26319](https://github.com/angular/angular/issues/26319)) ([00b5c7b](https://github.com/angular/angular/commit/00b5c7b))
|
||||
* **service-worker:** do not blow up when caches are unwritable ([#26042](https://github.com/angular/angular/issues/26042)) ([2bd767c](https://github.com/angular/angular/commit/2bd767c))
|
||||
* **upgrade:** properly destroy upgraded component elements and descendants ([#26209](https://github.com/angular/angular/issues/26209)) ([071934e](https://github.com/angular/angular/commit/071934e)), closes [#26208](https://github.com/angular/angular/issues/26208)
|
||||
* **upgrade:** trigger `$destroy` event on upgraded component element ([#25357](https://github.com/angular/angular/issues/25357)) ([2a672a9](https://github.com/angular/angular/commit/2a672a9)), closes [#25334](https://github.com/angular/angular/issues/25334)
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="6.1.10"></a>
|
||||
## [6.1.10](https://github.com/angular/angular/compare/6.1.9...6.1.10) (2018-10-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **platform-browser:** fix [#22155](https://github.com/angular/angular/issues/22155), destroy hammer manager when `HammerInstance.off()` is run ([#22156](https://github.com/angular/angular/issues/22156)) ([3b4d9dc](https://github.com/angular/angular/commit/3b4d9dc))
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="6.1.9"></a>
|
||||
## [6.1.9](https://github.com/angular/angular/compare/6.1.8...6.1.9) (2018-09-26)
|
||||
|
||||
|
||||
|
||||
@ -33,27 +135,9 @@
|
||||
* **core:** size regression with closure compiler ([#25531](https://github.com/angular/angular/issues/25531)) ([ebcf762](https://github.com/angular/angular/commit/ebcf762))
|
||||
* **docs-infra:** show "suggest edits" only for /guide and /tutorial dirs ([#24378](https://github.com/angular/angular/issues/24378)) ([66b7870](https://github.com/angular/angular/commit/66b7870))
|
||||
* **upgrade:** trigger `$destroy` event on upgraded component element ([#25357](https://github.com/angular/angular/issues/25357)) ([82e0676](https://github.com/angular/angular/commit/82e0676)), closes [#25334](https://github.com/angular/angular/issues/25334)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **docs-infra:** add "suggest edits" feature to all docs ([#24378](https://github.com/angular/angular/issues/24378)) ([82088a8](https://github.com/angular/angular/commit/82088a8))
|
||||
* **docs-infra:** disable "status" selector in API list when displaying only packages ([#25718](https://github.com/angular/angular/issues/25718)) ([6f7df8a](https://github.com/angular/angular/commit/6f7df8a)), closes [#25708](https://github.com/angular/angular/issues/25708)
|
||||
* **router:** warn if navigation triggered outside Angular zone ([#24959](https://github.com/angular/angular/issues/24959)) ([23a96dc](https://github.com/angular/angular/commit/23a96dc)), closes [#15770](https://github.com/angular/angular/issues/15770) [#15946](https://github.com/angular/angular/issues/15946) [#24728](https://github.com/angular/angular/issues/24728)
|
||||
|
||||
|
||||
<a name="7.0.0-beta.4"></a>
|
||||
# [7.0.0-beta.4](https://github.com/angular/angular/compare/7.0.0-beta.3...7.0.0-beta.4) (2018-08-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bazel:** Cache fileNameToModuleName lookups ([#25731](https://github.com/angular/angular/issues/25731)) ([f394ba0](https://github.com/angular/angular/commit/f394ba0))
|
||||
* **bazel:** move bazel managed runtime deps for downstream usage ([#25690](https://github.com/angular/angular/issues/25690)) ([6ed7993](https://github.com/angular/angular/commit/6ed7993))
|
||||
* **bazel:** only lookup amd module-name tags in .d.ts files ([#25710](https://github.com/angular/angular/issues/25710)) ([42072c4](https://github.com/angular/angular/commit/42072c4))
|
||||
* **compiler:** update compiler to generate new slot allocations ([#25607](https://github.com/angular/angular/issues/25607)) ([27e2039](https://github.com/angular/angular/commit/27e2039))
|
||||
|
||||
|
||||
|
||||
<a name="6.1.6"></a>
|
||||
## [6.1.6](https://github.com/angular/angular/compare/6.1.5...6.1.6) (2018-08-29)
|
||||
@ -67,14 +151,6 @@
|
||||
|
||||
Note: the 6.1.5 release on npm accidentally glitched-out midway, so we cut 6.1.6 instead. sorry! :-)
|
||||
|
||||
<a name="7.0.0-beta.3"></a>
|
||||
# [7.0.0-beta.3](https://github.com/angular/angular/compare/7.0.0-beta.2...7.0.0-beta.3) (2018-08-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **router:** add UrlSegment[] to CanLoad interface ([#13127](https://github.com/angular/angular/issues/13127)) ([07d8d39](https://github.com/angular/angular/commit/07d8d39)), closes [#12411](https://github.com/angular/angular/issues/12411)
|
||||
|
||||
|
||||
|
||||
<a name="6.1.4"></a>
|
||||
@ -87,15 +163,6 @@ Note: the 6.1.5 release on npm accidentally glitched-out midway, so we cut 6.1.6
|
||||
|
||||
|
||||
|
||||
<a name="7.0.0-beta.2"></a>
|
||||
# [7.0.0-beta.2](https://github.com/angular/angular/compare/7.0.0-beta.1...7.0.0-beta.2) (2018-08-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bazel:** correct type concatenated to devmode_js ([#25467](https://github.com/angular/angular/issues/25467)) ([fb2c524](https://github.com/angular/angular/commit/fb2c524))
|
||||
|
||||
|
||||
<a name="6.1.3"></a>
|
||||
## [6.1.3](https://github.com/angular/angular/compare/6.1.2...6.1.3) (2018-08-15)
|
||||
|
||||
@ -106,24 +173,6 @@ Note: the 6.1.5 release on npm accidentally glitched-out midway, so we cut 6.1.6
|
||||
|
||||
|
||||
|
||||
<a name="7.0.0-beta.1"></a>
|
||||
# [7.0.0-beta.1](https://github.com/angular/angular/compare/7.0.0-beta.0...7.0.0-beta.1) (2018-08-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler-cli:** use the oldProgram option in watch mode ([#21364](https://github.com/angular/angular/issues/21364)) ([c6e5b97](https://github.com/angular/angular/commit/c6e5b97)), closes [#21361](https://github.com/angular/angular/issues/21361)
|
||||
* **core:** In Testability.whenStable update callback, pass more complete ([#25010](https://github.com/angular/angular/issues/25010)) ([16c03c0](https://github.com/angular/angular/commit/16c03c0))
|
||||
* add mappings for ngfactory & ngsummary files to their module names in aot summary resolver ([#25335](https://github.com/angular/angular/issues/25335)) ([02e201a](https://github.com/angular/angular/commit/02e201a))
|
||||
* **router:** take base uri into account in `setUpLocationSync()` ([#20244](https://github.com/angular/angular/issues/20244)) ([ba1e25f](https://github.com/angular/angular/commit/ba1e25f)), closes [#20061](https://github.com/angular/angular/issues/20061)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **core:** add DoBootstrap interface. ([#24558](https://github.com/angular/angular/issues/24558)) ([732026c](https://github.com/angular/angular/commit/732026c)), closes [#24557](https://github.com/angular/angular/issues/24557)
|
||||
|
||||
|
||||
|
||||
<a name="6.1.2"></a>
|
||||
## [6.1.2](https://github.com/angular/angular/compare/6.1.1...6.1.2) (2018-08-08)
|
||||
|
||||
@ -134,22 +183,6 @@ Note: the 6.1.5 release on npm accidentally glitched-out midway, so we cut 6.1.6
|
||||
* add mappings for ngfactory & ngsummary files to their module names in aot summary resolver ([#25335](https://github.com/angular/angular/issues/25335)) ([054fbbe](https://github.com/angular/angular/commit/054fbbe))
|
||||
|
||||
|
||||
<a name="7.0.0-beta.0"></a>
|
||||
# [7.0.0-beta.0](https://github.com/angular/angular/compare/6.1.0...7.0.0-beta.0) (2018-08-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bazel:** allow compile_strategy to be (privately) imported ([#25080](https://github.com/angular/angular/issues/25080)) ([0d1d589](https://github.com/angular/angular/commit/0d1d589))
|
||||
* **compiler:** update compiler to flatten nested template fns ([#24943](https://github.com/angular/angular/issues/24943)) ([fe14f18](https://github.com/angular/angular/commit/fe14f18))
|
||||
* **compiler-cli:** correct realPath to realpath. ([#25023](https://github.com/angular/angular/issues/25023)) ([01e6dab](https://github.com/angular/angular/commit/01e6dab))
|
||||
* **core:** throw error message when @Output not initialized ([#19116](https://github.com/angular/angular/issues/19116)) ([adf510f](https://github.com/angular/angular/commit/adf510f)), closes [#3664](https://github.com/angular/angular/issues/3664)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler:** add "original" placeholder value on extracted XMB ([#25079](https://github.com/angular/angular/issues/25079)) ([e99d860](https://github.com/angular/angular/commit/e99d860))
|
||||
|
||||
|
||||
|
||||
<a name="6.1.1"></a>
|
||||
## [6.1.1](https://github.com/angular/angular/compare/6.1.0...6.1.1) (2018-08-02)
|
||||
|
@ -71,6 +71,8 @@ Before you submit your Pull Request (PR) consider the following guidelines:
|
||||
|
||||
1. Search [GitHub](https://github.com/angular/angular/pulls) for an open or closed PR
|
||||
that relates to your submission. You don't want to duplicate effort.
|
||||
1. Be sure that an issue describes the problem you're fixing, or documents the design for the feature you'd like to add.
|
||||
Discussing the design up front helps to ensure that we're ready to accept your work.
|
||||
1. Please sign our [Contributor License Agreement (CLA)](#cla) before sending PRs.
|
||||
We cannot accept code without this. Make sure you sign with the primary email address of the Git identity that has been granted access to the Angular repository.
|
||||
1. Fork the angular/angular repo.
|
||||
|
@ -13,12 +13,10 @@ Angular is a development platform for building mobile and desktop web applicatio
|
||||
|
||||
[Get started in 5 minutes][quickstart].
|
||||
|
||||
|
||||
## Changelog
|
||||
|
||||
[Learn about the latest improvements][changelog].
|
||||
|
||||
|
||||
## Want to help?
|
||||
|
||||
Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on our
|
||||
|
122
WORKSPACE
122
WORKSPACE
@ -1,96 +1,34 @@
|
||||
workspace(name = "angular")
|
||||
|
||||
#
|
||||
# Download Bazel toolchain dependencies as needed by build actions
|
||||
#
|
||||
http_archive(
|
||||
name = "build_bazel_rules_nodejs",
|
||||
urls = ["https://github.com/bazelbuild/rules_nodejs/archive/0.12.0.zip"],
|
||||
strip_prefix = "rules_nodejs-0.12.0",
|
||||
sha256 = "2977cdbc8ae0eed7d4186385af56a50a3321a549e2136a959998bba89d2edb6e",
|
||||
load(
|
||||
"//packages/bazel:package.bzl",
|
||||
"rules_angular_dependencies",
|
||||
"rules_angular_dev_dependencies",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "build_bazel_rules_typescript",
|
||||
url = "https://github.com/bazelbuild/rules_typescript/archive/0.16.2.zip",
|
||||
strip_prefix = "rules_typescript-0.16.2",
|
||||
sha256 = "31601b777840fbf600dbd1893ade0d1de37166e7ba52b90735b107cfb67e38c7",
|
||||
)
|
||||
load("@build_bazel_rules_typescript//:package.bzl", "rules_typescript_dependencies")
|
||||
rules_typescript_dependencies()
|
||||
# Uncomment for local bazel rules development
|
||||
#local_repository(
|
||||
# name = "build_bazel_rules_nodejs",
|
||||
# path = "../rules_nodejs",
|
||||
#)
|
||||
#local_repository(
|
||||
# name = "build_bazel_rules_typescript",
|
||||
# path = "../rules_typescript",
|
||||
#)
|
||||
|
||||
http_archive(
|
||||
name = "bazel_toolchains",
|
||||
urls = [
|
||||
"https://mirror.bazel.build/github.com/bazelbuild/bazel-toolchains/archive/5124557861ebf4c0b67f98180bff1f8551e0b421.tar.gz",
|
||||
"https://github.com/bazelbuild/bazel-toolchains/archive/5124557861ebf4c0b67f98180bff1f8551e0b421.tar.gz",
|
||||
],
|
||||
strip_prefix = "bazel-toolchains-5124557861ebf4c0b67f98180bff1f8551e0b421",
|
||||
sha256 = "c3b08805602cd1d2b67ebe96407c1e8c6ed3d4ce55236ae2efe2f1948f38168d",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "io_bazel_rules_sass",
|
||||
url = "https://github.com/bazelbuild/rules_sass/archive/1.11.0.zip",
|
||||
strip_prefix = "rules_sass-1.11.0",
|
||||
sha256 = "dbe9fb97d5a7833b2a733eebc78c9c1e3880f676ac8af16e58ccf2139cbcad03",
|
||||
)
|
||||
|
||||
# This commit matches the version of buildifier in angular/ngcontainer
|
||||
# If you change this, also check if it matches the version in the angular/ngcontainer
|
||||
# version in /.circleci/config.yml
|
||||
BAZEL_BUILDTOOLS_VERSION = "82b21607e00913b16fe1c51bec80232d9d6de31c"
|
||||
|
||||
http_archive(
|
||||
name = "com_github_bazelbuild_buildtools",
|
||||
url = "https://github.com/bazelbuild/buildtools/archive/%s.zip" % BAZEL_BUILDTOOLS_VERSION,
|
||||
strip_prefix = "buildtools-%s" % BAZEL_BUILDTOOLS_VERSION,
|
||||
sha256 = "edb24c2f9c55b10a820ec74db0564415c0cf553fa55e9fc709a6332fb6685eff",
|
||||
)
|
||||
|
||||
# Fetching the Bazel source code allows us to compile the Skylark linter
|
||||
http_archive(
|
||||
name = "io_bazel",
|
||||
url = "https://github.com/bazelbuild/bazel/archive/968f87900dce45a7af749a965b72dbac51b176b3.zip",
|
||||
strip_prefix = "bazel-968f87900dce45a7af749a965b72dbac51b176b3",
|
||||
sha256 = "e373d2ae24955c1254c495c9c421c009d88966565c35e4e8444c082cb1f0f48f",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "io_bazel_skydoc",
|
||||
# TODO: switch to upstream when https://github.com/bazelbuild/skydoc/pull/103 is merged
|
||||
url = "https://github.com/alexeagle/skydoc/archive/fe2e9f888d28e567fef62ec9d4a93c425526d701.zip",
|
||||
strip_prefix = "skydoc-fe2e9f888d28e567fef62ec9d4a93c425526d701",
|
||||
sha256 = "7bfb5545f59792a2745f2523b9eef363f9c3e7274791c030885e7069f8116016",
|
||||
)
|
||||
|
||||
# We have a source dependency on the Devkit repository, because it's built with
|
||||
# Bazel.
|
||||
# This allows us to edit sources and have the effect appear immediately without
|
||||
# re-packaging or "npm link"ing.
|
||||
# Even better, things like aspects will visit the entire graph including
|
||||
# ts_library rules in the devkit repository.
|
||||
http_archive(
|
||||
name = "angular_cli",
|
||||
url = "https://github.com/angular/angular-cli/archive/v6.1.0-rc.0.zip",
|
||||
strip_prefix = "angular-cli-6.1.0-rc.0",
|
||||
sha256 = "8cf320ea58c321e103f39087376feea502f20eaf79c61a4fdb05c7286c8684fd",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "org_brotli",
|
||||
url = "https://github.com/google/brotli/archive/v1.0.5.zip",
|
||||
strip_prefix = "brotli-1.0.5",
|
||||
sha256 = "774b893a0700b0692a76e2e5b7e7610dbbe330ffbe3fe864b4b52ca718061d5a",
|
||||
)
|
||||
# Angular Bazel users will call this function
|
||||
rules_angular_dependencies()
|
||||
# These are the dependencies only for us
|
||||
rules_angular_dev_dependencies()
|
||||
|
||||
#
|
||||
# Point Bazel to WORKSPACEs that live in subdirectories
|
||||
#
|
||||
|
||||
local_repository(
|
||||
http_archive(
|
||||
name = "rxjs",
|
||||
path = "node_modules/rxjs/src",
|
||||
url = "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.3.tgz",
|
||||
strip_prefix = "package/src",
|
||||
sha256 = "72b0b4e517f43358f554c125e40e39f67688cd2738a8998b4a266981ed32f403",
|
||||
)
|
||||
|
||||
# Point to the integration test workspace just so that Bazel doesn't descend into it
|
||||
@ -103,27 +41,37 @@ local_repository(
|
||||
#
|
||||
# Load and install our dependencies downloaded above.
|
||||
#
|
||||
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories", "yarn_install")
|
||||
|
||||
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories")
|
||||
|
||||
check_bazel_version("0.16.0", """
|
||||
check_bazel_version("0.18.0", """
|
||||
If you are on a Mac and using Homebrew, there is a breaking change to the installation in Bazel 0.16
|
||||
See https://blog.bazel.build/2018/08/22/bazel-homebrew.html
|
||||
|
||||
""")
|
||||
|
||||
node_repositories(
|
||||
node_version = "10.9.0",
|
||||
package_json = ["//:package.json"],
|
||||
preserve_symlinks = True,
|
||||
yarn_version = "1.9.2",
|
||||
)
|
||||
|
||||
yarn_install(
|
||||
name = "npm",
|
||||
package_json = "//tools:npm/package.json",
|
||||
yarn_lock = "//tools:npm/yarn.lock",
|
||||
)
|
||||
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_rules_dependencies", "go_register_toolchains")
|
||||
|
||||
go_rules_dependencies()
|
||||
|
||||
go_register_toolchains()
|
||||
|
||||
load("@io_bazel_rules_webtesting//web:repositories.bzl", "browser_repositories", "web_test_repositories")
|
||||
|
||||
web_test_repositories()
|
||||
|
||||
browser_repositories(
|
||||
chromium = True,
|
||||
firefox = True,
|
||||
@ -141,7 +89,9 @@ ng_setup_workspace()
|
||||
# Skylark documentation generation
|
||||
|
||||
load("@io_bazel_rules_sass//sass:sass_repositories.bzl", "sass_repositories")
|
||||
|
||||
sass_repositories()
|
||||
|
||||
load("@io_bazel_skydoc//skylark:skylark.bzl", "skydoc_repositories")
|
||||
|
||||
skydoc_repositories()
|
||||
|
@ -1,2 +1,2 @@
|
||||
# Periodically clean up builds that do not correspond to currently open PRs
|
||||
0 12 * * * root /usr/local/bin/aio-clean-up >> /var/log/cron.log 2>&1
|
||||
0 12 * * * /usr/local/bin/aio-clean-up >> /var/log/cron.log 2>&1
|
||||
|
@ -36,6 +36,11 @@ server {
|
||||
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
|
||||
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
|
||||
|
||||
error_page 404 /404.html;
|
||||
location "=/404.html" {
|
||||
internal;
|
||||
}
|
||||
|
||||
location "~/[^/]+\.[^/]+$" {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
@ -66,6 +71,21 @@ server {
|
||||
return 200 '';
|
||||
}
|
||||
|
||||
# Check PRs previewability
|
||||
location "~^/can-have-public-preview/\d+/?$" {
|
||||
if ($request_method != "GET") {
|
||||
add_header Allow "GET";
|
||||
return 405;
|
||||
}
|
||||
|
||||
proxy_pass_request_headers on;
|
||||
proxy_redirect off;
|
||||
proxy_method GET;
|
||||
proxy_pass http://{{$AIO_PREVIEW_SERVER_HOSTNAME}}:{{$AIO_PREVIEW_SERVER_PORT}}$request_uri;
|
||||
|
||||
resolver 127.0.0.1;
|
||||
}
|
||||
|
||||
# Notify about CircleCI builds
|
||||
location "~^/circle-build/?$" {
|
||||
if ($request_method != "POST") {
|
||||
|
@ -5,12 +5,12 @@ import * as shell from 'shelljs';
|
||||
import {HIDDEN_DIR_PREFIX} from '../common/constants';
|
||||
import {GithubApi} from '../common/github-api';
|
||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||
import {assertNotMissingOrEmpty, createLogger, getPrInfoFromDownloadPath} from '../common/utils';
|
||||
import {assertNotMissingOrEmpty, getPrInfoFromDownloadPath, Logger} from '../common/utils';
|
||||
|
||||
// Classes
|
||||
export class BuildCleaner {
|
||||
|
||||
private logger = createLogger('BuildCleaner');
|
||||
private logger = new Logger('BuildCleaner');
|
||||
|
||||
// Constructor
|
||||
constructor(protected buildsDir: string, protected githubOrg: string, protected githubRepo: string,
|
||||
@ -122,6 +122,6 @@ export class BuildCleaner {
|
||||
this.logger.log(`Existing downloads: ${existingDownloads.length}`);
|
||||
this.logger.log(`Removing ${toRemove.length} download(s): ${toRemove.join(', ')}`);
|
||||
|
||||
toRemove.forEach(filePath => shell.rm(filePath));
|
||||
toRemove.forEach(filePath => shell.rm(path.join(this.downloadsDir, filePath)));
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ export class CircleCiApi {
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`${baseUrl}: ${response.status} - ${response.statusText}`);
|
||||
}
|
||||
return response.json<BuildInfo>();
|
||||
return response.json();
|
||||
} catch (error) {
|
||||
throw new Error(`CircleCI build info request failed (${error.message})`);
|
||||
}
|
||||
@ -77,7 +77,7 @@ export class CircleCiApi {
|
||||
const baseUrl = `${CIRCLE_CI_API_URL}/${this.githubOrg}/${this.githubRepo}/${buildNumber}`;
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/artifacts?${this.tokenParam}`);
|
||||
const artifacts = await response.json<ArtifactResponse>();
|
||||
const artifacts = await response.json() as ArtifactResponse;
|
||||
const artifact = artifacts.find(item => item.path === artifactPath);
|
||||
if (!artifact) {
|
||||
throw new Error(`Missing artifact (${artifactPath}) for CircleCI build: ${buildNumber}`);
|
||||
|
@ -38,7 +38,8 @@ export class GithubApi {
|
||||
return this.request<T>('post', path, data);
|
||||
}
|
||||
|
||||
public getPaginated<T>(pathname: string, baseParams: RequestParams = {}, currentPage: number = 0): Promise<T[]> {
|
||||
// In GitHub API paginated requests, page numbering is 1-based. (https://developer.github.com/v3/#pagination)
|
||||
public getPaginated<T>(pathname: string, baseParams: RequestParams = {}, currentPage: number = 1): Promise<T[]> {
|
||||
const perPage = 100;
|
||||
const params = {
|
||||
...baseParams,
|
||||
|
@ -74,6 +74,6 @@ export class GithubPullRequests {
|
||||
*/
|
||||
public fetchFiles(pr: number): Promise<FileInfo[]> {
|
||||
assert(pr > 0, `Invalid PR number: ${pr}`);
|
||||
return this.api.get<FileInfo[]>(`/repos/${this.repoSlug}/pulls/${pr}/files`);
|
||||
return this.api.getPaginated<FileInfo>(`/repos/${this.repoSlug}/pulls/${pr}/files`);
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
export const runTests = (specFiles: string[], helpers?: string[]) => {
|
||||
// We can't use `import` here, because of the following mess:
|
||||
// - GitHub project `jasmine/jasmine` is `jasmine-core` on npm and its typings `@types/jasmine`.
|
||||
// - GitHub project `jasmine/jasmine-npm` is `jasmine` on npm and has no typings.
|
||||
//
|
||||
// Using `import...from 'jasmine'` here, would import from `@types/jasmine` (which refers to the
|
||||
// `jasmine-core` module and the `jasmine` module).
|
||||
// tslint:disable-next-line: no-var-requires variable-name
|
||||
const Jasmine = require('jasmine');
|
||||
// We can't use `import...from` here, because of the following mess:
|
||||
// - GitHub project `jasmine/jasmine` is `jasmine-core` on npm and its typings `@types/jasmine`.
|
||||
// - GitHub project `jasmine/jasmine-npm` is `jasmine` on npm and has no typings.
|
||||
//
|
||||
// Using `import...from 'jasmine'` here, would import from `@types/jasmine` (which refers to the
|
||||
// `jasmine-core` module and the `jasmine` module).
|
||||
import Jasmine = require('jasmine');
|
||||
import 'source-map-support/register';
|
||||
|
||||
export const runTests = (specFiles: string[]) => {
|
||||
const config = {
|
||||
helpers,
|
||||
random: true,
|
||||
spec_files: specFiles,
|
||||
stopSpecOnExpectationFailure: true,
|
||||
@ -16,7 +16,7 @@ export const runTests = (specFiles: string[], helpers?: string[]) => {
|
||||
|
||||
process.on('unhandledRejection', (reason: any) => console.log('Unhandled rejection:', reason));
|
||||
|
||||
const runner = new Jasmine();
|
||||
const runner = new Jasmine({});
|
||||
runner.loadConfig(config);
|
||||
runner.onComplete((passed: boolean) => process.exit(passed ? 0 : 1));
|
||||
runner.execute();
|
||||
|
@ -74,12 +74,25 @@ export const getEnvVar = (name: string, isOptional = false): string => {
|
||||
return value || '';
|
||||
};
|
||||
|
||||
export function createLogger(scope: string) {
|
||||
const padding = ' '.repeat(20 - scope.length);
|
||||
return {
|
||||
error: (...args: any[]) => console.error(`[${new Date()}]`, `${scope}:${padding}`, ...args),
|
||||
info: (...args: any[]) => console.info(`[${new Date()}]`, `${scope}:${padding}`, ...args),
|
||||
log: (...args: any[]) => console.log(`[${new Date()}]`, `${scope}:${padding}`, ...args),
|
||||
warn: (...args: any[]) => console.warn(`[${new Date()}]`, `${scope}:${padding}`, ...args),
|
||||
};
|
||||
/**
|
||||
* A basic logger implementation.
|
||||
* Delegates to `console`, but prepends each message with the current date and specified scope (i.e caller).
|
||||
*/
|
||||
export class Logger {
|
||||
private padding = ' '.repeat(20 - this.scope.length);
|
||||
|
||||
/**
|
||||
* Create a new `Logger` instance for the specified `scope`.
|
||||
* @param scope The logger's scope (added to all messages).
|
||||
*/
|
||||
constructor(private scope: string) {}
|
||||
|
||||
public error(...args: any[]) { this.callMethod('error', args); }
|
||||
public info(...args: any[]) { this.callMethod('info', args); }
|
||||
public log(...args: any[]) { this.callMethod('log', args); }
|
||||
public warn(...args: any[]) { this.callMethod('warn', args); }
|
||||
|
||||
private callMethod(method: 'error' | 'info' | 'log' | 'warn', args: any[]) {
|
||||
console[method](`[${new Date()}]`, `${this.scope}:${this.padding}`, ...args);
|
||||
}
|
||||
}
|
||||
|
@ -5,14 +5,14 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as shell from 'shelljs';
|
||||
import {HIDDEN_DIR_PREFIX} from '../common/constants';
|
||||
import {assertNotMissingOrEmpty, computeShortSha, createLogger} from '../common/utils';
|
||||
import {assertNotMissingOrEmpty, computeShortSha, Logger} from '../common/utils';
|
||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
|
||||
import {PreviewServerError} from './preview-error';
|
||||
|
||||
// Classes
|
||||
export class BuildCreator extends EventEmitter {
|
||||
|
||||
private logger = createLogger('BuildCreator');
|
||||
private logger = new Logger('BuildCreator');
|
||||
|
||||
// Constructor
|
||||
constructor(protected buildsDir: string) {
|
||||
|
@ -4,7 +4,7 @@ import {dirname} from 'path';
|
||||
import {mkdir} from 'shelljs';
|
||||
import {promisify} from 'util';
|
||||
import {CircleCiApi} from '../common/circle-ci-api';
|
||||
import {assert, assertNotMissingOrEmpty, computeArtifactDownloadPath, createLogger} from '../common/utils';
|
||||
import {assert, assertNotMissingOrEmpty, computeArtifactDownloadPath, Logger} from '../common/utils';
|
||||
import {PreviewServerError} from './preview-error';
|
||||
|
||||
export interface GithubInfo {
|
||||
@ -19,7 +19,7 @@ export interface GithubInfo {
|
||||
* A helper that can get information about builds and download build artifacts.
|
||||
*/
|
||||
export class BuildRetriever {
|
||||
private logger = createLogger('BuildRetriever');
|
||||
private logger = new Logger('BuildRetriever');
|
||||
constructor(private api: CircleCiApi, private downloadSizeLimit: number, private downloadDir: string) {
|
||||
assert(downloadSizeLimit > 0, 'Invalid parameter "downloadSizeLimit" should be a number greater than 0.');
|
||||
assertNotMissingOrEmpty('downloadDir', downloadDir);
|
||||
@ -34,7 +34,7 @@ export class BuildRetriever {
|
||||
const buildInfo = await this.api.getBuildInfo(buildNum);
|
||||
const githubInfo: GithubInfo = {
|
||||
org: buildInfo.username,
|
||||
pr: getPrfromBranch(buildInfo.branch),
|
||||
pr: getPrFromBranch(buildInfo.branch),
|
||||
repo: buildInfo.reponame,
|
||||
sha: buildInfo.vcs_revision,
|
||||
success: !buildInfo.failed,
|
||||
@ -73,7 +73,7 @@ export class BuildRetriever {
|
||||
}
|
||||
}
|
||||
|
||||
function getPrfromBranch(branch: string): number {
|
||||
function getPrFromBranch(branch: string): number {
|
||||
// CircleCI only exposes PR numbers via the `branch` field :-(
|
||||
const match = /^pull\/(\d+)$/.exec(branch);
|
||||
if (!match) {
|
||||
|
@ -2,11 +2,12 @@
|
||||
import * as bodyParser from 'body-parser';
|
||||
import * as express from 'express';
|
||||
import * as http from 'http';
|
||||
import {AddressInfo} from 'net';
|
||||
import {CircleCiApi} from '../common/circle-ci-api';
|
||||
import {GithubApi} from '../common/github-api';
|
||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||
import {GithubTeams} from '../common/github-teams';
|
||||
import {assert, assertNotMissingOrEmpty, createLogger} from '../common/utils';
|
||||
import {assert, assertNotMissingOrEmpty, Logger} from '../common/utils';
|
||||
import {BuildCreator} from './build-creator';
|
||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
|
||||
import {BuildRetriever} from './build-retriever';
|
||||
@ -31,7 +32,7 @@ export interface PreviewServerConfig {
|
||||
trustedPrLabel: string;
|
||||
}
|
||||
|
||||
const logger = createLogger('PreviewServer');
|
||||
const logger = new Logger('PreviewServer');
|
||||
|
||||
// Classes
|
||||
export class PreviewServerFactory {
|
||||
@ -52,7 +53,7 @@ export class PreviewServerFactory {
|
||||
const httpServer = http.createServer(middleware as any);
|
||||
|
||||
httpServer.on('listening', () => {
|
||||
const info = httpServer.address();
|
||||
const info = httpServer.address() as AddressInfo;
|
||||
logger.info(`Up and running (and listening on ${info.address}:${info.port})...`);
|
||||
});
|
||||
|
||||
@ -63,10 +64,36 @@ export class PreviewServerFactory {
|
||||
buildCreator: BuildCreator, cfg: PreviewServerConfig): express.Express {
|
||||
const middleware = express();
|
||||
const jsonParser = bodyParser.json();
|
||||
const significantFilesRe = new RegExp(cfg.significantFilesPattern);
|
||||
|
||||
// RESPOND TO IS-ALIVE PING
|
||||
middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200));
|
||||
|
||||
// RESPOND TO CAN-HAVE-PUBLIC-PREVIEW CHECK
|
||||
const canHavePublicPreviewRe = /^\/can-have-public-preview\/(\d+)\/?$/;
|
||||
middleware.get(canHavePublicPreviewRe, async (req, res) => {
|
||||
try {
|
||||
const pr = +canHavePublicPreviewRe.exec(req.url)![1];
|
||||
|
||||
if (!await buildVerifier.getSignificantFilesChanged(pr, significantFilesRe)) {
|
||||
// Cannot have preview: PR did not touch relevant files: `aio/` or `packages/` (except for spec files).
|
||||
res.send({canHavePublicPreview: false, reason: 'No significant files touched.'});
|
||||
logger.log(`PR:${pr} - Cannot have a public preview, because it did not touch any significant files.`);
|
||||
} else if (!await buildVerifier.getPrIsTrusted(pr)) {
|
||||
// Cannot have preview: PR not automatically verifiable as "trusted".
|
||||
res.send({canHavePublicPreview: false, reason: 'Not automatically verifiable as "trusted".'});
|
||||
logger.log(`PR:${pr} - Cannot have a public preview, because not automatically verifiable as "trusted".`);
|
||||
} else {
|
||||
// Can have preview.
|
||||
res.send({canHavePublicPreview: true, reason: null});
|
||||
logger.log(`PR:${pr} - Can have a public preview.`);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Previewability check error', err);
|
||||
respondWithError(res, err);
|
||||
}
|
||||
});
|
||||
|
||||
// CIRCLE_CI BUILD COMPLETE WEBHOOK
|
||||
middleware.post(/^\/circle-build\/?$/, jsonParser, async (req, res) => {
|
||||
try {
|
||||
@ -107,7 +134,7 @@ export class PreviewServerFactory {
|
||||
`Invalid webhook: expected "githubRepo" property to equal "${cfg.githubRepo}" but got "${repo}".`);
|
||||
|
||||
// Do not deploy unless this PR has touched relevant files: `aio/` or `packages/` (except for spec files)
|
||||
if (!await buildVerifier.getSignificantFilesChanged(pr, new RegExp(cfg.significantFilesPattern))) {
|
||||
if (!await buildVerifier.getSignificantFilesChanged(pr, significantFilesRe)) {
|
||||
res.sendStatus(204);
|
||||
logger.log(`PR:${pr}, Build:${buildNum} - ` +
|
||||
`Skipping preview processing because this PR did not touch any significant files.`);
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
AIO_NGINX_PORT_HTTPS,
|
||||
AIO_WWW_USER,
|
||||
} from '../common/env-variables';
|
||||
import {computeShortSha, createLogger} from '../common/utils';
|
||||
import {computeShortSha, Logger} from '../common/utils';
|
||||
|
||||
// Interfaces - Types
|
||||
export interface CmdResult { success: boolean; err: Error | null; stdout: string; stderr: string; }
|
||||
@ -31,7 +31,7 @@ class Helper {
|
||||
https: AIO_NGINX_PORT_HTTPS,
|
||||
};
|
||||
|
||||
private logger = createLogger('TestHelper');
|
||||
private logger = new Logger('TestHelper');
|
||||
|
||||
// Constructor
|
||||
constructor() {
|
||||
@ -105,7 +105,7 @@ class Helper {
|
||||
Object.keys(this.portPerScheme).forEach(scheme => suiteFactory(scheme, this.portPerScheme[scheme]));
|
||||
}
|
||||
|
||||
public verifyResponse(status: number | [number, string], regex = /^/): VerifyCmdResultFn {
|
||||
public verifyResponse(status: number | [number, string], regex: string | RegExp = /^/): VerifyCmdResultFn {
|
||||
let statusCode: number;
|
||||
let statusText: string;
|
||||
|
||||
@ -180,26 +180,42 @@ class Helper {
|
||||
}
|
||||
}
|
||||
|
||||
interface DefaultCurlOptions {
|
||||
defaultMethod?: CurlOptions['method'];
|
||||
defaultOptions?: CurlOptions['options'];
|
||||
defaultHeaders?: CurlOptions['headers'];
|
||||
defaultData?: CurlOptions['data'];
|
||||
defaultExtraPath?: CurlOptions['extraPath'];
|
||||
}
|
||||
|
||||
interface CurlOptions {
|
||||
method?: string;
|
||||
options?: string;
|
||||
headers?: string[];
|
||||
data?: any;
|
||||
url?: string;
|
||||
extraPath?: string;
|
||||
}
|
||||
|
||||
export function makeCurl(baseUrl: string) {
|
||||
export function makeCurl(baseUrl: string, {
|
||||
defaultMethod = 'POST',
|
||||
defaultOptions = '',
|
||||
defaultHeaders = ['Content-Type: application/json'],
|
||||
defaultData = {},
|
||||
defaultExtraPath = '',
|
||||
}: DefaultCurlOptions = {}) {
|
||||
return function curl({
|
||||
method = 'POST',
|
||||
options = '',
|
||||
data = {},
|
||||
method = defaultMethod,
|
||||
options = defaultOptions,
|
||||
headers = defaultHeaders,
|
||||
data = defaultData,
|
||||
url = baseUrl,
|
||||
extraPath = '',
|
||||
extraPath = defaultExtraPath,
|
||||
}: CurlOptions) {
|
||||
const dataString = data ? JSON.stringify(data) : '';
|
||||
const cmd = `curl -iLX ${method} ` +
|
||||
`${options} ` +
|
||||
`--header "Content-Type: application/json" ` +
|
||||
headers.map(header => `--header "${header}" `).join('') +
|
||||
`--data '${dataString}' ` +
|
||||
`${url}${extraPath}`;
|
||||
return helper.runCmd(cmd);
|
||||
|
@ -2,7 +2,7 @@
|
||||
import * as nock from 'nock';
|
||||
import * as tar from 'tar-stream';
|
||||
import {gzipSync} from 'zlib';
|
||||
import {createLogger, getEnvVar} from '../common/utils';
|
||||
import {getEnvVar, Logger} from '../common/utils';
|
||||
import {BuildNums, PrNums, SHA} from './constants';
|
||||
|
||||
// We are using the `nock` library to fake responses from REST requests, when testing.
|
||||
@ -14,7 +14,7 @@ import {BuildNums, PrNums, SHA} from './constants';
|
||||
// below and return a suitable response. This is quite complicated to setup since the
|
||||
// response from, say, CircleCI will affect what request is made to, say, Github.
|
||||
|
||||
const logger = createLogger('NOCK');
|
||||
const logger = new Logger('mock-external-apis');
|
||||
|
||||
const log = (...args: any[]) => {
|
||||
// Filter out non-matching URL checks
|
||||
@ -76,7 +76,7 @@ const GITHUB_PULLS_URL = `/repos/${AIO_GITHUB_ORGANIZATION}/${AIO_GITHUB_REPO}/p
|
||||
const GITHUB_TEAMS_URL = `/orgs/${AIO_GITHUB_ORGANIZATION}/teams`;
|
||||
|
||||
const getIssueUrl = (prNum: number) => `${GITHUB_ISSUES_URL}/${prNum}`;
|
||||
const getFilesUrl = (prNum: number) => `${GITHUB_PULLS_URL}/${prNum}/files`;
|
||||
const getFilesUrl = (prNum: number, pageNum = 1) => `${GITHUB_PULLS_URL}/${prNum}/files?page=${pageNum}&per_page=100`;
|
||||
const getCommentUrl = (prNum: number) => `${getIssueUrl(prNum)}/comments`;
|
||||
const getTeamMembershipUrl = (teamId: number, username: string) => `/teams/${teamId}/memberships/${username}`;
|
||||
|
||||
@ -97,7 +97,7 @@ const githubApi = nock(GITHUB_API_HOST).log(log).persist().matchHeader('Authoriz
|
||||
//////////////////////////////
|
||||
|
||||
// GENERAL responses
|
||||
githubApi.get(GITHUB_TEAMS_URL + '?page=0&per_page=100').reply(200, TEST_TEAM_INFO);
|
||||
githubApi.get(GITHUB_TEAMS_URL + '?page=1&per_page=100').reply(200, TEST_TEAM_INFO);
|
||||
githubApi.post(getCommentUrl(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).reply(200);
|
||||
|
||||
// BUILD_INFO errors
|
||||
|
@ -3,6 +3,7 @@ import * as path from 'path';
|
||||
import {rm} from 'shelljs';
|
||||
import {AIO_BUILDS_DIR, AIO_NGINX_HOSTNAME, AIO_NGINX_PORT_HTTP, AIO_NGINX_PORT_HTTPS} from '../common/env-variables';
|
||||
import {computeShortSha} from '../common/utils';
|
||||
import {PrNums} from './constants';
|
||||
import {helper as h} from './helper';
|
||||
import {customMatchers} from './jasmine-custom-matchers';
|
||||
|
||||
@ -252,6 +253,42 @@ describe(`nginx`, () => {
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/can-have-public-preview`, () => {
|
||||
const baseUrl = `${scheme}://${host}/can-have-public-preview`;
|
||||
|
||||
|
||||
it('should disallow non-GET requests', async () => {
|
||||
await Promise.all([
|
||||
h.runCmd(`curl -iLX POST ${baseUrl}/42`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX PUT ${baseUrl}/42`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX PATCH ${baseUrl}/42`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX DELETE ${baseUrl}/42`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
it('should pass requests through to the preview server', async () => {
|
||||
await h.runCmd(`curl -iLX GET ${baseUrl}/${PrNums.CHANGED_FILES_ERROR}`).
|
||||
then(h.verifyResponse(500, /CHANGED_FILES_ERROR/));
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths', async () => {
|
||||
const cmdPrefix = `curl -iLX GET ${baseUrl}`;
|
||||
|
||||
await Promise.all([
|
||||
h.runCmd(`${cmdPrefix}/foo/42`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}-foo/42`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}nfoo/42`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/42/foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/f00`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/`).then(h.verifyResponse(404)),
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/circle-build`, () => {
|
||||
|
||||
it('should disallow non-POST requests', done => {
|
||||
@ -287,6 +324,7 @@ describe(`nginx`, () => {
|
||||
h.runCmd(`${cmdPrefix}/circle-build/42`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
@ -18,6 +18,92 @@ describe('preview-server', () => {
|
||||
afterEach(() => h.cleanUp());
|
||||
|
||||
|
||||
describe(`${host}/can-have-public-preview`, () => {
|
||||
const curl = makeCurl(`${host}/can-have-public-preview`, {
|
||||
defaultData: null,
|
||||
defaultExtraPath: `/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`,
|
||||
defaultHeaders: [],
|
||||
defaultMethod: 'GET',
|
||||
});
|
||||
|
||||
|
||||
it('should disallow non-GET requests', async () => {
|
||||
const bodyRegex = /^Unknown resource in request/;
|
||||
|
||||
await Promise.all([
|
||||
curl({method: 'POST'}).then(h.verifyResponse(404, bodyRegex)),
|
||||
curl({method: 'PUT'}).then(h.verifyResponse(404, bodyRegex)),
|
||||
curl({method: 'PATCH'}).then(h.verifyResponse(404, bodyRegex)),
|
||||
curl({method: 'DELETE'}).then(h.verifyResponse(404, bodyRegex)),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths', async () => {
|
||||
const bodyRegex = /^Unknown resource in request/;
|
||||
|
||||
await Promise.all([
|
||||
curl({extraPath: `/foo/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(404, bodyRegex)),
|
||||
curl({extraPath: `-foo/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(404, bodyRegex)),
|
||||
curl({extraPath: `nfoo/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(404, bodyRegex)),
|
||||
curl({extraPath: `/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}/foo`}).then(h.verifyResponse(404, bodyRegex)),
|
||||
curl({extraPath: '/f00'}).then(h.verifyResponse(404, bodyRegex)),
|
||||
curl({extraPath: '/'}).then(h.verifyResponse(404, bodyRegex)),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 500 if checking for significant file changes fails', async () => {
|
||||
await Promise.all([
|
||||
curl({extraPath: `/${PrNums.CHANGED_FILES_404}`}).then(h.verifyResponse(500, /CHANGED_FILES_404/)),
|
||||
curl({extraPath: `/${PrNums.CHANGED_FILES_ERROR}`}).then(h.verifyResponse(500, /CHANGED_FILES_ERROR/)),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 200 (false) if no significant files were touched', async () => {
|
||||
const expectedResponse = JSON.stringify({
|
||||
canHavePublicPreview: false,
|
||||
reason: 'No significant files touched.',
|
||||
});
|
||||
|
||||
await curl({extraPath: `/${PrNums.CHANGED_FILES_NONE}`}).then(h.verifyResponse(200, expectedResponse));
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 500 if checking "trusted" status fails', async () => {
|
||||
await curl({extraPath: `/${PrNums.TRUST_CHECK_ERROR}`}).then(h.verifyResponse(500, 'TRUST_CHECK_ERROR'));
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 200 (false) if the PR is not automatically verifiable as "trusted"', async () => {
|
||||
const expectedResponse = JSON.stringify({
|
||||
canHavePublicPreview: false,
|
||||
reason: 'Not automatically verifiable as \\"trusted\\".',
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
curl({extraPath: `/${PrNums.TRUST_CHECK_INACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(200, expectedResponse)),
|
||||
curl({extraPath: `/${PrNums.TRUST_CHECK_UNTRUSTED}`}).then(h.verifyResponse(200, expectedResponse)),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 200 (true) if the PR can have a public preview', async () => {
|
||||
const expectedResponse = JSON.stringify({
|
||||
canHavePublicPreview: true,
|
||||
reason: null,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
curl({extraPath: `/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(200, expectedResponse)),
|
||||
curl({extraPath: `/${PrNums.TRUST_CHECK_TRUSTED_LABEL}`}).then(h.verifyResponse(200, expectedResponse)),
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/circle-build`, () => {
|
||||
|
||||
const curl = makeCurl(`${host}/circle-build`);
|
||||
|
@ -7,43 +7,49 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"prebuild": "yarn clean-dist",
|
||||
"build": "tsc",
|
||||
"build-watch": "yarn build --watch",
|
||||
"build": "yarn ~~build",
|
||||
"prebuild-watch": "yarn prebuild",
|
||||
"build-watch": "yarn ~~build-watch",
|
||||
"clean-dist": "node --eval \"require('shelljs').rm('-rf', 'dist')\"",
|
||||
"dev": "concurrently --kill-others --raw --success first \"yarn build-watch\" \"yarn test-watch\"",
|
||||
"predev": "yarn build || true",
|
||||
"dev": "run-p ~~build-watch ~~test-watch",
|
||||
"lint": "tslint --project tsconfig.json",
|
||||
"pre~~test-only": "yarn lint",
|
||||
"~~test-only": "node dist/test",
|
||||
"pretest": "yarn build",
|
||||
"test": "yarn ~~test-only",
|
||||
"pretest-watch": "yarn build",
|
||||
"test-watch": "nodemon --exec \"yarn ~~test-only\" --watch dist"
|
||||
"pretest-watch": "yarn pretest",
|
||||
"test-watch": "yarn ~~test-watch",
|
||||
"~~build": "tsc",
|
||||
"~~build-watch": "yarn ~~build --watch",
|
||||
"pre~~test-only": "yarn lint",
|
||||
"~~test-only": "node dist/test",
|
||||
"~~test-watch": "nodemon --delay 1 --exec \"yarn ~~test-only\" --watch dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"body-parser": "^1.18.2",
|
||||
"body-parser": "^1.18.3",
|
||||
"delete-empty": "^2.0.0",
|
||||
"express": "^4.15.4",
|
||||
"jasmine": "^2.8.0",
|
||||
"nock": "^9.2.5",
|
||||
"node-fetch": "^2.1.2",
|
||||
"shelljs": "^0.8.1",
|
||||
"tar-stream": "^1.6.0",
|
||||
"tslib": "^1.7.1"
|
||||
"express": "^4.16.3",
|
||||
"jasmine": "^3.2.0",
|
||||
"nock": "^9.6.1",
|
||||
"node-fetch": "^2.2.0",
|
||||
"shelljs": "^0.8.2",
|
||||
"source-map-support": "^0.5.9",
|
||||
"tar-stream": "^1.6.1",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/body-parser": "^1.16.5",
|
||||
"@types/express": "^4.0.37",
|
||||
"@types/jasmine": "^2.6.0",
|
||||
"@types/nock": "^9.1.3",
|
||||
"@types/node": "^8.0.30",
|
||||
"@types/node-fetch": "^1.6.8",
|
||||
"@types/body-parser": "^1.17.0",
|
||||
"@types/express": "^4.16.0",
|
||||
"@types/jasmine": "^2.8.8",
|
||||
"@types/nock": "^9.3.0",
|
||||
"@types/node": "^10.9.2",
|
||||
"@types/node-fetch": "^2.1.2",
|
||||
"@types/shelljs": "^0.8.0",
|
||||
"@types/supertest": "^2.0.3",
|
||||
"concurrently": "^3.5.0",
|
||||
"nodemon": "^1.12.1",
|
||||
"supertest": "^3.0.0",
|
||||
"tslint": "^5.7.0",
|
||||
"tslint-jasmine-noSkipOrFocus": "^1.0.8",
|
||||
"typescript": "^2.5.2"
|
||||
"@types/supertest": "^2.0.5",
|
||||
"nodemon": "^1.18.3",
|
||||
"npm-run-all": "^4.1.3",
|
||||
"supertest": "^3.1.0",
|
||||
"tslint": "^5.11.0",
|
||||
"tslint-jasmine-noSkipOrFocus": "^1.0.9",
|
||||
"typescript": "^3.0.1"
|
||||
}
|
||||
}
|
||||
|
@ -5,25 +5,28 @@ import * as shell from 'shelljs';
|
||||
import {BuildCleaner} from '../../lib/clean-up/build-cleaner';
|
||||
import {HIDDEN_DIR_PREFIX} from '../../lib/common/constants';
|
||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||
import {Logger} from '../../lib/common/utils';
|
||||
|
||||
const EXISTING_BUILDS = [10, 20, 30, 40];
|
||||
const EXISTING_DOWNLOADS = [
|
||||
'downloads/10-ABCDEF0-build.zip',
|
||||
'downloads/10-1234567-build.zip',
|
||||
'downloads/20-ABCDEF0-build.zip',
|
||||
'downloads/20-1234567-build.zip',
|
||||
'10-ABCDEF0-build.zip',
|
||||
'10-1234567-build.zip',
|
||||
'20-ABCDEF0-build.zip',
|
||||
'20-1234567-build.zip',
|
||||
];
|
||||
const OPEN_PRS = [10, 40];
|
||||
const ANY_DATE = jasmine.any(String);
|
||||
|
||||
// Tests
|
||||
describe('BuildCleaner', () => {
|
||||
let loggerErrorSpy: jasmine.Spy;
|
||||
let loggerLogSpy: jasmine.Spy;
|
||||
let cleaner: BuildCleaner;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(console, 'error');
|
||||
spyOn(console, 'log');
|
||||
cleaner = new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', 'downloads', 'build.zip');
|
||||
loggerErrorSpy = spyOn(Logger.prototype, 'error');
|
||||
loggerLogSpy = spyOn(Logger.prototype, 'log');
|
||||
cleaner = new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', '/downloads', 'build.zip');
|
||||
});
|
||||
|
||||
describe('constructor()', () => {
|
||||
@ -51,11 +54,13 @@ describe('BuildCleaner', () => {
|
||||
toThrowError('Missing or empty required parameter \'githubToken\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'downloadsDir\' is empty', () => {
|
||||
expect(() => new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', '', 'build.zip')).
|
||||
toThrowError('Missing or empty required parameter \'downloadsDir\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'artifactPath\' is empty', () => {
|
||||
expect(() => new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', 'downloads', '')).
|
||||
toThrowError('Missing or empty required parameter \'artifactPath\'!');
|
||||
@ -85,9 +90,12 @@ describe('BuildCleaner', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
it('should return a promise', async () => {
|
||||
const promise = cleaner.cleanUp();
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
|
||||
// Do not complete the test and release the spies synchronously, to avoid running the actual implementations.
|
||||
await promise;
|
||||
});
|
||||
|
||||
|
||||
@ -160,6 +168,7 @@ describe('BuildCleaner', () => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
it('should reject if \'removeUnnecessaryDownloads()\' rejects', async () => {
|
||||
try {
|
||||
cleanerRemoveUnnecessaryDownloadsSpy.and.callFake(() => Promise.reject('Test'));
|
||||
@ -168,6 +177,7 @@ describe('BuildCleaner', () => {
|
||||
expect(err).toBe('Test');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@ -277,11 +287,14 @@ describe('BuildCleaner', () => {
|
||||
prDeferred.resolve([{id: 0, number: 1}, {id: 1, number: 2}, {id: 2, number: 3}]);
|
||||
});
|
||||
|
||||
|
||||
it('should log the number of open PRs', () => {
|
||||
promise.then(prNumbers => {
|
||||
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ', `Open pull requests: ${prNumbers}`);
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith(
|
||||
ANY_DATE, 'BuildCleaner: ', `Open pull requests: ${prNumbers}`);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@ -301,9 +314,9 @@ describe('BuildCleaner', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should get the contents of the builds directory', () => {
|
||||
it('should get the contents of the downloads directory', () => {
|
||||
expect(fsReaddirSpy).toHaveBeenCalled();
|
||||
expect(fsReaddirSpy.calls.argsFor(0)[0]).toBe('downloads');
|
||||
expect(fsReaddirSpy.calls.argsFor(0)[0]).toBe('/downloads');
|
||||
});
|
||||
|
||||
|
||||
@ -317,7 +330,7 @@ describe('BuildCleaner', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the returned files (as numbers)', done => {
|
||||
it('should resolve with the returned file names', done => {
|
||||
promise.then(result => {
|
||||
expect(result).toEqual(EXISTING_DOWNLOADS);
|
||||
done();
|
||||
@ -383,8 +396,7 @@ describe('BuildCleaner', () => {
|
||||
|
||||
cleaner.removeDir('/foo/bar');
|
||||
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
jasmine.any(String), 'BuildCleaner: ', 'ERROR: Unable to remove \'/foo/bar\' due to:', 'Test');
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('ERROR: Unable to remove \'/foo/bar\' due to:', 'Test');
|
||||
});
|
||||
|
||||
});
|
||||
@ -401,8 +413,8 @@ describe('BuildCleaner', () => {
|
||||
it('should log the number of existing builds and builds to be removed', () => {
|
||||
cleaner.removeUnnecessaryBuilds([1, 2, 3], [3, 4, 5, 6]);
|
||||
|
||||
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ', 'Existing builds: 3');
|
||||
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ', 'Removing 2 build(s): 1, 2');
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith('Existing builds: 3');
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith('Removing 2 build(s): 1, 2');
|
||||
});
|
||||
|
||||
|
||||
@ -454,25 +466,36 @@ describe('BuildCleaner', () => {
|
||||
|
||||
|
||||
describe('removeUnnecessaryDownloads()', () => {
|
||||
let shellRmSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(shell, 'rm');
|
||||
shellRmSpy = spyOn(shell, 'rm');
|
||||
});
|
||||
|
||||
|
||||
it('should log the number of existing downloads and downloads to be removed', () => {
|
||||
cleaner.removeUnnecessaryDownloads(EXISTING_DOWNLOADS, OPEN_PRS);
|
||||
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith('Existing downloads: 4');
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith('Removing 2 download(s): 20-ABCDEF0-build.zip, 20-1234567-build.zip');
|
||||
});
|
||||
|
||||
|
||||
it('should construct full paths to directories (by prepending \'downloadsDir\')', () => {
|
||||
cleaner.removeUnnecessaryDownloads(['dl-1', 'dl-2', 'dl-3'], []);
|
||||
|
||||
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/dl-1'));
|
||||
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/dl-2'));
|
||||
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/dl-3'));
|
||||
});
|
||||
|
||||
|
||||
it('should remove the downloads that do not correspond to open PRs', () => {
|
||||
cleaner.removeUnnecessaryDownloads(EXISTING_DOWNLOADS, OPEN_PRS);
|
||||
expect(shell.rm).toHaveBeenCalledTimes(2);
|
||||
expect(shell.rm).toHaveBeenCalledWith('downloads/20-ABCDEF0-build.zip');
|
||||
expect(shell.rm).toHaveBeenCalledWith('downloads/20-1234567-build.zip');
|
||||
expect(shellRmSpy).toHaveBeenCalledTimes(2);
|
||||
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/20-ABCDEF0-build.zip'));
|
||||
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/20-1234567-build.zip'));
|
||||
});
|
||||
|
||||
|
||||
it('should log the number of existing builds and builds to be removed', () => {
|
||||
cleaner.removeUnnecessaryDownloads(EXISTING_DOWNLOADS, OPEN_PRS);
|
||||
|
||||
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ', 'Existing downloads: 4');
|
||||
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ',
|
||||
'Removing 2 download(s): downloads/20-ABCDEF0-build.zip, downloads/20-1234567-build.zip');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -126,8 +126,8 @@ describe('GithubApi', () => {
|
||||
(api as any).getPaginated('/foo/bar');
|
||||
(api as any).getPaginated('/foo/bar', {baz: 'qux'});
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith('/foo/bar', {page: 0, per_page: 100});
|
||||
expect(api.get).toHaveBeenCalledWith('/foo/bar', {baz: 'qux', page: 0, per_page: 100});
|
||||
expect(api.get).toHaveBeenCalledWith('/foo/bar', {page: 1, per_page: 100});
|
||||
expect(api.get).toHaveBeenCalledWith('/foo/bar', {baz: 'qux', page: 1, per_page: 100});
|
||||
});
|
||||
|
||||
|
||||
@ -162,9 +162,9 @@ describe('GithubApi', () => {
|
||||
const paramsForPage = (page: number) => ({baz: 'qux', page, per_page: 100});
|
||||
|
||||
expect(apiGetSpy).toHaveBeenCalledTimes(3);
|
||||
expect(apiGetSpy.calls.argsFor(0)).toEqual(['/foo/bar', paramsForPage(0)]);
|
||||
expect(apiGetSpy.calls.argsFor(1)).toEqual(['/foo/bar', paramsForPage(1)]);
|
||||
expect(apiGetSpy.calls.argsFor(2)).toEqual(['/foo/bar', paramsForPage(2)]);
|
||||
expect(apiGetSpy.calls.argsFor(0)).toEqual(['/foo/bar', paramsForPage(1)]);
|
||||
expect(apiGetSpy.calls.argsFor(1)).toEqual(['/foo/bar', paramsForPage(2)]);
|
||||
expect(apiGetSpy.calls.argsFor(2)).toEqual(['/foo/bar', paramsForPage(3)]);
|
||||
|
||||
expect(data).toEqual(allItems);
|
||||
|
||||
|
@ -4,13 +4,13 @@ import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||
|
||||
// Tests
|
||||
describe('GithubPullRequests', () => {
|
||||
|
||||
let githubApi: jasmine.SpyObj<GithubApi>;
|
||||
|
||||
beforeEach(() => {
|
||||
githubApi = jasmine.createSpyObj('githubApi', ['post', 'get', 'getPaginated']);
|
||||
});
|
||||
|
||||
|
||||
describe('constructor()', () => {
|
||||
|
||||
it('should throw if \'githubOrg\' is missing or empty', () => {
|
||||
@ -95,16 +95,14 @@ describe('GithubPullRequests', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('fetchAll()', () => {
|
||||
let prs: GithubPullRequests;
|
||||
|
||||
beforeEach(() => {
|
||||
prs = new GithubPullRequests(githubApi, 'foo', 'bar');
|
||||
spyOn(console, 'log');
|
||||
});
|
||||
beforeEach(() => prs = new GithubPullRequests(githubApi, 'foo', 'bar'));
|
||||
|
||||
|
||||
it('should call \'getPaginated()\' with the correct pathname and params', () => {
|
||||
@ -131,8 +129,10 @@ describe('GithubPullRequests', () => {
|
||||
githubApi.getPaginated.and.returnValue('Test');
|
||||
expect(prs.fetchAll() as any).toBe('Test');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('fetchFiles()', () => {
|
||||
let prs: GithubPullRequests;
|
||||
|
||||
@ -141,21 +141,21 @@ describe('GithubPullRequests', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should make a GET request to GitHub with the correct pathname', () => {
|
||||
it('should make a paginated GET request to GitHub with the correct pathname', () => {
|
||||
prs.fetchFiles(42);
|
||||
expect(githubApi.get).toHaveBeenCalledWith('/repos/foo/bar/pulls/42/files');
|
||||
expect(githubApi.getPaginated).toHaveBeenCalledWith('/repos/foo/bar/pulls/42/files');
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the data returned from GitHub', done => {
|
||||
const expected: any = [{ sha: 'ABCDE', filename: 'a/b/c'}, { sha: '12345', filename: 'x/y/z' }];
|
||||
githubApi.get.and.callFake(() => Promise.resolve(expected));
|
||||
prs.fetch(42).then(data => {
|
||||
const expected: any = [{sha: 'ABCDE', filename: 'a/b/c'}, {sha: '12345', filename: 'x/y/z'}];
|
||||
githubApi.getPaginated.and.callFake(() => Promise.resolve(expected));
|
||||
prs.fetchFiles(42).then(data => {
|
||||
expect(data).toEqual(expected);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
// Imports
|
||||
import {resolve as resolvePath} from 'path';
|
||||
import {
|
||||
assert,
|
||||
assertNotMissingOrEmpty,
|
||||
@ -6,6 +7,7 @@ import {
|
||||
computeShortSha,
|
||||
getEnvVar,
|
||||
getPrInfoFromDownloadPath,
|
||||
Logger,
|
||||
} from '../../lib/common/utils';
|
||||
|
||||
// Tests
|
||||
@ -19,6 +21,7 @@ describe('utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('assert', () => {
|
||||
it('should throw if passed a false value', () => {
|
||||
expect(() => assert(false, 'error message')).toThrowError('error message');
|
||||
@ -29,6 +32,7 @@ describe('utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('computeArtifactDownloadPath', () => {
|
||||
it('should compute an absolute path based on the artifact info provided', () => {
|
||||
const downloadDir = '/a/b/c';
|
||||
@ -36,10 +40,11 @@ describe('utils', () => {
|
||||
const sha = 'ABCDEF1234567';
|
||||
const artifactPath = 'a/path/to/file.zip';
|
||||
const path = computeArtifactDownloadPath(downloadDir, pr, sha, artifactPath);
|
||||
expect(path).toEqual('/a/b/c/123-ABCDEF1-file.zip');
|
||||
expect(path).toBe(resolvePath('/a/b/c/123-ABCDEF1-file.zip'));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('getPrInfoFromDownloadPath', () => {
|
||||
it('should extract the PR and SHA from the file path', () => {
|
||||
const {pr, sha} = getPrInfoFromDownloadPath('a/b/c/12345-ABCDE-artifact.zip');
|
||||
@ -48,6 +53,7 @@ describe('utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('assertNotMissingOrEmpty()', () => {
|
||||
|
||||
it('should throw if passed an empty value', () => {
|
||||
@ -122,4 +128,79 @@ describe('utils', () => {
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('Logger', () => {
|
||||
let consoleErrorSpy: jasmine.Spy;
|
||||
let consoleInfoSpy: jasmine.Spy;
|
||||
let consoleLogSpy: jasmine.Spy;
|
||||
let consoleWarnSpy: jasmine.Spy;
|
||||
let logger: Logger;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleErrorSpy = spyOn(console, 'error');
|
||||
consoleInfoSpy = spyOn(console, 'info');
|
||||
consoleLogSpy = spyOn(console, 'log');
|
||||
consoleWarnSpy = spyOn(console, 'warn');
|
||||
|
||||
logger = new Logger('TestScope');
|
||||
});
|
||||
|
||||
|
||||
it('should delegate to `console`', () => {
|
||||
logger.error('foo');
|
||||
expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
|
||||
expect(consoleErrorSpy.calls.argsFor(0)).toContain('foo');
|
||||
|
||||
logger.info('bar');
|
||||
expect(consoleInfoSpy).toHaveBeenCalledTimes(1);
|
||||
expect(consoleInfoSpy.calls.argsFor(0)).toContain('bar');
|
||||
|
||||
logger.log('baz');
|
||||
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
|
||||
expect(consoleLogSpy.calls.argsFor(0)).toContain('baz');
|
||||
|
||||
logger.warn('qux');
|
||||
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||
expect(consoleWarnSpy.calls.argsFor(0)).toContain('qux');
|
||||
});
|
||||
|
||||
|
||||
it('should prepend messages with the current date and logger\'s scope', () => {
|
||||
const mockDate = new Date(1337);
|
||||
const expectedDateStr = `[${mockDate}]`;
|
||||
const expectedScopeStr = 'TestScope: ';
|
||||
|
||||
jasmine.clock().mockDate(mockDate);
|
||||
jasmine.clock().withMock(() => {
|
||||
logger.error();
|
||||
logger.info();
|
||||
logger.log();
|
||||
logger.warn();
|
||||
});
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
|
||||
expect(consoleInfoSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
|
||||
});
|
||||
|
||||
|
||||
it('should pass all arguments to `console`', () => {
|
||||
const someString = jasmine.any(String);
|
||||
|
||||
logger.error('foo1', 'foo2');
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(someString, someString, 'foo1', 'foo2');
|
||||
|
||||
logger.info('bar1', 'bar2');
|
||||
expect(consoleInfoSpy).toHaveBeenCalledWith(someString, someString, 'bar1', 'bar2');
|
||||
|
||||
logger.log('baz1', 'baz2');
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(someString, someString, 'baz1', 'baz2');
|
||||
|
||||
logger.warn('qux1', 'qux2');
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(someString, someString, 'qux1', 'qux2');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -1,6 +0,0 @@
|
||||
declare namespace jasmine {
|
||||
export interface DoneFn extends Function {
|
||||
(): void;
|
||||
fail: (message: Error | string) => void;
|
||||
}
|
||||
}
|
@ -3,5 +3,4 @@ import {runTests} from '../lib/common/run-tests';
|
||||
|
||||
// Run
|
||||
const specFiles = [`${__dirname}/**/*.spec.js`];
|
||||
const helpers = [`${__dirname}/helpers.js`];
|
||||
runTests(specFiles, helpers);
|
||||
runTests(specFiles);
|
||||
|
@ -5,6 +5,7 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as shell from 'shelljs';
|
||||
import {SHORT_SHA_LEN} from '../../lib/common/constants';
|
||||
import {Logger} from '../../lib/common/utils';
|
||||
import {BuildCreator} from '../../lib/preview-server/build-creator';
|
||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events';
|
||||
import {PreviewServerError} from '../../lib/preview-server/preview-error';
|
||||
@ -491,7 +492,7 @@ describe('BuildCreator', () => {
|
||||
beforeEach(() => {
|
||||
cpExecCbs = [];
|
||||
|
||||
consoleWarnSpy = spyOn(console, 'warn');
|
||||
consoleWarnSpy = spyOn(Logger.prototype, 'warn');
|
||||
shellChmodSpy = spyOn(shell, 'chmod');
|
||||
shellRmSpy = spyOn(shell, 'rm');
|
||||
cpExecSpy = spyOn(cp, 'exec').and.callFake((_: string, cb: (...args: any[]) => void) => cpExecCbs.push(cb));
|
||||
@ -513,8 +514,7 @@ describe('BuildCreator', () => {
|
||||
|
||||
it('should log (as a warning) any stderr output if extracting succeeded', done => {
|
||||
(bc as any).extractArchive('foo', 'bar').
|
||||
then(() => expect(consoleWarnSpy)
|
||||
.toHaveBeenCalledWith(jasmine.any(String), 'BuildCreator: ', 'This is stderr')).
|
||||
then(() => expect(consoleWarnSpy).toHaveBeenCalledWith('This is stderr')).
|
||||
then(done);
|
||||
|
||||
cpExecCbs[0](null, 'This is stdout', 'This is stderr');
|
||||
|
@ -1,11 +1,13 @@
|
||||
import * as fs from 'fs';
|
||||
import * as nock from 'nock';
|
||||
import {resolve as resolvePath} from 'path';
|
||||
import {BuildInfo, CircleCiApi} from '../../lib/common/circle-ci-api';
|
||||
import {Logger} from '../../lib/common/utils';
|
||||
import {BuildRetriever} from '../../lib/preview-server/build-retriever';
|
||||
|
||||
describe('BuildRetriever', () => {
|
||||
const MAX_DOWNLOAD_SIZE = 10000;
|
||||
const DOWNLOAD_DIR = '/DOWNLOAD/DIR';
|
||||
const DOWNLOAD_DIR = resolvePath('/DOWNLOAD/DIR');
|
||||
const BASE_URL = 'http://test.com';
|
||||
const ARTIFACT_PATH = '/some/path/build.zip';
|
||||
|
||||
@ -29,10 +31,6 @@ describe('BuildRetriever', () => {
|
||||
vcs_revision: 'COMMIT',
|
||||
};
|
||||
|
||||
spyOn(console, 'log');
|
||||
spyOn(console, 'warn');
|
||||
spyOn(console, 'error');
|
||||
|
||||
api = new CircleCiApi('ORG', 'REPO', 'TOKEN');
|
||||
spyOn(api, 'getBuildInfo').and.callFake(() => Promise.resolve(BUILD_INFO));
|
||||
getBuildArtifactUrlSpy = spyOn(api, 'getBuildArtifactUrl')
|
||||
@ -91,6 +89,7 @@ describe('BuildRetriever', () => {
|
||||
let retriever: BuildRetriever;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(Logger.prototype, 'warn');
|
||||
retriever = new BuildRetriever(api, MAX_DOWNLOAD_SIZE, DOWNLOAD_DIR);
|
||||
});
|
||||
|
||||
@ -133,11 +132,14 @@ describe('BuildRetriever', () => {
|
||||
|
||||
it('should write the artifact file to disk', async () => {
|
||||
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(200, ARTIFACT_CONTENTS);
|
||||
const downloadPath = resolvePath(`${DOWNLOAD_DIR}/777-COMMIT-build.zip`);
|
||||
|
||||
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
|
||||
expect(writeFileSpy)
|
||||
.toHaveBeenCalledWith(`${DOWNLOAD_DIR}/777-COMMIT-build.zip`, jasmine.any(Buffer), jasmine.any(Function));
|
||||
expect(writeFileSpy).toHaveBeenCalledWith(downloadPath, jasmine.any(Buffer), jasmine.any(Function));
|
||||
|
||||
const buffer: Buffer = writeFileSpy.calls.mostRecent().args[1];
|
||||
expect(buffer.toString()).toEqual(ARTIFACT_CONTENTS);
|
||||
|
||||
artifactRequest.done();
|
||||
});
|
||||
|
||||
|
@ -2,11 +2,11 @@
|
||||
import * as express from 'express';
|
||||
import * as http from 'http';
|
||||
import * as supertest from 'supertest';
|
||||
import {promisify} from 'util';
|
||||
import {CircleCiApi} from '../../lib/common/circle-ci-api';
|
||||
import {GithubApi} from '../../lib/common/github-api';
|
||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||
import {GithubTeams} from '../../lib/common/github-teams';
|
||||
import {Logger} from '../../lib/common/utils';
|
||||
import {BuildCreator} from '../../lib/preview-server/build-creator';
|
||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events';
|
||||
import {BuildRetriever, GithubInfo} from '../../lib/preview-server/build-retriever';
|
||||
@ -38,15 +38,18 @@ describe('PreviewServerFactory', () => {
|
||||
significantFilesPattern: '^(?:aio|packages)\\/(?!.*[._]spec\\.[jt]s$)',
|
||||
trustedPrLabel: 'trusted: pr-label',
|
||||
};
|
||||
let loggerErrorSpy: jasmine.Spy;
|
||||
let loggerInfoSpy: jasmine.Spy;
|
||||
let loggerLogSpy: jasmine.Spy;
|
||||
|
||||
// Helpers
|
||||
const createPreviewServer = (partialConfig: Partial<PreviewServerConfig> = {}) =>
|
||||
PreviewServerFactory.create({...defaultConfig, ...partialConfig});
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(console, 'error');
|
||||
spyOn(console, 'info');
|
||||
spyOn(console, 'log');
|
||||
loggerErrorSpy = spyOn(Logger.prototype, 'error');
|
||||
loggerInfoSpy = spyOn(Logger.prototype, 'info');
|
||||
loggerLogSpy = spyOn(Logger.prototype, 'log');
|
||||
});
|
||||
|
||||
describe('create()', () => {
|
||||
@ -140,11 +143,10 @@ describe('PreviewServerFactory', () => {
|
||||
const server = createPreviewServer();
|
||||
server.address = () => ({address: 'foo', family: '', port: 1337});
|
||||
|
||||
expect(console.info).not.toHaveBeenCalled();
|
||||
expect(loggerInfoSpy).not.toHaveBeenCalled();
|
||||
|
||||
server.emit('listening');
|
||||
expect(console.info).toHaveBeenCalledWith(
|
||||
jasmine.any(String), 'PreviewServer: ', 'Up and running (and listening on foo:1337)...');
|
||||
expect(loggerInfoSpy).toHaveBeenCalledWith('Up and running (and listening on foo:1337)...');
|
||||
});
|
||||
|
||||
});
|
||||
@ -241,10 +243,6 @@ describe('PreviewServerFactory', () => {
|
||||
let buildCreator: BuildCreator;
|
||||
let agent: supertest.SuperTest<supertest.Test>;
|
||||
|
||||
// Helpers
|
||||
const promisifyRequest = async (req: supertest.Request) => await promisify(req.end.bind(req))();
|
||||
const verifyRequests = async (reqs: supertest.Request[]) => await Promise.all(reqs.map(promisifyRequest));
|
||||
|
||||
beforeEach(() => {
|
||||
const circleCiApi = new CircleCiApi(defaultConfig.githubOrg, defaultConfig.githubRepo,
|
||||
defaultConfig.circleCiToken);
|
||||
@ -261,10 +259,11 @@ describe('PreviewServerFactory', () => {
|
||||
agent = supertest.agent(middleware);
|
||||
});
|
||||
|
||||
|
||||
describe('GET /health-check', () => {
|
||||
|
||||
it('should respond with 200', async () => {
|
||||
await verifyRequests([
|
||||
await Promise.all([
|
||||
agent.get('/health-check').expect(200),
|
||||
agent.get('/health-check/').expect(200),
|
||||
]);
|
||||
@ -272,7 +271,7 @@ describe('PreviewServerFactory', () => {
|
||||
|
||||
|
||||
it('should respond with 404 for non-GET requests', async () => {
|
||||
await verifyRequests([
|
||||
await Promise.all([
|
||||
agent.put('/health-check').expect(404),
|
||||
agent.post('/health-check').expect(404),
|
||||
agent.patch('/health-check').expect(404),
|
||||
@ -282,7 +281,7 @@ describe('PreviewServerFactory', () => {
|
||||
|
||||
|
||||
it('should respond with 404 if the path does not match exactly', async () => {
|
||||
await verifyRequests([
|
||||
await Promise.all([
|
||||
agent.get('/health-check/foo').expect(404),
|
||||
agent.get('/health-check-foo').expect(404),
|
||||
agent.get('/health-checknfoo').expect(404),
|
||||
@ -294,7 +293,104 @@ describe('PreviewServerFactory', () => {
|
||||
|
||||
});
|
||||
|
||||
describe('/circle-build', () => {
|
||||
|
||||
describe('GET /can-have-public-preview/<pr>', () => {
|
||||
const baseUrl = '/can-have-public-preview';
|
||||
const pr = 777;
|
||||
const url = `${baseUrl}/${pr}`;
|
||||
let bvGetPrIsTrustedSpy: jasmine.Spy;
|
||||
let bvGetSignificantFilesChangedSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
bvGetPrIsTrustedSpy = spyOn(buildVerifier, 'getPrIsTrusted').and.returnValue(Promise.resolve(true));
|
||||
bvGetSignificantFilesChangedSpy = spyOn(buildVerifier, 'getSignificantFilesChanged').
|
||||
and.returnValue(Promise.resolve(true));
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for non-GET requests', async () => {
|
||||
await Promise.all([
|
||||
agent.put(url).expect(404),
|
||||
agent.post(url).expect(404),
|
||||
agent.patch(url).expect(404),
|
||||
agent.delete(url).expect(404),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 if the path does not match exactly', async () => {
|
||||
await Promise.all([
|
||||
agent.get('/can-have-public-preview/42/foo').expect(404),
|
||||
agent.get('/can-have-public-preview-foo/42').expect(404),
|
||||
agent.get('/can-have-public-previewnfoo/42').expect(404),
|
||||
agent.get('/foo/can-have-public-preview/42').expect(404),
|
||||
agent.get('/foo-can-have-public-preview/42').expect(404),
|
||||
agent.get('/fooncan-have-public-preview/42').expect(404),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
it('should respond appropriately if the PR did not touch any significant files', async () => {
|
||||
bvGetSignificantFilesChangedSpy.and.returnValue(Promise.resolve(false));
|
||||
|
||||
const expectedResponse = {canHavePublicPreview: false, reason: 'No significant files touched.'};
|
||||
const expectedLog = `PR:${pr} - Cannot have a public preview, because it did not touch any significant files.`;
|
||||
|
||||
await agent.get(url).expect(200, expectedResponse);
|
||||
|
||||
expect(bvGetSignificantFilesChangedSpy).toHaveBeenCalledWith(pr, jasmine.any(RegExp));
|
||||
expect(bvGetPrIsTrustedSpy).not.toHaveBeenCalled();
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith(expectedLog);
|
||||
});
|
||||
|
||||
|
||||
it('should respond appropriately if the PR is not automatically verifiable as "trusted"', async () => {
|
||||
bvGetPrIsTrustedSpy.and.returnValue(Promise.resolve(false));
|
||||
|
||||
const expectedResponse = {canHavePublicPreview: false, reason: 'Not automatically verifiable as "trusted".'};
|
||||
const expectedLog =
|
||||
`PR:${pr} - Cannot have a public preview, because not automatically verifiable as "trusted".`;
|
||||
|
||||
await agent.get(url).expect(200, expectedResponse);
|
||||
|
||||
expect(bvGetSignificantFilesChangedSpy).toHaveBeenCalledWith(pr, jasmine.any(RegExp));
|
||||
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(pr);
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith(expectedLog);
|
||||
});
|
||||
|
||||
|
||||
it('should respond appropriately if the PR can have a preview', async () => {
|
||||
const expectedResponse = {canHavePublicPreview: true, reason: null};
|
||||
const expectedLog = `PR:${pr} - Can have a public preview.`;
|
||||
|
||||
await agent.get(url).expect(200, expectedResponse);
|
||||
|
||||
expect(bvGetSignificantFilesChangedSpy).toHaveBeenCalledWith(pr, jasmine.any(RegExp));
|
||||
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(pr);
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith(expectedLog);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with error if `getSignificantFilesChanged()` fails', async () => {
|
||||
bvGetSignificantFilesChangedSpy.and.callFake(() => Promise.reject('getSignificantFilesChanged error'));
|
||||
|
||||
await agent.get(url).expect(500, 'getSignificantFilesChanged error');
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('Previewability check error', 'getSignificantFilesChanged error');
|
||||
});
|
||||
|
||||
|
||||
it('should respond with error if `getPrIsTrusted()` fails', async () => {
|
||||
const error = new Error('getPrIsTrusted error');
|
||||
bvGetPrIsTrustedSpy.and.callFake(() => { throw error; });
|
||||
|
||||
await agent.get(url).expect(500, 'getPrIsTrusted error');
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('Previewability check error', error);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('POST /circle-build', () => {
|
||||
let getGithubInfoSpy: jasmine.Spy;
|
||||
let getSignificantFilesChangedSpy: jasmine.Spy;
|
||||
let downloadBuildArtifactSpy: jasmine.Spy;
|
||||
@ -359,7 +455,7 @@ describe('PreviewServerFactory', () => {
|
||||
await agent.post(URL).send(BASIC_PAYLOAD).expect(204);
|
||||
expect(getGithubInfoSpy).not.toHaveBeenCalled();
|
||||
expect(getSignificantFilesChangedSpy).not.toHaveBeenCalled();
|
||||
expect(console.log).toHaveBeenCalledWith(jasmine.any(String), 'PreviewServer: ',
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith(
|
||||
'Build:12345, Job:lint -', 'Skipping preview processing because this is not the "aio_preview" job.');
|
||||
expect(downloadBuildArtifactSpy).not.toHaveBeenCalled();
|
||||
expect(getPrIsTrustedSpy).not.toHaveBeenCalled();
|
||||
@ -371,7 +467,7 @@ describe('PreviewServerFactory', () => {
|
||||
await agent.post(URL).send(BASIC_PAYLOAD).expect(204);
|
||||
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
|
||||
expect(getSignificantFilesChangedSpy).toHaveBeenCalledWith(PR, jasmine.any(RegExp));
|
||||
expect(console.log).toHaveBeenCalledWith(jasmine.any(String), 'PreviewServer: ',
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith(
|
||||
'PR:777, Build:12345 - Skipping preview processing because this PR did not touch any significant files.');
|
||||
expect(downloadBuildArtifactSpy).not.toHaveBeenCalled();
|
||||
expect(getPrIsTrustedSpy).not.toHaveBeenCalled();
|
||||
@ -467,7 +563,7 @@ describe('PreviewServerFactory', () => {
|
||||
|
||||
|
||||
it('should respond with 404 for non-POST requests', async () => {
|
||||
await verifyRequests([
|
||||
await Promise.all([
|
||||
agent.get(url).expect(404),
|
||||
agent.put(url).expect(404),
|
||||
agent.patch(url).expect(404),
|
||||
@ -482,7 +578,7 @@ describe('PreviewServerFactory', () => {
|
||||
const request1 = agent.post(url);
|
||||
const request2 = agent.post(url).send();
|
||||
|
||||
await verifyRequests([
|
||||
await Promise.all([
|
||||
request1.expect(400, responseBody),
|
||||
request2.expect(400, responseBody),
|
||||
]);
|
||||
@ -495,7 +591,7 @@ describe('PreviewServerFactory', () => {
|
||||
const request1 = agent.post(url).send({});
|
||||
const request2 = agent.post(url).send({number: null});
|
||||
|
||||
await verifyRequests([
|
||||
await Promise.all([
|
||||
request1.expect(400, `${responseBodyPrefix} {}`),
|
||||
request2.expect(400, `${responseBodyPrefix} {"number":null}`),
|
||||
]);
|
||||
@ -503,7 +599,7 @@ describe('PreviewServerFactory', () => {
|
||||
|
||||
|
||||
it('should call \'BuildVerifier#gtPrIsTrusted()\' with the correct arguments', async () => {
|
||||
await promisifyRequest(createRequest(+pr));
|
||||
await createRequest(+pr);
|
||||
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9);
|
||||
});
|
||||
|
||||
@ -511,9 +607,8 @@ describe('PreviewServerFactory', () => {
|
||||
it('should propagate errors from BuildVerifier', async () => {
|
||||
bvGetPrIsTrustedSpy.and.callFake(() => Promise.reject('Test'));
|
||||
|
||||
const req = createRequest(+pr).expect(500, 'Test');
|
||||
await createRequest(+pr).expect(500, 'Test');
|
||||
|
||||
await promisifyRequest(req);
|
||||
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9);
|
||||
expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled();
|
||||
});
|
||||
@ -522,19 +617,17 @@ describe('PreviewServerFactory', () => {
|
||||
it('should call \'BuildCreator#updatePrVisibility()\' with the correct arguments', async () => {
|
||||
bvGetPrIsTrustedSpy.and.callFake((pr2: number) => Promise.resolve(pr2 === 42));
|
||||
|
||||
await promisifyRequest(createRequest(24));
|
||||
await createRequest(24);
|
||||
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(24, false);
|
||||
|
||||
await promisifyRequest(createRequest(42));
|
||||
await createRequest(42);
|
||||
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(42, true);
|
||||
});
|
||||
|
||||
|
||||
it('should propagate errors from BuildCreator', async () => {
|
||||
bcUpdatePrVisibilitySpy.and.callFake(() => Promise.reject('Test'));
|
||||
|
||||
const req = createRequest(+pr).expect(500, 'Test');
|
||||
await verifyRequests([req]);
|
||||
await createRequest(+pr).expect(500, 'Test');
|
||||
});
|
||||
|
||||
|
||||
@ -544,7 +637,7 @@ describe('PreviewServerFactory', () => {
|
||||
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||
|
||||
const reqs = [4, 2].map(num => createRequest(num).expect(200, http.STATUS_CODES[200]));
|
||||
await verifyRequests(reqs);
|
||||
await Promise.all(reqs);
|
||||
});
|
||||
|
||||
|
||||
@ -552,7 +645,7 @@ describe('PreviewServerFactory', () => {
|
||||
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||
|
||||
const reqs = [4, 2].map(num => createRequest(num, 'labeled').expect(200, http.STATUS_CODES[200]));
|
||||
await verifyRequests(reqs);
|
||||
await Promise.all(reqs);
|
||||
});
|
||||
|
||||
|
||||
@ -560,14 +653,13 @@ describe('PreviewServerFactory', () => {
|
||||
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||
|
||||
const reqs = [4, 2].map(num => createRequest(num, 'unlabeled').expect(200, http.STATUS_CODES[200]));
|
||||
await verifyRequests(reqs);
|
||||
await Promise.all(reqs);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 200 (and do nothing) if \'action\' implies no visibility change', async () => {
|
||||
const promises = ['foo', 'notlabeled'].
|
||||
map(action => createRequest(+pr, action).expect(200, http.STATUS_CODES[200])).
|
||||
map(promisifyRequest);
|
||||
map(action => createRequest(+pr, action).expect(200, http.STATUS_CODES[200]));
|
||||
|
||||
await Promise.all(promises);
|
||||
expect(bvGetPrIsTrustedSpy).not.toHaveBeenCalled();
|
||||
@ -584,7 +676,7 @@ describe('PreviewServerFactory', () => {
|
||||
it('should respond with 404', async () => {
|
||||
const responseFor = (method: string) => `Unknown resource in request: ${method.toUpperCase()} /some/url`;
|
||||
|
||||
await verifyRequests([
|
||||
await Promise.all([
|
||||
agent.get('/some/url').expect(404, responseFor('get')),
|
||||
agent.put('/some/url').expect(404, responseFor('put')),
|
||||
agent.post('/some/url').expect(404, responseFor('post')),
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@
|
||||
set -eu -o pipefail
|
||||
|
||||
# Set up env variables
|
||||
export AIO_CIRCLE_CI_TOKEN=UNUSED_CIRCLE_CI_TOKEN
|
||||
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null)
|
||||
|
||||
# Run the clean-up
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
TODO (gkalpak): Add docs. Mention:
|
||||
- Testing on CI.
|
||||
Relevant files: `scripts/ci/test-aio.sh`, `aio/aio-builds-setup/scripts/test.sh`
|
||||
Relevant files: `aio/aio-builds-setup/scripts/test.sh`
|
||||
- Deploying from CI.
|
||||
Relevant files: `scripts/ci/deploy.sh`, `aio/scripts/deploy-to-firebase.sh`
|
||||
Relevant files: `.circleci/config.yml`, `scripts/ci/deploy.sh`, `aio/scripts/build-artifacts.sh`,
|
||||
`aio/scripts/deploy-to-firebase.sh`
|
||||
|
@ -34,34 +34,31 @@ container:
|
||||
|
||||
|
||||
### On CI (CircleCI)
|
||||
- Build job completes successfully.
|
||||
- The CI script checks whether the build job was initiated by a PR against the angular/angular
|
||||
master branch.
|
||||
- The CI script checks whether the PR has touched any files that might affect the angular.io app
|
||||
(currently the `aio/` or `packages/` directories, ignoring spec files).
|
||||
- The CI script builds the angular.io project.
|
||||
- The CI script gzips and stores the build artifacts in the CI infrastructure.
|
||||
- When the build completes CircleCI triggers a webhook on the preview-server.
|
||||
- When the build completes, CircleCI triggers a webhook on the preview-server.
|
||||
|
||||
More info on how to set things up on CI can be found [here](misc--integrate-with-ci.md).
|
||||
|
||||
|
||||
### Hosting build artifacts
|
||||
|
||||
- nginx receives the webhook trigger and passes it through to the preview server.
|
||||
- The preview-server runs several preliminary checks to determine whether the request is valid and
|
||||
whether the corresponding PR can have a (public or non-public) preview (more details can be found
|
||||
[here](overview--security-model.md)).
|
||||
- The preview-server makes a request to CircleCI for the URL of the AIO build artifacts.
|
||||
- The preview-server makes a request to this URL to receive the artifact - failing if the size
|
||||
exceeds the specified max file size - and stores it in a temporary location.
|
||||
- The preview-server runs several checks to determine whether the request should be accepted and
|
||||
whether it should be publicly accessible or stored for later verification (more details can be
|
||||
found [here](overview--security-model.md)).
|
||||
- The preview-server runs more checks to determine whether the preview should be publicly accessible
|
||||
or stored for later verification (more details can be found [here](overview--security-model.md)).
|
||||
- The preview-server changes the "visibility" of the associated PR, if necessary. For example, if
|
||||
builds for the same PR had been previously deployed as non-public and the current build has been
|
||||
automatically verified, all previous builds are made public as well.
|
||||
If the PR transitions from "non-public" to "public", the preview-server posts a comment on the
|
||||
corresponding PR on GitHub mentioning the SHAs and the links where the previews can be found.
|
||||
- The preview-server verifies that it is not trying to overwrite an existing build.
|
||||
- The preview-server deploys the artifacts to a sub-directory named after the PR number and the first
|
||||
few characters of the SHA: `<PR>/<SHA>/`
|
||||
- The preview-server deploys the artifacts to a sub-directory named after the PR number and the
|
||||
first few characters of the SHA: `<PR>/<SHA>/`
|
||||
(Non-publicly accessible PRs will be stored in a different location, but again derived from the PR
|
||||
number and SHA.)
|
||||
- If the PR is publicly accessible, the preview-server posts a comment on the corresponding PR on
|
||||
@ -101,8 +98,8 @@ More info on the possible HTTP status codes and their meaning can be found
|
||||
|
||||
### Removing obsolete artifacts
|
||||
In order to avoid flooding the disk with unnecessary build artifacts, there is a cronjob that runs a
|
||||
clean-up tasks once a day. The task retrieves all open PRs from GitHub and removes all directories
|
||||
that do not correspond with an open PR.
|
||||
clean-up task once a day. The task retrieves all open PRs from GitHub and removes all directories
|
||||
that do not correspond to an open PR.
|
||||
|
||||
|
||||
### Health-check
|
||||
|
@ -1,8 +1,8 @@
|
||||
# Overview - HTTP Status Codes
|
||||
|
||||
|
||||
This is a list of all the possible HTTP status codes returned by the nginx and preview servers, along
|
||||
with a brief explanation of what they mean:
|
||||
This is a list of all the possible HTTP status codes returned by the nginx and preview servers,
|
||||
along with a brief explanation of what they mean:
|
||||
|
||||
|
||||
## `http://*.ngbuilds.io/*`
|
||||
@ -25,6 +25,23 @@ with a brief explanation of what they mean:
|
||||
File not found.
|
||||
|
||||
|
||||
## `https://ngbuilds.io/can-have-public-preview/<pr>`
|
||||
|
||||
- **200 (OK)**:
|
||||
Whether the PR can have a public preview (based on its author, label, changed files).
|
||||
_Response type:_ JSON
|
||||
_Response format:_
|
||||
```ts
|
||||
{
|
||||
canHavePublicPreview: boolean,
|
||||
reason: string | null,
|
||||
}
|
||||
```
|
||||
|
||||
- **405 (Method Not Allowed)**:
|
||||
Request method other than GET.
|
||||
|
||||
|
||||
## `https://ngbuilds.io/circle-build`
|
||||
|
||||
- **201 (Created)**:
|
||||
|
@ -11,8 +11,8 @@ part of the CI process and serving them publicly.
|
||||
|
||||
## Security objectives
|
||||
|
||||
- **Prevent hosting arbitrary content to on servers.**
|
||||
Since there is no restriction on who can submit a PR, we cannot allow arbitrary untrusted PRs'
|
||||
- **Prevent hosting arbitrary content on our servers.**
|
||||
Since there is no restriction on who can submit a PR, we cannot allow arbitrary, untrusted PRs'
|
||||
build artifacts to be hosted.
|
||||
|
||||
- **Prevent overwriting other people's hosted build artifacts.**
|
||||
@ -40,40 +40,49 @@ part of the CI process and serving them publicly.
|
||||
### In a nutshell
|
||||
The implemented approach can be broken up to the following sub-tasks:
|
||||
|
||||
0. Receive notification from CircleCI of a completed build.
|
||||
1. Verify that the build is valid and download the artifact.
|
||||
2. Fetch the PR's metadata, including author and labels.
|
||||
3. Check whether the PR can be automatically verified as "trusted" (based on its author or labels).
|
||||
4. If necessary, update the corresponding PR's verification status.
|
||||
5. Deploy the artifacts to the corresponding PR's directory.
|
||||
6. Prevent overwriting previously deployed artifacts (which ensures that the guarantees established
|
||||
1. Receive notification from CircleCI of a completed build.
|
||||
2. Verify that the build is valid and can have a preview.
|
||||
3. Download the build artifact.
|
||||
4. Fetch the PR's metadata, including author and labels.
|
||||
5. Check whether the PR can be automatically verified as "trusted" (based on its author or labels).
|
||||
6. If necessary, update the corresponding PR's verification status.
|
||||
7. Deploy the artifacts to the corresponding PR's directory.
|
||||
8. Prevent overwriting previously deployed artifacts (which ensures that the guarantees established
|
||||
during deployment will remain valid until the artifacts are removed).
|
||||
7. Prevent hosted preview files from accessing anything outside their directory.
|
||||
9. Prevent hosted preview files from accessing anything outside their directory.
|
||||
|
||||
|
||||
### Implementation details
|
||||
This section describes how each of the aforementioned sub-tasks is accomplished:
|
||||
|
||||
0. **Receive notification from CircleCI of a completed build**
|
||||
1. **Receive notification from CircleCI of a completed build**
|
||||
|
||||
CircleCI is configured to trigger a webhook on our preview-server whenever a build completes.
|
||||
The payload contains the number of the build that completed.
|
||||
|
||||
1. **Verify that the build is valid and download the artifact.**
|
||||
2. **Verify that the build is valid and can have a preview.**
|
||||
|
||||
We cannot trust that the data in the webhook trigger is authentic, so we only extract the build
|
||||
number and then run a direct query against the CircleCI API to get hold of the real data for
|
||||
the given build number.
|
||||
|
||||
If the build was not successful then we ignore this trigger. Otherwise we check that the
|
||||
associated github organisation and repository are what we expect (e.g. angular/angular).
|
||||
We perform a number of preliminary checks:
|
||||
- Was the webhook triggered by the designated CircleCI job (currently `aio_preview`)?
|
||||
- Was the build successful?
|
||||
- Are the associated GitHub organisation and repository what we expect (e.g. `angular/angular`)?
|
||||
- Has the PR touched any files that might affect the angular.io app (currently the `aio/` or
|
||||
`packages/` directories, ignoring spec files)?
|
||||
|
||||
Next we make another call to the CircleCI API to get a list of the URLS for artifacts of that
|
||||
If any of the preliminary checks fails, the process is aborted and not preview is generated.
|
||||
|
||||
3. **Download the build artifact.**
|
||||
|
||||
Next we make another call to the CircleCI API to get a list of the URLs for artifacts of that
|
||||
build. If there is one that matches the configured artifact path, we download the contents of the
|
||||
build artifact and store it in a local folder. This download has a maximum size limit to prevent
|
||||
PRs from producing artifacts that are so large they would cause the preview server to crash.
|
||||
|
||||
2. **Fetch the PR's metadata, including author and labels**.
|
||||
4. **Fetch the PR's metadata, including author and labels**.
|
||||
|
||||
Once we have securely downloaded the artifact for a build, we retrieve the PR's metadata -
|
||||
including the author's username and the labels - using the
|
||||
@ -81,7 +90,7 @@ This section describes how each of the aforementioned sub-tasks is accomplished:
|
||||
To avoid rate-limit restrictions, we use a Personal Access Token (issued by
|
||||
[@mary-poppins](https://github.com/mary-poppins)).
|
||||
|
||||
3. **Check whether the PR can be automatically verified as "trusted"**.
|
||||
5. **Check whether the PR can be automatically verified as "trusted"**.
|
||||
|
||||
"Trusted" means that we are confident that the build artifacts are suitable for being deployed
|
||||
and publicly accessible on the preview server. There are two ways to check that:
|
||||
@ -93,31 +102,32 @@ This section describes how each of the aforementioned sub-tasks is accomplished:
|
||||
`read:org` scope issued by a user that can "see" the specified GitHub organization.
|
||||
Here too, we use the token by @mary-poppins.
|
||||
|
||||
4. **If necessary update the corresponding PR's verification status**.
|
||||
6. **If necessary update the corresponding PR's verification status**.
|
||||
|
||||
Once we have determined whether the PR is considered "trusted", we update its "visibility" (i.e.
|
||||
whether it is publicly accessible or not), based on the new verification status. For example, if
|
||||
a PR was initially considered "not trusted" but the check triggered by a new build determined
|
||||
otherwise, the PR (and all the previously hosted previews) are made public. It works the same
|
||||
otherwise, the PR (and all the previously downloaded previews) are made public. It works the same
|
||||
way if a PR has gone from "trusted" to "not trusted".
|
||||
|
||||
5. **Deploy the artifacts to the corresponding PR's directory.**
|
||||
7. **Deploy the artifacts to the corresponding PR's directory.**
|
||||
|
||||
With the preceding steps, we have verified that the build artifacts are valid.
|
||||
Additionally, we have determined whether the PR can be trusted to have its previews
|
||||
publicly accessible or whether further verification is necessary. The artifacts will be stored to
|
||||
the PR's directory, but will not be publicly accessible unless the PR has been verified.
|
||||
Essentially, as long as sub-tasks 1, 2 and 3 can be securely accomplished, it is possible to
|
||||
"project" the trust we have in a team's members through the PR to the build artifacts.
|
||||
With the preceding steps, we have verified that the build artifacts are valid. Additionally, we
|
||||
have determined whether the PR can be trusted to have its previews publicly accessible or whether
|
||||
further verification is necessary.
|
||||
|
||||
6. **Prevent overwriting previously deployed artifacts**.
|
||||
The artifacts will be stored to the PR's directory, but will not be publicly accessible unless
|
||||
the PR has been verified. Essentially, as long as sub-tasks 2, 3, 4 and 5 can be securely
|
||||
accomplished, it is possible to "project" the trust we have in a team's members through the PR to
|
||||
the build artifacts.
|
||||
|
||||
8. **Prevent overwriting previously deployed artifacts**.
|
||||
|
||||
In order to enforce this restriction (and ensure that the deployed artifacts' validity is
|
||||
preserved throughout their "lifetime"), the server that handles the artifacts (currently a Node.js
|
||||
Express server) rejects builds that have already been handled.
|
||||
preserved throughout their "lifetime"), the server that handles the artifacts (currently a Node.js Express server) rejects builds that have already been handled.
|
||||
_Note: A PR can contain multiple builds; one for each SHA that was built on CircleCI._
|
||||
|
||||
7. **Prevent hosted preview files from accessing anything outside their directory.**
|
||||
9. **Prevent hosted preview files from accessing anything outside their directory.**
|
||||
|
||||
Nginx (which is used to serve the hosted preview) has been configured to not follow symlinks
|
||||
outside of the directory where the preview files are stored.
|
||||
@ -130,10 +140,10 @@ This section describes how each of the aforementioned sub-tasks is accomplished:
|
||||
This means that any secret access keys need only be stored on the preview-server and not on any of
|
||||
the CI build infrastructure (e.g. CircleCI).
|
||||
|
||||
- Each trusted PR author has full control over the content that is hosted as a preview for their PRs.
|
||||
Part of the security model relies on the trustworthiness of these authors.
|
||||
- Each trusted PR author has full control over the content that is hosted as a preview for their
|
||||
PRs. Part of the security model relies on the trustworthiness of these authors.
|
||||
|
||||
- Adding the specified label on a PR to mark it as trusted, gives the author full control over
|
||||
the content that is hosted for the specific PR preview (e.g. by pushing more commits to it).
|
||||
The user adding the label is responsible for ensuring that this control is not abused and that
|
||||
the PR is either closed (one way of another) or the access is revoked.
|
||||
- Adding the specified label on a PR to mark it as trusted, gives the author full control over the
|
||||
content that is hosted for the specific PR preview (e.g. by pushing more commits to it). The user
|
||||
adding the label is responsible for ensuring that this control is not abused and that the PR is
|
||||
either closed (one way of another) or the access is revoked.
|
||||
|
@ -8,7 +8,7 @@ Necessary secrets:
|
||||
1. `GITHUB_TOKEN`
|
||||
- Used for:
|
||||
- Retrieving open PRs without rate-limiting.
|
||||
- Retrieving PR author.
|
||||
- Retrieving PR info, such as author, labels, changed files.
|
||||
- Retrieving members of the trusted GitHub teams.
|
||||
- Posting comments with preview links on PRs.
|
||||
|
||||
@ -25,8 +25,9 @@ Necessary secrets:
|
||||
- Generate new token with the `public_repo` scope.
|
||||
|
||||
2. `CIRCLE_CI_TOKEN`
|
||||
- Visit https://circleci.com/gh/angular/angular/edit#api
|
||||
- Create an API token with `Build Artifacts` scope
|
||||
- Visit https://circleci.com/gh/angular/angular/edit#api.
|
||||
- Create an API token with `Build Artifacts` scope.
|
||||
|
||||
|
||||
## Save secrets on the VM
|
||||
|
||||
|
3
aio/content/cli-src/.gitignore
vendored
Normal file
3
aio/content/cli-src/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/node_modules
|
||||
package.json
|
||||
yarn.lock
|
102
aio/content/cli/index.md
Normal file
102
aio/content/cli/index.md
Normal file
@ -0,0 +1,102 @@
|
||||
<h1 class="no-toc">CLI Command Reference</h1>
|
||||
|
||||
The Angular CLI is a command-line interface tool that you use to initialize, develop, scaffold, and maintain Angular applications. You can use the tool directly in a command shell, or indirectly through an interactive UI such as [Angular Console](https://angularconsole.com).
|
||||
|
||||
## Installing Angular CLI
|
||||
|
||||
Major versions of Angular CLI follow the supported major version of Angular, but minor versions can be released separately.
|
||||
|
||||
Install the CLI using the `npm` package manager:
|
||||
<code-example format="." language="bash">
|
||||
npm install -g @angular/cli
|
||||
</code-example>
|
||||
|
||||
For details about changes between versions, and information about updating from previous releases,
|
||||
see the Releases tab on GitHub: https://github.com/angular/angular-cli/releases
|
||||
|
||||
## Basic workflow
|
||||
|
||||
Invoke the tool on the command line through the `ng` executable.
|
||||
Online help is available on the command line.
|
||||
Enter the following to list commands or options for a given command (such as [generate](cli/generate)) with a short description.
|
||||
|
||||
<code-example format="." language="bash">
|
||||
ng help
|
||||
ng generate --help
|
||||
</code-example>
|
||||
|
||||
To create, build, and serve a new, basic Angular project on a development server, go to the parent directory of your new workspace use the following commands:
|
||||
|
||||
<code-example format="." language="bash">
|
||||
ng new my-first-project
|
||||
cd my-first-project
|
||||
ng serve
|
||||
</code-example>
|
||||
|
||||
In your browser, open http://localhost:4200/ to see the new app run.
|
||||
When you use the [ng serve](cli/serve) command to build an app and serve it locally, the server automatically rebuilds the app and reloads the page when you change any of the source files.
|
||||
|
||||
## Workspaces and project files
|
||||
|
||||
The [ng new](cli/new) command creates an *Angular workspace* folder and generates a new app skeleton.
|
||||
A workspace can contain multiple apps and libraries.
|
||||
The initial app created by the [ng new](cli/new) command is at the top level of the workspace.
|
||||
When you generate an additional app or library in a workspace, it goes into a `projects/` subfolder.
|
||||
|
||||
A newly generated app contains the source files for a root module, with a root component and template.
|
||||
Each app has a `src` folder that contains the logic, data, and assets.
|
||||
|
||||
You can edit the generated files directly, or add to and modify them using CLI commands.
|
||||
Use the [ng generate](cli/generate) command to add new files for additional components and services, and code for new pipes, directives, and so on.
|
||||
Commands such as [add](cli/add) and [generate](cli/generate), which create or operate on apps and libraries, must be executed from within a workspace or project folder.
|
||||
|
||||
* See more about the [Workspace file structure](guide/file-structure).
|
||||
|
||||
### Workspace and project configuration
|
||||
|
||||
A single workspace configuration file, `angular.json`, is created at the top level of the workspace.
|
||||
This is where you can set per-project defaults for CLI command options, and specify configurations to use when the CLI builds a project for different targets.
|
||||
|
||||
The [ng config](cli/config) command lets you set and retrieve configuration values from the command line, or you can edit the `angular.json` file directly.
|
||||
Note that option names in the configuration file must use [camelCase](guide/glossary#case-types), while option names supplied to commands can use either camelCase or dash-case.
|
||||
|
||||
* See the [complete schema](https://github.com/angular/angular-cli/wiki/angular-workspace) for `angular.json`.
|
||||
<!-- * Learn more about *configuration options for Angular(links to new guide or topics TBD)*. -->
|
||||
|
||||
|
||||
## CLI command-language syntax
|
||||
|
||||
Command syntax is shown as follows:
|
||||
|
||||
`ng` *commandNameOrAlias* *requiredArg* [*optionalArg*] `[options]`
|
||||
|
||||
* Most commands, and some options, have aliases. Aliases are shown in the syntax statement for each command.
|
||||
|
||||
* Option names are prefixed with a double dash (--).
|
||||
Option aliases are prefixed with a single dash (-).
|
||||
Arguments are not prefixed.
|
||||
For example: `ng build my-app -c production`
|
||||
|
||||
* Typically, the name of a generated artifact can be given as an argument to the command or specified with the --name option.
|
||||
|
||||
* Argument and option names can be given in either
|
||||
[camelCase or dash-case](guide/glossary#case-types).
|
||||
`--myOptionName` is equivalent to `--my-option-name`.
|
||||
|
||||
### Boolean and enumerated options
|
||||
|
||||
Boolean options have two forms: `--thisOption` sets the flag, `--noThisOption` clears it.
|
||||
If neither option is supplied, the flag remains in its default state, as listed in the reference documentation.
|
||||
|
||||
Allowed values are given with each enumerated option description, with the default value in **bold**.
|
||||
|
||||
### Relative paths
|
||||
|
||||
Options that specify files can be given as absolute paths, or as paths relative to the current working directory, which is generally either the workspace or project root.
|
||||
|
||||
### Schematics
|
||||
|
||||
The [ng generate](cli/generate) and [ng add](cli/add) commands take as an argument the artifact or library to be generated or added to the current project.
|
||||
In addition to any general options, each artifact or library defines its own options in a *schematic*.
|
||||
Schematic options are supplied to the command in the same format as immediate command options.
|
||||
|
@ -1,351 +1,251 @@
|
||||
'use strict'; // necessary for es6 output in node
|
||||
|
||||
import { browser, element, by, ElementFinder } from 'protractor';
|
||||
import { logging, promise } from 'selenium-webdriver';
|
||||
import { browser, ExpectedConditions as EC } from 'protractor';
|
||||
import { logging } from 'selenium-webdriver';
|
||||
import * as openClose from './open-close.po';
|
||||
import * as statusSlider from './status-slider.po';
|
||||
import * as toggle from './toggle.po';
|
||||
import * as enterLeave from './enter-leave.po';
|
||||
import * as auto from './auto.po';
|
||||
import * as filterStagger from './filter-stagger.po';
|
||||
import * as heroGroups from './hero-groups';
|
||||
import { getLinkById, sleepFor } from './util';
|
||||
|
||||
/**
|
||||
* The tests here basically just checking that the end styles
|
||||
* of each animation are in effect.
|
||||
*
|
||||
* Relies on the Angular testability only becoming stable once
|
||||
* animation(s) have finished.
|
||||
*
|
||||
* Ideally we'd use https://developer.mozilla.org/en-US/docs/Web/API/Document/getAnimations
|
||||
* but they're not supported in Chrome at the moment. The upcoming nganimate polyfill
|
||||
* may also add some introspection support.
|
||||
*/
|
||||
describe('Animation Tests', () => {
|
||||
const openCloseHref = getLinkById('open-close');
|
||||
const statusSliderHref = getLinkById('status');
|
||||
const toggleHref = getLinkById('toggle');
|
||||
const enterLeaveHref = getLinkById('enter-leave');
|
||||
const autoHref = getLinkById('auto');
|
||||
const filterHref = getLinkById('heroes');
|
||||
const heroGroupsHref = getLinkById('hero-groups');
|
||||
|
||||
const INACTIVE_COLOR = 'rgba(238, 238, 238, 1)';
|
||||
const ACTIVE_COLOR = 'rgba(207, 216, 220, 1)';
|
||||
const NO_TRANSFORM_MATRIX_REGEX = /matrix\(1,\s*0,\s*0,\s*1,\s*0,\s*0\)/;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeAll(() => {
|
||||
browser.get('');
|
||||
});
|
||||
|
||||
describe('basic states', () => {
|
||||
describe('Open/Close Component', () => {
|
||||
const closedHeight = '100px';
|
||||
const openHeight = '200px';
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('app-hero-list-basic'));
|
||||
beforeAll(async () => {
|
||||
await openCloseHref.click();
|
||||
sleepFor();
|
||||
});
|
||||
|
||||
it('animates between active and inactive', () => {
|
||||
addInactiveHero();
|
||||
it('should be open', async () => {
|
||||
const toggleButton = openClose.getToggleButton();
|
||||
const container = openClose.getComponentContainer();
|
||||
let text = await container.getText();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
if (text.includes('Closed')) {
|
||||
await toggleButton.click();
|
||||
await browser.wait(async () => await container.getCssValue('height') === openHeight, 2000);
|
||||
}
|
||||
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
text = await container.getText();
|
||||
const containerHeight = await container.getCssValue('height');
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.1);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
expect(text).toContain('The box is now Open!');
|
||||
expect(containerHeight).toBe(openHeight);
|
||||
});
|
||||
|
||||
it('should be closed', async () => {
|
||||
const toggleButton = openClose.getToggleButton();
|
||||
const container = openClose.getComponentContainer();
|
||||
let text = await container.getText();
|
||||
|
||||
if (text.includes('Open')) {
|
||||
await toggleButton.click();
|
||||
await browser.wait(async () => await container.getCssValue('height') === closedHeight, 2000);
|
||||
}
|
||||
|
||||
text = await container.getText();
|
||||
const containerHeight = await container.getCssValue('height');
|
||||
|
||||
expect(text).toContain('The box is now Closed!');
|
||||
expect(containerHeight).toBe(closedHeight);
|
||||
});
|
||||
|
||||
describe('styles inline in transitions', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(function() {
|
||||
host = element(by.css('app-hero-list-inline-styles'));
|
||||
});
|
||||
|
||||
it('are not kept after animation', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('combined transition syntax', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('app-hero-list-combined-transitions'));
|
||||
});
|
||||
|
||||
it('animates between active and inactive', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.1);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('two-way transition syntax', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('app-hero-list-twoway'));
|
||||
});
|
||||
|
||||
it('animates between active and inactive', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.1);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('enter & leave', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('app-hero-list-enter-leave'));
|
||||
});
|
||||
|
||||
it('adds and removes element', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
||||
|
||||
removeHero();
|
||||
expect(li.isPresent()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('enter & leave & states', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(function() {
|
||||
host = element(by.css('app-hero-list-enter-leave-states'));
|
||||
});
|
||||
|
||||
it('adds and removes and animates between active and inactive', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
|
||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.1);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
||||
|
||||
removeHero();
|
||||
expect(li.isPresent()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('auto style calc', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(function() {
|
||||
host = element(by.css('app-hero-list-auto'));
|
||||
});
|
||||
|
||||
it('adds and removes element', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
expect(li.getCssValue('height')).toBe('50px');
|
||||
|
||||
removeHero();
|
||||
expect(li.isPresent()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('different timings', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('app-hero-list-timings'));
|
||||
});
|
||||
|
||||
it('adds and removes element', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
||||
expect(li.getCssValue('opacity')).toMatch('1');
|
||||
|
||||
removeHero();
|
||||
expect(li.isPresent()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('multiple keyframes', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('app-hero-list-multistep'));
|
||||
});
|
||||
|
||||
it('adds and removes element', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
||||
expect(li.getCssValue('opacity')).toMatch('1');
|
||||
|
||||
removeHero();
|
||||
expect(li.isPresent()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('parallel groups', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('app-hero-list-groups'));
|
||||
});
|
||||
|
||||
it('adds and removes element', () => {
|
||||
addInactiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX);
|
||||
expect(li.getCssValue('opacity')).toMatch('1');
|
||||
|
||||
removeHero(700);
|
||||
expect(li.isPresent()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('adding active heroes', () => {
|
||||
|
||||
let host: ElementFinder;
|
||||
|
||||
beforeEach(() => {
|
||||
host = element(by.css('app-hero-list-basic'));
|
||||
});
|
||||
|
||||
it('animates between active and inactive', () => {
|
||||
addActiveHero();
|
||||
|
||||
let li = host.element(by.css('li'));
|
||||
|
||||
expect(getScaleX(li)).toBe(1.1);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.0);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR);
|
||||
|
||||
li.click();
|
||||
browser.driver.sleep(300);
|
||||
expect(getScaleX(li)).toBe(1.1);
|
||||
expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR);
|
||||
});
|
||||
});
|
||||
|
||||
describe('callbacks', () => {
|
||||
it('fires a callback on start and done', () => {
|
||||
addActiveHero();
|
||||
browser.manage().logs().get(logging.Type.BROWSER)
|
||||
.then((logs: logging.Entry[]) => {
|
||||
const animationMessages = logs.filter((log) => {
|
||||
return log.message.indexOf('Animation') !== -1 ? true : false;
|
||||
});
|
||||
it('should log animation events', async () => {
|
||||
const toggleButton = openClose.getToggleButton();
|
||||
const loggingCheckbox = openClose.getLoggingCheckbox();
|
||||
await loggingCheckbox.click();
|
||||
await toggleButton.click();
|
||||
|
||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||
const animationMessages = logs.filter(({ message }) => message.includes('Animation'));
|
||||
|
||||
expect(animationMessages.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Status Slider Component', () => {
|
||||
const activeColor = 'rgba(255, 165, 0, 1)';
|
||||
const inactiveColor = 'rgba(0, 0, 255, 1)';
|
||||
|
||||
beforeAll(async () => {
|
||||
await statusSliderHref.click();
|
||||
sleepFor(2000);
|
||||
});
|
||||
|
||||
function addActiveHero(sleep?: number) {
|
||||
sleep = sleep || 500;
|
||||
element(by.buttonText('Add active hero')).click();
|
||||
browser.driver.sleep(sleep);
|
||||
it('should be inactive with an orange background', async () => {
|
||||
const toggleButton = statusSlider.getToggleButton();
|
||||
const container = statusSlider.getComponentContainer();
|
||||
let text = await container.getText();
|
||||
|
||||
if (text === 'Active') {
|
||||
await toggleButton.click();
|
||||
await browser.wait(async () => await container.getCssValue('backgroundColor') === inactiveColor, 2000);
|
||||
}
|
||||
|
||||
function addInactiveHero(sleep?: number) {
|
||||
sleep = sleep || 500;
|
||||
element(by.buttonText('Add inactive hero')).click();
|
||||
browser.driver.sleep(sleep);
|
||||
}
|
||||
text = await container.getText();
|
||||
const bgColor = await container.getCssValue('backgroundColor');
|
||||
|
||||
function removeHero(sleep?: number) {
|
||||
sleep = sleep || 500;
|
||||
element(by.buttonText('Remove hero')).click();
|
||||
browser.driver.sleep(sleep);
|
||||
}
|
||||
|
||||
function getScaleX(el: ElementFinder) {
|
||||
return Promise.all([
|
||||
getBoundingClientWidth(el),
|
||||
getOffsetWidth(el)
|
||||
]).then(function(promiseResolutions) {
|
||||
let clientWidth = promiseResolutions[0];
|
||||
let offsetWidth = promiseResolutions[1];
|
||||
return clientWidth / offsetWidth;
|
||||
expect(text).toBe('Inactive');
|
||||
expect(bgColor).toBe(inactiveColor);
|
||||
});
|
||||
|
||||
it('should be active with a blue background', async () => {
|
||||
const toggleButton = statusSlider.getToggleButton();
|
||||
const container = statusSlider.getComponentContainer();
|
||||
let text = await container.getText();
|
||||
|
||||
if (text === 'Inactive') {
|
||||
await toggleButton.click();
|
||||
await browser.wait(async () => await container.getCssValue('backgroundColor') === activeColor, 2000);
|
||||
}
|
||||
|
||||
function getBoundingClientWidth(el: ElementFinder) {
|
||||
return browser.executeScript(
|
||||
'return arguments[0].getBoundingClientRect().width',
|
||||
el.getWebElement()
|
||||
) as PromiseLike<number>;
|
||||
}
|
||||
text = await container.getText();
|
||||
const bgColor = await container.getCssValue('backgroundColor');
|
||||
|
||||
function getOffsetWidth(el: ElementFinder) {
|
||||
return browser.executeScript(
|
||||
'return arguments[0].offsetWidth',
|
||||
el.getWebElement()
|
||||
) as PromiseLike<number>;
|
||||
}
|
||||
expect(text).toBe('Active');
|
||||
expect(bgColor).toBe(activeColor);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Toggle Animations Component', () => {
|
||||
beforeAll(async () => {
|
||||
await toggleHref.click();
|
||||
sleepFor();
|
||||
});
|
||||
|
||||
it('should disabled animations on the child element', async () => {
|
||||
const toggleButton = toggle.getToggleAnimationsButton();
|
||||
|
||||
await toggleButton.click();
|
||||
|
||||
const container = toggle.getComponentContainer();
|
||||
const cssClasses = await container.getAttribute('class');
|
||||
|
||||
expect(cssClasses).toContain('ng-animate-disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Enter/Leave Component', () => {
|
||||
beforeAll(async () => {
|
||||
await enterLeaveHref.click();
|
||||
sleepFor(100);
|
||||
});
|
||||
|
||||
it('should attach a flyInOut trigger to the list of items', async () => {
|
||||
const heroesList = enterLeave.getHeroesList();
|
||||
const hero = heroesList.get(0);
|
||||
const cssClasses = await hero.getAttribute('class');
|
||||
const transform = await hero.getCssValue('transform');
|
||||
|
||||
expect(cssClasses).toContain('ng-trigger-flyInOut');
|
||||
expect(transform).toBe('matrix(1, 0, 0, 1, 0, 0)');
|
||||
});
|
||||
|
||||
it('should remove the hero from the list when clicked', async () => {
|
||||
const heroesList = enterLeave.getHeroesList();
|
||||
const total = await heroesList.count();
|
||||
const hero = heroesList.get(0);
|
||||
|
||||
await hero.click();
|
||||
await browser.wait(async () => await heroesList.count() < total, 2000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Auto Calculation Component', () => {
|
||||
beforeAll(async () => {
|
||||
await autoHref.click();
|
||||
sleepFor(0);
|
||||
});
|
||||
|
||||
it('should attach a shrinkOut trigger to the list of items', async () => {
|
||||
const heroesList = auto.getHeroesList();
|
||||
const hero = heroesList.get(0);
|
||||
const cssClasses = await hero.getAttribute('class');
|
||||
|
||||
expect(cssClasses).toContain('ng-trigger-shrinkOut');
|
||||
});
|
||||
|
||||
it('should remove the hero from the list when clicked', async () => {
|
||||
const heroesList = auto.getHeroesList();
|
||||
const total = await heroesList.count();
|
||||
const hero = heroesList.get(0);
|
||||
|
||||
await hero.click();
|
||||
await browser.wait(async () => await heroesList.count() < total, 2000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filter/Stagger Component', () => {
|
||||
beforeAll(async () => {
|
||||
await filterHref.click();
|
||||
sleepFor();
|
||||
});
|
||||
|
||||
it('should attach a filterAnimations trigger to the list container', async () => {
|
||||
const heroesList = filterStagger.getComponentContainer();
|
||||
const cssClasses = await heroesList.getAttribute('class');
|
||||
|
||||
expect(cssClasses).toContain('ng-trigger-filterAnimation');
|
||||
});
|
||||
|
||||
it('should filter down the list when a search is performed', async () => {
|
||||
const heroesList = filterStagger.getHeroesList();
|
||||
const total = await heroesList.count();
|
||||
|
||||
const formInput = filterStagger.getFormInput();
|
||||
await formInput.sendKeys('Mag');
|
||||
|
||||
await browser.wait(async () => await heroesList.count() === 2, 2000);
|
||||
|
||||
const newTotal = await heroesList.count();
|
||||
expect(newTotal).toBeLessThan(total);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Hero Groups Component', () => {
|
||||
beforeAll(async () => {
|
||||
await heroGroupsHref.click();
|
||||
sleepFor(300);
|
||||
});
|
||||
|
||||
it('should attach a flyInOut trigger to the list of items', async () => {
|
||||
const heroesList = heroGroups.getHeroesList();
|
||||
const hero = heroesList.get(0);
|
||||
const cssClasses = await hero.getAttribute('class');
|
||||
const transform = await hero.getCssValue('transform');
|
||||
const opacity = await hero.getCssValue('opacity');
|
||||
|
||||
expect(cssClasses).toContain('ng-trigger-flyInOut');
|
||||
expect(transform).toBe('matrix(1, 0, 0, 1, 0, 0)');
|
||||
expect(opacity).toBe('1');
|
||||
});
|
||||
|
||||
it('should remove the hero from the list when clicked', async () => {
|
||||
const heroesList = heroGroups.getHeroesList();
|
||||
const total = await heroesList.count();
|
||||
const hero = heroesList.get(0);
|
||||
|
||||
await hero.click();
|
||||
await browser.wait(async () => await heroesList.count() < total, 2000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
19
aio/content/examples/animations/e2e/src/auto.po.ts
Normal file
19
aio/content/examples/animations/e2e/src/auto.po.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { by } from 'protractor';
|
||||
import { locate } from './util';
|
||||
|
||||
export function getPage() {
|
||||
return by.css('app-hero-list-auto-page');
|
||||
}
|
||||
|
||||
export function getComponent() {
|
||||
return by.css('app-hero-list-auto');
|
||||
}
|
||||
|
||||
export function getComponentContainer() {
|
||||
const findContainer = () => by.css('ul');
|
||||
return locate(getComponent(), findContainer());
|
||||
}
|
||||
|
||||
export function getHeroesList() {
|
||||
return getComponentContainer().all(by.css('li'));
|
||||
}
|
19
aio/content/examples/animations/e2e/src/enter-leave.po.ts
Normal file
19
aio/content/examples/animations/e2e/src/enter-leave.po.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { by } from 'protractor';
|
||||
import { locate } from './util';
|
||||
|
||||
export function getPage() {
|
||||
return by.css('app-hero-list-enter-leave-page');
|
||||
}
|
||||
|
||||
export function getComponent() {
|
||||
return by.css('app-hero-list-enter-leave');
|
||||
}
|
||||
|
||||
export function getComponentContainer() {
|
||||
const findContainer = () => by.css('ul');
|
||||
return locate(getComponent(), findContainer());
|
||||
}
|
||||
|
||||
export function getHeroesList() {
|
||||
return getComponentContainer().all(by.css('li'));
|
||||
}
|
20
aio/content/examples/animations/e2e/src/filter-stagger.po.ts
Normal file
20
aio/content/examples/animations/e2e/src/filter-stagger.po.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { by } from 'protractor';
|
||||
import { locate } from './util';
|
||||
|
||||
export function getPage() {
|
||||
return by.css('app-hero-list-page');
|
||||
}
|
||||
|
||||
export function getComponentContainer() {
|
||||
const findContainer = () => by.css('ul');
|
||||
return locate(getPage(), findContainer());
|
||||
}
|
||||
|
||||
export function getHeroesList() {
|
||||
return getComponentContainer().all(by.css('li'));
|
||||
}
|
||||
|
||||
export function getFormInput() {
|
||||
const formInput = () => by.css('form > input');
|
||||
return locate(getPage(), formInput());
|
||||
}
|
19
aio/content/examples/animations/e2e/src/hero-groups.ts
Normal file
19
aio/content/examples/animations/e2e/src/hero-groups.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { by } from 'protractor';
|
||||
import { locate } from './util';
|
||||
|
||||
export function getPage() {
|
||||
return by.css('app-hero-list-groups-page');
|
||||
}
|
||||
|
||||
export function getComponent() {
|
||||
return by.css('app-hero-list-groups');
|
||||
}
|
||||
|
||||
export function getComponentContainer() {
|
||||
const findContainer = () => by.css('ul');
|
||||
return locate(getComponent(), findContainer());
|
||||
}
|
||||
|
||||
export function getHeroesList() {
|
||||
return getComponentContainer().all(by.css('li'));
|
||||
}
|
25
aio/content/examples/animations/e2e/src/open-close.po.ts
Normal file
25
aio/content/examples/animations/e2e/src/open-close.po.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { by } from 'protractor';
|
||||
import { locate } from './util';
|
||||
|
||||
export function getPage() {
|
||||
return by.css('app-open-close-page');
|
||||
}
|
||||
|
||||
export function getComponent() {
|
||||
return by.css('app-open-close');
|
||||
}
|
||||
|
||||
export function getToggleButton() {
|
||||
const toggleButton = () => by.buttonText('Toggle Open/Close');
|
||||
return locate(getComponent(), toggleButton());
|
||||
}
|
||||
|
||||
export function getLoggingCheckbox() {
|
||||
const loggingCheckbox = () => by.css('section > input[type="checkbox"]');
|
||||
return locate(getPage(), loggingCheckbox());
|
||||
}
|
||||
|
||||
export function getComponentContainer() {
|
||||
const findContainer = () => by.css('div');
|
||||
return locate(getComponent(), findContainer());
|
||||
}
|
20
aio/content/examples/animations/e2e/src/status-slider.po.ts
Normal file
20
aio/content/examples/animations/e2e/src/status-slider.po.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { by } from 'protractor';
|
||||
import { locate } from './util';
|
||||
|
||||
export function getPage() {
|
||||
return by.css('app-status-slider-page');
|
||||
}
|
||||
|
||||
export function getComponent() {
|
||||
return by.css('app-status-slider');
|
||||
}
|
||||
|
||||
export function getToggleButton() {
|
||||
const toggleButton = () => by.buttonText('Toggle Status');
|
||||
return locate(getComponent(), toggleButton());
|
||||
}
|
||||
|
||||
export function getComponentContainer() {
|
||||
const findContainer = () => by.css('div');
|
||||
return locate(getComponent(), findContainer());
|
||||
}
|
25
aio/content/examples/animations/e2e/src/toggle.po.ts
Normal file
25
aio/content/examples/animations/e2e/src/toggle.po.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { by } from 'protractor';
|
||||
import { locate } from './util';
|
||||
|
||||
export function getPage() {
|
||||
return by.css('app-toggle-animations-child-page');
|
||||
}
|
||||
|
||||
export function getComponent() {
|
||||
return by.css('app-open-close-toggle');
|
||||
}
|
||||
|
||||
export function getToggleButton() {
|
||||
const toggleButton = () => by.buttonText('Toggle Open/Closed');
|
||||
return locate(getComponent(), toggleButton());
|
||||
}
|
||||
|
||||
export function getToggleAnimationsButton() {
|
||||
const toggleAnimationsButton = () => by.buttonText('Toggle Animations');
|
||||
return locate(getComponent(), toggleAnimationsButton());
|
||||
}
|
||||
|
||||
export function getComponentContainer() {
|
||||
const findContainer = () => by.css('div');
|
||||
return locate(getComponent()).all(findContainer()).get(0);
|
||||
}
|
19
aio/content/examples/animations/e2e/src/util.ts
Normal file
19
aio/content/examples/animations/e2e/src/util.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Locator, ElementFinder, browser, by, element } from 'protractor';
|
||||
|
||||
/**
|
||||
*
|
||||
* locate(finder1, finder2) => element(finder1).element(finder2).element(finderN);
|
||||
*/
|
||||
export function locate(locator: Locator, ...locators: Locator[]) {
|
||||
return locators.reduce((current: ElementFinder, next: Locator) => {
|
||||
return current.element(next);
|
||||
}, element(locator)) as ElementFinder;
|
||||
}
|
||||
|
||||
export async function sleepFor(time = 1000) {
|
||||
return await browser.sleep(time);
|
||||
}
|
||||
|
||||
export function getLinkById(id: string) {
|
||||
return element(by.css(`a[id=${id}]`));
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
<p>
|
||||
Angular's animations library makes it easy to define and apply animation effects such as page and list transitions.
|
||||
</p>
|
15
aio/content/examples/animations/src/app/about.component.ts
Normal file
15
aio/content/examples/animations/src/app/about.component.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about',
|
||||
templateUrl: './about.component.html',
|
||||
styleUrls: ['./about.component.css']
|
||||
})
|
||||
export class AboutComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
11
aio/content/examples/animations/src/app/animations.1.ts
Normal file
11
aio/content/examples/animations/src/app/animations.1.ts
Normal file
@ -0,0 +1,11 @@
|
||||
// #docregion
|
||||
import { animation, style, animate } from '@angular/animations';
|
||||
|
||||
export const transAnimation = animation([
|
||||
style({
|
||||
height: '{{ height }}',
|
||||
opacity: '{{ opacity }}',
|
||||
backgroundColor: '{{ backgroundColor }}'
|
||||
}),
|
||||
animate('{{ time }}')
|
||||
]);
|
74
aio/content/examples/animations/src/app/animations.ts
Normal file
74
aio/content/examples/animations/src/app/animations.ts
Normal file
@ -0,0 +1,74 @@
|
||||
// #docregion reusable
|
||||
import {
|
||||
animation, trigger, animateChild, group,
|
||||
transition, animate, style, query
|
||||
} from '@angular/animations';
|
||||
|
||||
export const transAnimation = animation([
|
||||
style({
|
||||
height: '{{ height }}',
|
||||
opacity: '{{ opacity }}',
|
||||
backgroundColor: '{{ backgroundColor }}'
|
||||
}),
|
||||
animate('{{ time }}')
|
||||
]);
|
||||
// #enddocregion reusable
|
||||
|
||||
// Routable animations
|
||||
// #docregion route-animations
|
||||
export const slideInAnimation =
|
||||
// #docregion style-view
|
||||
trigger('routeAnimations', [
|
||||
transition('HomePage <=> AboutPage', [
|
||||
style({ position: 'relative' }),
|
||||
query(':enter, :leave', [
|
||||
style({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%'
|
||||
})
|
||||
]),
|
||||
// #enddocregion style-view
|
||||
// #docregion query
|
||||
query(':enter', [
|
||||
style({ left: '-100%'})
|
||||
]),
|
||||
query(':leave', animateChild()),
|
||||
group([
|
||||
query(':leave', [
|
||||
animate('300ms ease-out', style({ left: '100%'}))
|
||||
]),
|
||||
query(':enter', [
|
||||
animate('300ms ease-out', style({ left: '0%'}))
|
||||
])
|
||||
]),
|
||||
query(':enter', animateChild()),
|
||||
]),
|
||||
transition('* <=> FilterPage', [
|
||||
style({ position: 'relative' }),
|
||||
query(':enter, :leave', [
|
||||
style({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%'
|
||||
})
|
||||
]),
|
||||
query(':enter', [
|
||||
style({ left: '-100%'})
|
||||
]),
|
||||
query(':leave', animateChild()),
|
||||
group([
|
||||
query(':leave', [
|
||||
animate('200ms ease-out', style({ left: '100%'}))
|
||||
]),
|
||||
query(':enter', [
|
||||
animate('300ms ease-out', style({ left: '0%'}))
|
||||
])
|
||||
]),
|
||||
query(':enter', animateChild()),
|
||||
])
|
||||
// #enddocregion query
|
||||
]);
|
||||
// #enddocregion route-animations
|
35
aio/content/examples/animations/src/app/app.component.1.ts
Normal file
35
aio/content/examples/animations/src/app/app.component.1.ts
Normal file
@ -0,0 +1,35 @@
|
||||
// #docplaster
|
||||
// #docregion imports
|
||||
import { Component, HostBinding } from '@angular/core';
|
||||
import {
|
||||
trigger,
|
||||
state,
|
||||
style,
|
||||
animate,
|
||||
transition,
|
||||
// ...
|
||||
} from '@angular/animations';
|
||||
|
||||
// #enddocregion imports
|
||||
|
||||
// #docregion decorator, toggle-app-animations
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrls: ['app.component.css'],
|
||||
animations: [
|
||||
// animation triggers go here
|
||||
]
|
||||
})
|
||||
// #enddocregion decorator
|
||||
export class AppComponent {
|
||||
@HostBinding('@.disabled')
|
||||
public animationsDisabled = false;
|
||||
// #enddocregion toggle-app-animations
|
||||
|
||||
toggleAnimations() {
|
||||
this.animationsDisabled = !this.animationsDisabled;
|
||||
}
|
||||
// #docregion toggle-app-animations
|
||||
}
|
||||
// #enddocregion toggle-app-animations
|
@ -0,0 +1,7 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-top: 100px;
|
||||
}
|
21
aio/content/examples/animations/src/app/app.component.html
Normal file
21
aio/content/examples/animations/src/app/app.component.html
Normal file
@ -0,0 +1,21 @@
|
||||
<h1>Animations</h1>
|
||||
|
||||
Toggle All Animations <input type="checkbox" [checked]="!animationsDisabled" (click)="toggleAnimations()"/>
|
||||
|
||||
<nav>
|
||||
<a id="home" routerLink="/home" routerLinkActive="active">Home</a>
|
||||
<a id="about" routerLink="/about" routerLinkActive="active">About</a>
|
||||
<a id="open-close" routerLink="/open-close" routerLinkActive="active">Open/Close</a>
|
||||
<a id="status" routerLink="/status" routerLinkActive="active">Status Slider</a>
|
||||
<a id="toggle" routerLink="/toggle" routerLinkActive="active">Toggle Animations</a>
|
||||
<a id="enter-leave" routerLink="/enter-leave" routerLinkActive="active">Enter/Leave</a>
|
||||
<a id="auto" routerLink="/auto" routerLinkActive="active">Auto Calculation</a>
|
||||
<a id="heroes" routerLink="/heroes" routerLinkActive="active">Filter/Stagger</a>
|
||||
<a id="hero-groups" routerLink="/hero-groups" routerLinkActive="active">Hero Groups</a>
|
||||
</nav>
|
||||
|
||||
<!-- #docregion route-animations-outlet -->
|
||||
<div [@routeAnimations]="prepareRoute(outlet)" >
|
||||
<router-outlet #outlet="outlet"></router-outlet>
|
||||
</div>
|
||||
<!-- #enddocregion route-animations-outlet -->
|
47
aio/content/examples/animations/src/app/app.component.ts
Normal file
47
aio/content/examples/animations/src/app/app.component.ts
Normal file
@ -0,0 +1,47 @@
|
||||
// #docplaster
|
||||
// #docregion imports
|
||||
import { Component, HostBinding } from '@angular/core';
|
||||
import {
|
||||
trigger,
|
||||
state,
|
||||
style,
|
||||
animate,
|
||||
transition,
|
||||
// ...
|
||||
} from '@angular/animations';
|
||||
|
||||
// #enddocregion imports
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { slideInAnimation } from './animations';
|
||||
|
||||
// #docregion decorator, toggle-app-animations, define
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrls: ['app.component.css'],
|
||||
animations: [
|
||||
// #enddocregion decorator
|
||||
slideInAnimation
|
||||
// #docregion decorator
|
||||
// animation triggers go here
|
||||
]
|
||||
})
|
||||
// #enddocregion decorator, define
|
||||
export class AppComponent {
|
||||
@HostBinding('@.disabled')
|
||||
public animationsDisabled = false;
|
||||
// #enddocregion toggle-app-animations
|
||||
|
||||
// #docregion prepare-router-outlet
|
||||
prepareRoute(outlet: RouterOutlet) {
|
||||
return outlet && outlet.activatedRouteData && outlet.activatedRouteData['animation'];
|
||||
}
|
||||
|
||||
// #enddocregion prepare-router-outlet
|
||||
|
||||
toggleAnimations() {
|
||||
this.animationsDisabled = !this.animationsDisabled;
|
||||
}
|
||||
// #docregion toggle-app-animations
|
||||
}
|
||||
// #enddocregion toggle-app-animations
|
13
aio/content/examples/animations/src/app/app.module.1.ts
Normal file
13
aio/content/examples/animations/src/app/app.module.1.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule
|
||||
],
|
||||
declarations: [ ],
|
||||
bootstrap: [ ]
|
||||
})
|
||||
export class AppModule { }
|
@ -1,43 +1,63 @@
|
||||
// #docplaster
|
||||
// #docregion route-animation-data
|
||||
import { NgModule } from '@angular/core';
|
||||
// #docregion animations-module
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
// #enddocregion animations-module
|
||||
|
||||
import { HeroTeamBuilderComponent } from './hero-team-builder.component';
|
||||
import { HeroListBasicComponent } from './hero-list-basic.component';
|
||||
import { HeroListInlineStylesComponent } from './hero-list-inline-styles.component';
|
||||
import { HeroListEnterLeaveComponent } from './hero-list-enter-leave.component';
|
||||
import { HeroListEnterLeaveStatesComponent } from './hero-list-enter-leave-states.component';
|
||||
import { HeroListCombinedTransitionsComponent } from './hero-list-combined-transitions.component';
|
||||
import { HeroListTwowayComponent } from './hero-list-twoway.component';
|
||||
import { HeroListAutoComponent } from './hero-list-auto.component';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { AppComponent } from './app.component';
|
||||
import { OpenCloseComponent } from './open-close.component';
|
||||
import { OpenClosePageComponent } from './open-close-page.component';
|
||||
import { OpenCloseChildComponent } from './open-close.component.4';
|
||||
import { ToggleAnimationsPageComponent } from './toggle-animations-page.component';
|
||||
import { StatusSliderComponent } from './status-slider.component';
|
||||
import { StatusSliderPageComponent } from './status-slider-page.component';
|
||||
import { HeroListPageComponent } from './hero-list-page.component';
|
||||
import { HeroListGroupPageComponent } from './hero-list-group-page.component';
|
||||
import { HeroListGroupsComponent } from './hero-list-groups.component';
|
||||
import { HeroListMultistepComponent } from './hero-list-multistep.component';
|
||||
import { HeroListTimingsComponent } from './hero-list-timings.component';
|
||||
// #docregion animations-module
|
||||
import { HeroListEnterLeavePageComponent } from './hero-list-enter-leave-page.component';
|
||||
import { HeroListEnterLeaveComponent } from './hero-list-enter-leave.component';
|
||||
import { HeroListAutoCalcPageComponent } from './hero-list-auto-page.component';
|
||||
import { HeroListAutoComponent } from './hero-list-auto.component';
|
||||
import { HomeComponent } from './home.component';
|
||||
import { AboutComponent } from './about.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [ BrowserModule, BrowserAnimationsModule ],
|
||||
// ... more stuff ...
|
||||
// #enddocregion animations-module
|
||||
declarations: [
|
||||
HeroTeamBuilderComponent,
|
||||
HeroListBasicComponent,
|
||||
HeroListInlineStylesComponent,
|
||||
HeroListCombinedTransitionsComponent,
|
||||
HeroListTwowayComponent,
|
||||
HeroListEnterLeaveComponent,
|
||||
HeroListEnterLeaveStatesComponent,
|
||||
HeroListAutoComponent,
|
||||
HeroListTimingsComponent,
|
||||
HeroListMultistepComponent,
|
||||
HeroListGroupsComponent
|
||||
imports: [
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
RouterModule.forRoot([
|
||||
{ path: '', pathMatch: 'full', redirectTo: '/enter-leave' },
|
||||
{ path: 'open-close', component: OpenClosePageComponent },
|
||||
{ path: 'status', component: StatusSliderPageComponent },
|
||||
{ path: 'toggle', component: ToggleAnimationsPageComponent },
|
||||
{ path: 'heroes', component: HeroListPageComponent, data: {animation: 'FilterPage'} },
|
||||
{ path: 'hero-groups', component: HeroListGroupPageComponent },
|
||||
{ path: 'enter-leave', component: HeroListEnterLeavePageComponent },
|
||||
{ path: 'auto', component: HeroListAutoCalcPageComponent },
|
||||
{ path: 'home', component: HomeComponent, data: {animation: 'HomePage'} },
|
||||
{ path: 'about', component: AboutComponent, data: {animation: 'AboutPage'} },
|
||||
|
||||
])
|
||||
],
|
||||
bootstrap: [ HeroTeamBuilderComponent ]
|
||||
// #docregion animations-module
|
||||
// #enddocregion route-animation-data
|
||||
declarations: [
|
||||
AppComponent,
|
||||
StatusSliderComponent,
|
||||
OpenCloseComponent,
|
||||
OpenCloseChildComponent,
|
||||
OpenClosePageComponent,
|
||||
StatusSliderPageComponent,
|
||||
ToggleAnimationsPageComponent,
|
||||
HeroListPageComponent,
|
||||
HeroListGroupsComponent,
|
||||
HeroListGroupPageComponent,
|
||||
HeroListEnterLeavePageComponent,
|
||||
HeroListEnterLeaveComponent,
|
||||
HeroListAutoCalcPageComponent,
|
||||
HeroListAutoComponent,
|
||||
HomeComponent,
|
||||
AboutComponent
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
||||
// #enddocregion animations-module
|
||||
|
||||
|
@ -0,0 +1,20 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { HEROES } from './mock-heroes';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero-list-auto-page',
|
||||
template: `
|
||||
<section>
|
||||
<h2>Automatic Calculation</h2>
|
||||
|
||||
<app-hero-list-auto [heroes]="heroes" (remove)="onRemove($event)"></app-hero-list-auto>
|
||||
</section>
|
||||
`
|
||||
})
|
||||
export class HeroListAutoCalcPageComponent {
|
||||
heroes = HEROES.slice();
|
||||
|
||||
onRemove(id: number) {
|
||||
this.heroes = this.heroes.filter(hero => hero.id !== id);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<ul class="heroes">
|
||||
<li *ngFor="let hero of heroes"
|
||||
[@shrinkOut]="'in'" (click)="removeHero(hero.id)">
|
||||
<div class="inner">
|
||||
<span class="badge">{{ hero.id }}</span>
|
||||
<span>{{ hero.name }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
@ -1,6 +1,8 @@
|
||||
import {
|
||||
Component,
|
||||
Input
|
||||
Input,
|
||||
Output,
|
||||
EventEmitter
|
||||
} from '@angular/core';
|
||||
import {
|
||||
trigger,
|
||||
@ -10,38 +12,30 @@ import {
|
||||
transition
|
||||
} from '@angular/animations';
|
||||
|
||||
import { Hero } from './hero.service';
|
||||
import { Hero } from './hero';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero-list-auto',
|
||||
// #docregion template
|
||||
template: `
|
||||
<ul>
|
||||
<li *ngFor="let hero of heroes"
|
||||
[@shrinkOut]="'in'">
|
||||
{{hero.name}}
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
// #enddocregion template
|
||||
styleUrls: ['./hero-list.component.css'],
|
||||
|
||||
/* When the element leaves (transition "in => void" occurs),
|
||||
* get the element's current computed height and animate
|
||||
* it down to 0.
|
||||
*/
|
||||
// #docregion animationdef
|
||||
templateUrl: 'hero-list-auto.component.html',
|
||||
styleUrls: ['./hero-list-page.component.css'],
|
||||
// #docregion auto-calc
|
||||
animations: [
|
||||
trigger('shrinkOut', [
|
||||
state('in', style({height: '*'})),
|
||||
state('in', style({ height: '*' })),
|
||||
transition('* => void', [
|
||||
style({height: '*'}),
|
||||
animate(250, style({height: 0}))
|
||||
style({ height: '*' }),
|
||||
animate(250, style({ height: 0 }))
|
||||
])
|
||||
])
|
||||
]
|
||||
// #enddocregion animationdef
|
||||
// #enddocregion auto-calc
|
||||
})
|
||||
export class HeroListAutoComponent {
|
||||
@Input() heroes: Hero[];
|
||||
|
||||
@Output() remove = new EventEmitter<number>();
|
||||
|
||||
removeHero(id: number) {
|
||||
this.remove.emit(id);
|
||||
}
|
||||
}
|
||||
|
@ -1,70 +0,0 @@
|
||||
// #docplaster
|
||||
// #docregion
|
||||
// #docregion imports
|
||||
import {
|
||||
Component,
|
||||
Input
|
||||
} from '@angular/core';
|
||||
import {
|
||||
trigger,
|
||||
state,
|
||||
style,
|
||||
animate,
|
||||
transition
|
||||
} from '@angular/animations';
|
||||
// #enddocregion imports
|
||||
|
||||
import { Hero } from './hero.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero-list-basic',
|
||||
// #enddocregion
|
||||
/* The click event calls hero.toggleState(), which
|
||||
* causes the state of that hero to switch from
|
||||
* active to inactive or vice versa.
|
||||
*/
|
||||
// #docregion
|
||||
// #docregion template
|
||||
template: `
|
||||
<ul>
|
||||
<li *ngFor="let hero of heroes"
|
||||
[@heroState]="hero.state"
|
||||
(click)="hero.toggleState()">
|
||||
{{hero.name}}
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
// #enddocregion template
|
||||
styleUrls: ['./hero-list.component.css'],
|
||||
// #enddocregion
|
||||
/**
|
||||
* Define two states, "inactive" and "active", and the end
|
||||
* styles that apply whenever the element is in those states.
|
||||
* Then define animations for transitioning between the states,
|
||||
* one in each direction
|
||||
*/
|
||||
// #docregion
|
||||
// #docregion animationdef
|
||||
animations: [
|
||||
trigger('heroState', [
|
||||
// #docregion states
|
||||
state('inactive', style({
|
||||
backgroundColor: '#eee',
|
||||
transform: 'scale(1)'
|
||||
})),
|
||||
state('active', style({
|
||||
backgroundColor: '#cfd8dc',
|
||||
transform: 'scale(1.1)'
|
||||
})),
|
||||
// #enddocregion states
|
||||
// #docregion transitions
|
||||
transition('inactive => active', animate('100ms ease-in')),
|
||||
transition('active => inactive', animate('100ms ease-out'))
|
||||
// #enddocregion transitions
|
||||
])
|
||||
]
|
||||
// #enddocregion animationdef
|
||||
})
|
||||
export class HeroListBasicComponent {
|
||||
@Input() heroes: Hero[];
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
// #docregion
|
||||
// #docregion imports
|
||||
import {
|
||||
Component,
|
||||
Input
|
||||
} from '@angular/core';
|
||||
import {
|
||||
trigger,
|
||||
state,
|
||||
style,
|
||||
animate,
|
||||
transition
|
||||
} from '@angular/animations';
|
||||
// #enddocregion imports
|
||||
|
||||
import { Hero } from './hero.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero-list-combined-transitions',
|
||||
// #docregion template
|
||||
template: `
|
||||
<ul>
|
||||
<li *ngFor="let hero of heroes"
|
||||
[@heroState]="hero.state"
|
||||
(click)="hero.toggleState()">
|
||||
{{hero.name}}
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
// #enddocregion template
|
||||
styleUrls: ['./hero-list.component.css'],
|
||||
/*
|
||||
* Define two states, "inactive" and "active", and the end
|
||||
* styles that apply whenever the element is in those states.
|
||||
* Then define an animated transition between these two
|
||||
* states, in *both* directions.
|
||||
*/
|
||||
// #docregion animationdef
|
||||
animations: [
|
||||
trigger('heroState', [
|
||||
state('inactive', style({
|
||||
backgroundColor: '#eee',
|
||||
transform: 'scale(1)'
|
||||
})),
|
||||
state('active', style({
|
||||
backgroundColor: '#cfd8dc',
|
||||
transform: 'scale(1.1)'
|
||||
})),
|
||||
// #docregion transitions
|
||||
transition('inactive => active, active => inactive',
|
||||
animate('100ms ease-out'))
|
||||
// #enddocregion transitions
|
||||
])
|
||||
]
|
||||
// #enddocregion animationdef
|
||||
})
|
||||
export class HeroListCombinedTransitionsComponent {
|
||||
@Input() heroes: Hero[];
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { HEROES } from './mock-heroes';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero-list-enter-leave-page',
|
||||
template: `
|
||||
<section>
|
||||
<h2>Enter/Leave</h2>
|
||||
|
||||
<app-hero-list-enter-leave [heroes]="heroes" (remove)="onRemove($event)"></app-hero-list-enter-leave>
|
||||
</section>
|
||||
`
|
||||
})
|
||||
export class HeroListEnterLeavePageComponent {
|
||||
heroes = HEROES.slice();
|
||||
|
||||
onRemove(id: number) {
|
||||
this.heroes = this.heroes.filter(hero => hero.id !== id);
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
import {
|
||||
Component,
|
||||
Input
|
||||
} from '@angular/core';
|
||||
import {
|
||||
trigger,
|
||||
state,
|
||||
style,
|
||||
animate,
|
||||
transition
|
||||
} from '@angular/animations';
|
||||
|
||||
import { Hero } from './hero.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero-list-enter-leave-states',
|
||||
// #docregion template
|
||||
template: `
|
||||
<ul>
|
||||
<li *ngFor="let hero of heroes"
|
||||
(click)="hero.toggleState()"
|
||||
[@heroState]="hero.state">
|
||||
{{hero.name}}
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
// #enddocregion template
|
||||
styleUrls: ['./hero-list.component.css'],
|
||||
/* The elements here have two possible states based
|
||||
* on the hero state, "active", or "inactive". We animate
|
||||
* six transitions: Between the two states in both directions,
|
||||
* and between each state and void. With this we can animate
|
||||
* the enter and leave of elements differently based on which
|
||||
* state they are in when they are added and removed.
|
||||
*/
|
||||
// #docregion animationdef
|
||||
animations: [
|
||||
trigger('heroState', [
|
||||
state('inactive', style({transform: 'translateX(0) scale(1)'})),
|
||||
state('active', style({transform: 'translateX(0) scale(1.1)'})),
|
||||
transition('inactive => active', animate('100ms ease-in')),
|
||||
transition('active => inactive', animate('100ms ease-out')),
|
||||
transition('void => inactive', [
|
||||
style({transform: 'translateX(-100%) scale(1)'}),
|
||||
animate(100)
|
||||
]),
|
||||
transition('inactive => void', [
|
||||
animate(100, style({transform: 'translateX(100%) scale(1)'}))
|
||||
]),
|
||||
transition('void => active', [
|
||||
style({transform: 'translateX(0) scale(0)'}),
|
||||
animate(200)
|
||||
]),
|
||||
transition('active => void', [
|
||||
animate(200, style({transform: 'translateX(0) scale(0)'}))
|
||||
])
|
||||
])
|
||||
]
|
||||
// #enddocregion animationdef
|
||||
})
|
||||
export class HeroListEnterLeaveStatesComponent {
|
||||
@Input() heroes: Hero[];
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import {
|
||||
Component,
|
||||
Input
|
||||
Input,
|
||||
Output,
|
||||
EventEmitter
|
||||
} from '@angular/core';
|
||||
import {
|
||||
trigger,
|
||||
@ -10,37 +12,34 @@ import {
|
||||
transition
|
||||
} from '@angular/animations';
|
||||
|
||||
import { Hero } from './hero.service';
|
||||
import { Hero } from './hero';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero-list-enter-leave',
|
||||
// #docregion template
|
||||
template: `
|
||||
<ul>
|
||||
<ul class="heroes">
|
||||
<li *ngFor="let hero of heroes"
|
||||
[@flyInOut]="'in'">
|
||||
{{hero.name}}
|
||||
[@flyInOut]="'in'" (click)="removeHero(hero.id)">
|
||||
<div class="inner">
|
||||
<span class="badge">{{ hero.id }}</span>
|
||||
<span>{{ hero.name }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
// #enddocregion template
|
||||
styleUrls: ['./hero-list.component.css'],
|
||||
/* The element here always has the state "in" when it
|
||||
* is present. We animate two transitions: From void
|
||||
* to in and from in to void, to achieve an animated
|
||||
* enter and leave transition. The element enters from
|
||||
* the left and leaves to the right using translateX.
|
||||
*/
|
||||
styleUrls: ['./hero-list-page.component.css'],
|
||||
// #docregion animationdef
|
||||
animations: [
|
||||
trigger('flyInOut', [
|
||||
state('in', style({transform: 'translateX(0)'})),
|
||||
state('in', style({ transform: 'translateX(0)' })),
|
||||
transition('void => *', [
|
||||
style({transform: 'translateX(-100%)'}),
|
||||
style({ transform: 'translateX(-100%)' }),
|
||||
animate(100)
|
||||
]),
|
||||
transition('* => void', [
|
||||
animate(100, style({transform: 'translateX(100%)'}))
|
||||
animate(100, style({ transform: 'translateX(100%)' }))
|
||||
])
|
||||
])
|
||||
]
|
||||
@ -48,4 +47,10 @@ import { Hero } from './hero.service';
|
||||
})
|
||||
export class HeroListEnterLeaveComponent {
|
||||
@Input() heroes: Hero[];
|
||||
|
||||
@Output() remove = new EventEmitter<number>();
|
||||
|
||||
removeHero(id: number) {
|
||||
this.remove.emit(id);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,20 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { HEROES } from './mock-heroes';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero-list-groups-page',
|
||||
template: `
|
||||
<section>
|
||||
<h2>Hero List Group</h2>
|
||||
|
||||
<app-hero-list-groups [heroes]="heroes" (remove)="onRemove($event)"></app-hero-list-groups>
|
||||
</section>
|
||||
`
|
||||
})
|
||||
export class HeroListGroupPageComponent {
|
||||
heroes = HEROES.slice();
|
||||
|
||||
onRemove(id: number) {
|
||||
this.heroes = this.heroes.filter(hero => hero.id !== id);
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import {
|
||||
Component,
|
||||
Input
|
||||
Input,
|
||||
Output,
|
||||
EventEmitter
|
||||
} from '@angular/core';
|
||||
import {
|
||||
trigger,
|
||||
@ -11,45 +13,31 @@ import {
|
||||
group
|
||||
} from '@angular/animations';
|
||||
|
||||
import { Hero } from './hero.service';
|
||||
import { Hero } from './hero';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero-list-groups',
|
||||
template: `
|
||||
<ul>
|
||||
<ul class="heroes">
|
||||
<li *ngFor="let hero of heroes"
|
||||
[@flyInOut]="'in'">
|
||||
{{hero.name}}
|
||||
[@flyInOut]="'in'" (click)="removeHero(hero.id)">
|
||||
<div class="inner">
|
||||
<span class="badge">{{ hero.id }}</span>
|
||||
<span>{{ hero.name }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
styleUrls: ['./hero-list.component.css'],
|
||||
styles: [`
|
||||
li {
|
||||
padding: 0 !important;
|
||||
text-align: center;
|
||||
}
|
||||
`],
|
||||
/* The element here always has the state "in" when it
|
||||
* is present. We animate two transitions: From void
|
||||
* to in and from in to void, to achieve an animated
|
||||
* enter and leave transition.
|
||||
*
|
||||
* The transitions have *parallel group* that allow
|
||||
* animating several properties at the same time but
|
||||
* with different timing configurations. On enter
|
||||
* (void => *) we start the opacity animation 0.1s
|
||||
* earlier than the translation/width animation.
|
||||
* On leave (* => void) we do the opposite -
|
||||
* the translation/width animation begins immediately
|
||||
* and the opacity animation 0.1s later.
|
||||
*/
|
||||
styleUrls: ['./hero-list-page.component.css'],
|
||||
// #docregion animationdef
|
||||
animations: [
|
||||
trigger('flyInOut', [
|
||||
state('in', style({width: 120, transform: 'translateX(0)', opacity: 1})),
|
||||
state('in', style({
|
||||
width: 120,
|
||||
transform: 'translateX(0)', opacity: 1
|
||||
})),
|
||||
transition('void => *', [
|
||||
style({width: 10, transform: 'translateX(50px)', opacity: 0}),
|
||||
style({ width: 10, transform: 'translateX(50px)', opacity: 0 }),
|
||||
group([
|
||||
animate('0.3s 0.1s ease', style({
|
||||
transform: 'translateX(0)',
|
||||
@ -77,4 +65,10 @@ import { Hero } from './hero.service';
|
||||
})
|
||||
export class HeroListGroupsComponent {
|
||||
@Input() heroes: Hero[];
|
||||
|
||||
@Output() remove = new EventEmitter<number>();
|
||||
|
||||
removeHero(id: number) {
|
||||
this.remove.emit(id);
|
||||
}
|
||||
}
|
||||
|
@ -1,60 +0,0 @@
|
||||
// #docregion
|
||||
// #docregion imports
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
trigger,
|
||||
style,
|
||||
animate,
|
||||
transition
|
||||
} from '@angular/animations';
|
||||
// #enddocregion imports
|
||||
|
||||
import { Hero } from './hero.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero-list-inline-styles',
|
||||
// #docregion template
|
||||
template: `
|
||||
<ul>
|
||||
<li *ngFor="let hero of heroes"
|
||||
[@heroState]="hero.state"
|
||||
(click)="hero.toggleState()">
|
||||
{{hero.name}}
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
// #enddocregion template
|
||||
styleUrls: ['./hero-list.component.css'],
|
||||
/**
|
||||
* Define two states, "inactive" and "active", and the end
|
||||
* styles that apply whenever the element is in those states.
|
||||
* Then define an animation for the inactive => active transition.
|
||||
* This animation has no end styles, but only styles that are
|
||||
* defined inline inside the transition and thus are only kept
|
||||
* as long as the animation is running.
|
||||
*/
|
||||
// #docregion animationdef
|
||||
animations: [
|
||||
trigger('heroState', [
|
||||
// #docregion transitions
|
||||
transition('inactive => active', [
|
||||
style({
|
||||
backgroundColor: '#cfd8dc',
|
||||
transform: 'scale(1.3)'
|
||||
}),
|
||||
animate('80ms ease-in', style({
|
||||
backgroundColor: '#eee',
|
||||
transform: 'scale(1)'
|
||||
}))
|
||||
]),
|
||||
// #enddocregion transitions
|
||||
])
|
||||
]
|
||||
// #enddocregion animationdef
|
||||
})
|
||||
export class HeroListInlineStylesComponent {
|
||||
@Input() heroes: Hero[];
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
trigger,
|
||||
state,
|
||||
style,
|
||||
animate,
|
||||
transition,
|
||||
keyframes,
|
||||
AnimationEvent
|
||||
} from '@angular/animations';
|
||||
|
||||
import { Hero } from './hero.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero-list-multistep',
|
||||
// #docregion template
|
||||
template: `
|
||||
<ul>
|
||||
<li *ngFor="let hero of heroes"
|
||||
(@flyInOut.start)="animationStarted($event)"
|
||||
(@flyInOut.done)="animationDone($event)"
|
||||
[@flyInOut]="'in'">
|
||||
{{hero.name}}
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
// #enddocregion template
|
||||
styleUrls: ['./hero-list.component.css'],
|
||||
/* The element here always has the state "in" when it
|
||||
* is present. We animate two transitions: From void
|
||||
* to in and from in to void, to achieve an animated
|
||||
* enter and leave transition. Each transition is
|
||||
* defined in terms of multiple keyframes, to give it
|
||||
* a bounce effect.
|
||||
*/
|
||||
// #docregion animationdef
|
||||
animations: [
|
||||
trigger('flyInOut', [
|
||||
state('in', style({transform: 'translateX(0)'})),
|
||||
transition('void => *', [
|
||||
animate(300, keyframes([
|
||||
style({opacity: 0, transform: 'translateX(-100%)', offset: 0}),
|
||||
style({opacity: 1, transform: 'translateX(15px)', offset: 0.3}),
|
||||
style({opacity: 1, transform: 'translateX(0)', offset: 1.0})
|
||||
]))
|
||||
]),
|
||||
transition('* => void', [
|
||||
animate(300, keyframes([
|
||||
style({opacity: 1, transform: 'translateX(0)', offset: 0}),
|
||||
style({opacity: 1, transform: 'translateX(-15px)', offset: 0.7}),
|
||||
style({opacity: 0, transform: 'translateX(100%)', offset: 1.0})
|
||||
]))
|
||||
])
|
||||
])
|
||||
]
|
||||
// #enddocregion animationdef
|
||||
})
|
||||
export class HeroListMultistepComponent {
|
||||
@Input() heroes: Hero[];
|
||||
|
||||
animationStarted(event: AnimationEvent) {
|
||||
console.warn('Animation started: ', event);
|
||||
}
|
||||
|
||||
animationDone(event: AnimationEvent) {
|
||||
console.warn('Animation done: ', event);
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
.heroes {
|
||||
margin: 0 0 2em 0;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
width: 15em;
|
||||
}
|
||||
|
||||
.heroes li {
|
||||
position: relative;
|
||||
height: 2.3em;
|
||||
overflow:hidden;
|
||||
margin: .5em;
|
||||
}
|
||||
|
||||
.heroes li > .inner {
|
||||
cursor: pointer;
|
||||
background-color: #EEE;
|
||||
padding: .3em 0;
|
||||
height: 1.6em;
|
||||
border-radius: 4px;
|
||||
width: 19em;
|
||||
}
|
||||
|
||||
.heroes li:hover > .inner {
|
||||
color: #607D8B;
|
||||
background-color: #DDD;
|
||||
transform: translateX(.1em);
|
||||
}
|
||||
|
||||
.heroes a {
|
||||
color: #888;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.heroes a:hover {
|
||||
color:#607D8B;
|
||||
}
|
||||
|
||||
.heroes .badge {
|
||||
display: inline-block;
|
||||
font-size: small;
|
||||
color: white;
|
||||
padding: 0.8em 0.7em 0 0.7em;
|
||||
background-color: #607D8B;
|
||||
line-height: 1em;
|
||||
position: relative;
|
||||
left: -1px;
|
||||
top: -4px;
|
||||
height: 1.8em;
|
||||
min-width: 16px;
|
||||
text-align: right;
|
||||
margin-right: .8em;
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: #eee;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
cursor: hand;
|
||||
font-family: Arial;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #cfd8dc;
|
||||
}
|
||||
|
||||
button.delete {
|
||||
position: relative;
|
||||
left: 24em;
|
||||
top: -32px;
|
||||
background-color: gray !important;
|
||||
color: white;
|
||||
display: inherit;
|
||||
padding: 5px 8px;
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 100%;
|
||||
margin-bottom: 2px;
|
||||
width: 11em;
|
||||
}
|
||||
|
||||
.heroes input {
|
||||
position: relative;
|
||||
top: -3px;
|
||||
width: 12em;
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
<!-- #docplaster -->
|
||||
<h2>Filter/Stagger</h2>
|
||||
|
||||
<form>
|
||||
<input #criteria (input)="updateCriteria(criteria.value)" placeholder="Search Heroes" />
|
||||
</form>
|
||||
|
||||
<!-- #docregion filter-animations -->
|
||||
<ul class="heroes" [@filterAnimation]="heroTotal">
|
||||
<!-- #enddocregion filter-animations -->
|
||||
<li *ngFor="let hero of heroes" class="hero">
|
||||
<div class="inner">
|
||||
<span class="badge">{{ hero.id }}</span>
|
||||
<span>{{ hero.name }}</span>
|
||||
</div>
|
||||
</li>
|
||||
<!-- #docregion filter-animations -->
|
||||
</ul>
|
||||
<!-- #enddocregion filter-animations -->
|
@ -0,0 +1,81 @@
|
||||
// #docplaster
|
||||
import { Component, HostBinding, OnInit } from '@angular/core';
|
||||
import { trigger, transition, animate, style, query, stagger } from '@angular/animations';
|
||||
import { HEROES } from './mock-heroes';
|
||||
|
||||
// #docregion filter-animations
|
||||
@Component({
|
||||
// #enddocregion filter-animations
|
||||
selector: 'app-hero-list-page',
|
||||
templateUrl: 'hero-list-page.component.html',
|
||||
styleUrls: ['hero-list-page.component.css'],
|
||||
// #docregion page-animations, filter-animations
|
||||
animations: [
|
||||
// #enddocregion filter-animations
|
||||
trigger('pageAnimations', [
|
||||
transition(':enter', [
|
||||
query('.hero, form', [
|
||||
style({opacity: 0, transform: 'translateY(-100px)'}),
|
||||
stagger(-30, [
|
||||
animate('500ms cubic-bezier(0.35, 0, 0.25, 1)', style({ opacity: 1, transform: 'none' }))
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
// #enddocregion page-animations
|
||||
// #docregion increment
|
||||
// #docregion filter-animations
|
||||
trigger('filterAnimation', [
|
||||
transition(':enter, * => 0, * => -1', []),
|
||||
transition(':increment', [
|
||||
query(':enter', [
|
||||
style({ opacity: 0, width: '0px' }),
|
||||
stagger(50, [
|
||||
animate('300ms ease-out', style({ opacity: 1, width: '*' })),
|
||||
]),
|
||||
], { optional: true })
|
||||
]),
|
||||
transition(':decrement', [
|
||||
query(':leave', [
|
||||
stagger(50, [
|
||||
animate('300ms ease-out', style({ opacity: 0, width: '0px' })),
|
||||
]),
|
||||
])
|
||||
]),
|
||||
]),
|
||||
// #enddocregion increment
|
||||
// #docregion page-animations
|
||||
]
|
||||
})
|
||||
export class HeroListPageComponent implements OnInit {
|
||||
// #enddocregion filter-animations
|
||||
@HostBinding('@pageAnimations')
|
||||
public animatePage = true;
|
||||
|
||||
_heroes = [];
|
||||
// #docregion filter-animations
|
||||
heroTotal = -1;
|
||||
// #enddocregion filter-animations
|
||||
get heroes() {
|
||||
return this._heroes;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._heroes = HEROES;
|
||||
}
|
||||
|
||||
updateCriteria(criteria: string) {
|
||||
criteria = criteria ? criteria.trim() : '';
|
||||
|
||||
this._heroes = HEROES.filter(hero => hero.name.toLowerCase().includes(criteria.toLowerCase()));
|
||||
const newTotal = this.heroes.length;
|
||||
|
||||
if (this.heroTotal !== newTotal) {
|
||||
this.heroTotal = newTotal;
|
||||
} else if (!criteria) {
|
||||
this.heroTotal = -1;
|
||||
}
|
||||
}
|
||||
// #docregion filter-animations
|
||||
}
|
||||
// #enddocregion filter-animations
|
@ -1,58 +0,0 @@
|
||||
import {
|
||||
Component,
|
||||
Input
|
||||
} from '@angular/core';
|
||||
import {
|
||||
trigger,
|
||||
state,
|
||||
style,
|
||||
animate,
|
||||
transition
|
||||
} from '@angular/animations';
|
||||
|
||||
import { Hero } from './hero.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero-list-timings',
|
||||
template: `
|
||||
<ul>
|
||||
<li *ngFor="let hero of heroes"
|
||||
[@flyInOut]="'in'"
|
||||
(click)="hero.toggleState()">
|
||||
{{hero.name}}
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
styleUrls: ['./hero-list.component.css'],
|
||||
/* The element here always has the state "in" when it
|
||||
* is present. We animate two transitions: From void
|
||||
* to in and from in to void, to achieve an animated
|
||||
* enter and leave transition. The element enters from
|
||||
* the left and leaves to the right using translateX,
|
||||
* and fades in/out using opacity. We use different easings
|
||||
* for enter and leave.
|
||||
*/
|
||||
// #docregion animationdef
|
||||
animations: [
|
||||
trigger('flyInOut', [
|
||||
state('in', style({opacity: 1, transform: 'translateX(0)'})),
|
||||
transition('void => *', [
|
||||
style({
|
||||
opacity: 0,
|
||||
transform: 'translateX(-100%)'
|
||||
}),
|
||||
animate('0.2s ease-in')
|
||||
]),
|
||||
transition('* => void', [
|
||||
animate('0.2s 0.1s ease-out', style({
|
||||
opacity: 0,
|
||||
transform: 'translateX(100%)'
|
||||
}))
|
||||
])
|
||||
])
|
||||
]
|
||||
// #enddocregion animationdef
|
||||
})
|
||||
export class HeroListTimingsComponent {
|
||||
@Input() heroes: Hero[];
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
// #docregion
|
||||
// #docregion imports
|
||||
import {
|
||||
Component,
|
||||
Input
|
||||
} from '@angular/core';
|
||||
import {
|
||||
trigger,
|
||||
state,
|
||||
style,
|
||||
animate,
|
||||
transition
|
||||
} from '@angular/animations';
|
||||
// #enddocregion imports
|
||||
|
||||
import { Hero } from './hero.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero-list-twoway',
|
||||
// #docregion template
|
||||
template: `
|
||||
<ul>
|
||||
<li *ngFor="let hero of heroes"
|
||||
[@heroState]="hero.state"
|
||||
(click)="hero.toggleState()">
|
||||
{{hero.name}}
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
// #enddocregion template
|
||||
styleUrls: ['./hero-list.component.css'],
|
||||
/*
|
||||
* Define two states, "inactive" and "active", and the end
|
||||
* styles that apply whenever the element is in those states.
|
||||
* Then define an animated transition between these two
|
||||
* states, in *both* directions.
|
||||
*/
|
||||
// #docregion animationdef
|
||||
animations: [
|
||||
trigger('heroState', [
|
||||
state('inactive', style({
|
||||
backgroundColor: '#eee',
|
||||
transform: 'scale(1)'
|
||||
})),
|
||||
state('active', style({
|
||||
backgroundColor: '#cfd8dc',
|
||||
transform: 'scale(1.1)'
|
||||
})),
|
||||
// #docregion transitions
|
||||
transition('inactive <=> active', animate('100ms ease-out'))
|
||||
// #enddocregion transitions
|
||||
])
|
||||
]
|
||||
// #enddocregion animationdef
|
||||
})
|
||||
export class HeroListTwowayComponent {
|
||||
@Input() heroes: Hero[];
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { Hero, HeroService } from './hero.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: `
|
||||
<div class="buttons">
|
||||
<button [disabled]="!heroService.canAdd()" (click)="heroService.addInactive()">Add inactive hero</button>
|
||||
<button [disabled]="!heroService.canAdd()" (click)="heroService.addActive()">Add active hero</button>
|
||||
<button [disabled]="!heroService.canRemove()" (click)="heroService.remove()">Remove hero</button>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h4>Basic State</h4>
|
||||
<p>Switch between active/inactive on click.</p>
|
||||
<app-hero-list-basic [heroes]="heroes"></app-hero-list-basic>
|
||||
</div>
|
||||
<div class="column">
|
||||
<h4>Styles inline in transitions</h4>
|
||||
<p>Animated effect on click, no persistend end styles.</p>
|
||||
<app-hero-list-inline-styles [heroes]="heroes"></app-hero-list-inline-styles>
|
||||
</div>
|
||||
<div class="column">
|
||||
<h4>Combined transition syntax</h4>
|
||||
<p>Switch between active/inactive on click. Define just one transition used in both directions.</p>
|
||||
<app-hero-list-combined-transitions [heroes]="heroes"></app-hero-list-combined-transitions>
|
||||
</div>
|
||||
<div class="column">
|
||||
<h4>Two-way transition syntax</h4>
|
||||
<p>Switch between active/inactive on click. Define just one transition used in both directions using the <=> syntax.</p>
|
||||
<app-hero-list-twoway [heroes]="heroes"></app-hero-list-twoway>
|
||||
</div>
|
||||
<div class="column">
|
||||
<h4>Enter & Leave</h4>
|
||||
<p>Enter and leave animations using the void state.</p>
|
||||
<app-hero-list-enter-leave [heroes]="heroes"></app-hero-list-enter-leave>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h4>Enter & Leave & States</h4>
|
||||
<p>
|
||||
Enter and leave animations combined with active/inactive state animations.
|
||||
Different enter and leave transitions depending on state.
|
||||
</p>
|
||||
<app-hero-list-enter-leave-states [heroes]="heroes"></app-hero-list-enter-leave-states>
|
||||
</div>
|
||||
<div class="column">
|
||||
<h4>Auto Style Calc</h4>
|
||||
<p>Leave animation from the current computed height using the auto-style value *.</p>
|
||||
<app-hero-list-auto [heroes]="heroes"></app-hero-list-auto>
|
||||
</div>
|
||||
<div class="column">
|
||||
<h4>Different Timings</h4>
|
||||
<p>Enter and leave animations with different easings, ease-in for enter, ease-out for leave.</p>
|
||||
<app-hero-list-timings [heroes]="heroes"></app-hero-list-timings>
|
||||
</div>
|
||||
<div class="column">
|
||||
<h4>Multiple Keyframes</h4>
|
||||
<p>Enter and leave animations with three keyframes in each, to give the transition some bounce.</p>
|
||||
<app-hero-list-multistep [heroes]="heroes"></app-hero-list-multistep>
|
||||
</div>
|
||||
<div class="column">
|
||||
<h4>Parallel Groups</h4>
|
||||
<p>Enter and leave animations with multiple properties animated in parallel with different timings.</p>
|
||||
<app-hero-list-groups [heroes]="heroes"></app-hero-list-groups>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.buttons {
|
||||
text-align: center;
|
||||
}
|
||||
button {
|
||||
padding: 1.5em 3em;
|
||||
}
|
||||
.columns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.column {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
}
|
||||
.column p {
|
||||
min-height: 6em;
|
||||
}
|
||||
`],
|
||||
providers: [HeroService]
|
||||
})
|
||||
export class HeroTeamBuilderComponent {
|
||||
heroes: Hero[];
|
||||
|
||||
constructor(private heroService: HeroService) {
|
||||
this.heroes = heroService.heroes;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user