Compare commits
407 Commits
4.0.0-rc.1
...
4.0.2
Author | SHA1 | Date | |
---|---|---|---|
14a2d1a6f7 | |||
61e089931f | |||
cbf30cb101 | |||
2f41b52e78 | |||
dcf60da16d | |||
9c9f409364 | |||
b44b983c1f | |||
978f80985c | |||
c10e50cf38 | |||
168a2eb5bf | |||
c9c7acd484 | |||
5b99533315 | |||
037805b741 | |||
0861fda69c | |||
feae7b6059 | |||
e4277a0869 | |||
1864ccb3dd | |||
6b79ab5abe | |||
53c12a84dc | |||
ca665303f4 | |||
0fe4985756 | |||
74087cb39d | |||
902bb2f026 | |||
23bf34853c | |||
8c4b963927 | |||
bfa4f70204 | |||
d481f6d150 | |||
fd6114561b | |||
c82851172e | |||
75478b2078 | |||
cdbb3dbd2a | |||
e72124c888 | |||
b8c0a97e35 | |||
5597fd3180 | |||
a88413f871 | |||
aa116524e6 | |||
4a5ad7ba30 | |||
d74e4d0633 | |||
a2c2b87aa3 | |||
8f4ea3e4b8 | |||
bf25e94f19 | |||
7983414e6a | |||
8248eba3e2 | |||
a65487528f | |||
426b3a19b7 | |||
2360676a7b | |||
ce3e03ff1a | |||
858c11cf7b | |||
95afaf495b | |||
d92930e975 | |||
c2892dada3 | |||
b2b1195534 | |||
e1b09e3bcc | |||
c65b75443e | |||
db0dca3fc1 | |||
7a715b2403 | |||
1ba296644d | |||
b800a0c824 | |||
0dda01e37c | |||
c8ab5cb0c5 | |||
92084f2b6a | |||
08f2f08d74 | |||
376088da70 | |||
73808dd38b | |||
ee03418b10 | |||
da700d1842 | |||
de87c47dd9 | |||
1060805a1f | |||
08d86751b9 | |||
9319b5f329 | |||
26f6bd4d3b | |||
edb2571a59 | |||
98cb974796 | |||
8b414222aa | |||
ea49a95bd9 | |||
a50d79df47 | |||
64285a2171 | |||
941f194a83 | |||
8b4edcc7ad | |||
c58499786c | |||
1bcbcfd56f | |||
90d2518d9a | |||
7354949763 | |||
5efc86069f | |||
97149f9424 | |||
bac265fdc2 | |||
e59e5e24b9 | |||
9e5d4781cb | |||
fc1f6efe0d | |||
c9710d4fb5 | |||
cf16f3b0dd | |||
a9e91115bf | |||
90f699fdcf | |||
fd7b855cfc | |||
20aab64c65 | |||
2eb027a793 | |||
b0a7bc77ee | |||
4e10faf1eb | |||
a0c6d44e18 | |||
9bc998c7a1 | |||
1a0c6d89b1 | |||
8c12374c4c | |||
45e2126273 | |||
41497b052d | |||
068cad1c1b | |||
31ef92fc36 | |||
b4081e3713 | |||
c8b4a33a7f | |||
a13ddf2e8a | |||
64beae9527 | |||
15a082c74e | |||
fbccd5cd38 | |||
1e8b132ade | |||
431eb309f3 | |||
8e6995c91e | |||
0759911431 | |||
1d7693c1e1 | |||
16e0423085 | |||
c2ffb6bfcd | |||
2489e4ba1b | |||
94da80148e | |||
764e90f9bb | |||
9bf2fb4a74 | |||
de3d2eeeba | |||
a805d00256 | |||
61135bc842 | |||
fa36ffda14 | |||
d3eda7a5b5 | |||
a755b715ed | |||
a9d5de0e56 | |||
f634c62cb3 | |||
f8c075ae27 | |||
aeb99645bb | |||
0d3e314df0 | |||
28ce68a13d | |||
80075afe8a | |||
bcc29ffdd1 | |||
0c43535ccc | |||
49829b4a4d | |||
5c5c2ae405 | |||
6e9264a79c | |||
a6fb78ee3c | |||
d0bc83ca27 | |||
b5b2fed54d | |||
4cef5dddc6 | |||
f92591054b | |||
2a0e55ffb5 | |||
9429032da1 | |||
791534f2f4 | |||
604546c287 | |||
c66437fc13 | |||
8415910375 | |||
5486e5417b | |||
2d78c8cc05 | |||
52bed7f9b3 | |||
7fb45283df | |||
b7212f5afe | |||
4e25601c4d | |||
add7829cb7 | |||
a52184bdda | |||
410aa33005 | |||
0ab49d4cec | |||
994089d36b | |||
d2fbbb44ae | |||
77fd91d615 | |||
492153a986 | |||
c0e05e6f03 | |||
73a46205a0 | |||
992aa17361 | |||
a4076c70cc | |||
52bbc9baf4 | |||
26d4ce29e8 | |||
41f61b0b5b | |||
f92d644c95 | |||
923d0c56e7 | |||
dd36d413ba | |||
013d806b79 | |||
6e98757665 | |||
313158132d | |||
6772c913c7 | |||
b11d0119ac | |||
637a489996 | |||
c2bd357825 | |||
4870f910d6 | |||
c95a3048ce | |||
0e6eb6d719 | |||
09574fc285 | |||
4347cb2119 | |||
f600d4e9e4 | |||
eaa04354d5 | |||
3f7cfde476 | |||
62d5543b01 | |||
f1b33ab7b1 | |||
029d0f25e5 | |||
3f38c6fdcd | |||
80112a9ea1 | |||
795638e18b | |||
322bf7a0d5 | |||
cd981499f9 | |||
c439742a54 | |||
a3e32fb7e1 | |||
1171f91a80 | |||
8e2c8b3e4d | |||
b00fe20afd | |||
36ce0afff6 | |||
5c0ea20bd0 | |||
49764a5bff | |||
4f7d62adac | |||
c10c060d20 | |||
5fe2d8fd80 | |||
5c34066058 | |||
50ab06e29d | |||
06fc42bc44 | |||
959a03a61f | |||
3b1956bbf2 | |||
3c15916e17 | |||
ff60c041f6 | |||
13686bb518 | |||
f093501501 | |||
80649ea03c | |||
778f7d6f33 | |||
2c5a671341 | |||
0aad270267 | |||
221899a930 | |||
060a2d11e5 | |||
abbbb4d52c | |||
ccd38dd54d | |||
cdc882bd36 | |||
1c1085b140 | |||
914797a8ff | |||
ab40fcb068 | |||
1847550ad1 | |||
6497633529 | |||
8850098ea4 | |||
eedca09d73 | |||
7b6dbf0952 | |||
e6c81d2a42 | |||
498a95148b | |||
21a18d6ceb | |||
195b863ea4 | |||
75147ff008 | |||
018e5c979b | |||
e7dab7e6c1 | |||
26efa3a25c | |||
893652a813 | |||
6559425b07 | |||
df914ef4bf | |||
4e1cf5b41a | |||
0c5f893f6e | |||
17f5f3b32c | |||
3bb59902f7 | |||
b804a488c5 | |||
cbde75e77b | |||
413e11fac2 | |||
0e2dd76c3b | |||
ff71eff157 | |||
fa1920a02b | |||
71cd2957f7 | |||
6c8638cf01 | |||
8b5c6b2732 | |||
601494734c | |||
1aebea52e1 | |||
920b3d259d | |||
fce55d87d2 | |||
53d62fa7d0 | |||
a69afeb614 | |||
5f9fb911f7 | |||
d17d4a3b54 | |||
814dc107d9 | |||
bebedfed24 | |||
2b44854885 | |||
b74ab83d2c | |||
da8ea350b2 | |||
1efd508217 | |||
58dd4673cd | |||
72563b61fb | |||
5aed1e36b8 | |||
8573e36574 | |||
3e51a19983 | |||
5ad5301a3e | |||
174d4c8ef7 | |||
4ca772eea3 | |||
093d69f0f0 | |||
b4ec80b21d | |||
a4476654aa | |||
4210d2b4b1 | |||
aa30c50144 | |||
fd34a58e13 | |||
e40f81b564 | |||
5ab2e28703 | |||
a3a7cf2090 | |||
d9d9d9de6f | |||
7e67f37fc4 | |||
d275667da0 | |||
03a5fd01c9 | |||
d28ea80db8 | |||
0e9277b4c3 | |||
2796790c7d | |||
028b274750 | |||
3ed1f64d43 | |||
96f11dad18 | |||
060d02eb82 | |||
951e653b0c | |||
37348989f0 | |||
c5644e5a0d | |||
c8d87a936b | |||
6b8413f7b3 | |||
bc831ff4a4 | |||
8a8d4fe24f | |||
9df9bdc0f5 | |||
c8ead9bcd0 | |||
115164033b | |||
794f8f4e6a | |||
ad3b44aef7 | |||
5df998d086 | |||
1cff1250ba | |||
07122f0ad9 | |||
ebd446397a | |||
55189b1b85 | |||
b017fbe48e | |||
0d6aa0caed | |||
b70c881c00 | |||
0d63e2a586 | |||
d83b7ba4c0 | |||
46d6e8d191 | |||
61ef756ef2 | |||
3529813ca0 | |||
b81693b30c | |||
5815983178 | |||
fe962f6de7 | |||
04e14589c4 | |||
d6c1ccaf14 | |||
4abd6f333c | |||
dd50922747 | |||
66cc88c8a8 | |||
b44bc9c022 | |||
b7e76cc2e1 | |||
3651d8d673 | |||
ba4b6f58d9 | |||
6cd3326b55 | |||
6bc6482765 | |||
f2adb2900d | |||
8343fb7740 | |||
84a65cf788 | |||
b0e0839075 | |||
4fe0b90948 | |||
928c5657c8 | |||
728fe472f8 | |||
32990307fe | |||
00fdcf4e58 | |||
ab0db66bf7 | |||
8757656508 | |||
01ff427685 | |||
2ebfa2ff31 | |||
b09ee424bf | |||
b8321e2f7d | |||
b8f0c3dc7b | |||
91fe3aadbc | |||
a24e652f2b | |||
207298cd3a | |||
62eafa4eec | |||
4626ca2bff | |||
38bb744008 | |||
dca83ec738 | |||
71e22b8d11 | |||
371dc4744c | |||
4767f107fb | |||
5ae4b77d8b | |||
887d32a9bf | |||
3883b736c0 | |||
c2e672cd1c | |||
93c0ab7131 | |||
e6e8123bdd | |||
49aa50886a | |||
180b705227 | |||
0f94b93c81 | |||
de57b2d9fd | |||
9560ad81b9 | |||
3168ef75da | |||
b6e6fc1724 | |||
dd499010be | |||
d2803da8b6 | |||
a6996a9cdd | |||
6bae7378b1 | |||
7a66a4115b | |||
79fc1e3959 | |||
9402df92de | |||
47bdc2b0b7 | |||
49b462e815 | |||
5753de50f0 | |||
d1182af1a4 | |||
bc9e1debf2 | |||
fc9e6b2a0a | |||
968995a4c6 | |||
5ba55b0e04 | |||
126fda2613 | |||
e58cb7ba08 | |||
5caab71f7d | |||
d2e42567a6 | |||
14d37fe052 | |||
f356041f09 | |||
77682a3397 | |||
213e210a93 | |||
c868457e21 | |||
f32f4de95c | |||
26ed2621a3 | |||
c7d1b3664b |
8
.gitignore
vendored
8
.gitignore
vendored
@ -17,16 +17,12 @@ modules/.vscode
|
||||
# Don't check in secret files
|
||||
*secret.js
|
||||
|
||||
# Ignore npm debug log
|
||||
# Ignore npm/yarn debug log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# build-analytics
|
||||
.build-analytics
|
||||
|
||||
# rollup-test output
|
||||
/modules/rollup-test/dist/
|
||||
|
||||
# angular.io
|
||||
/aio/node_modules
|
||||
/aio/src/content/docs
|
||||
/aio/dist
|
||||
|
@ -10,6 +10,7 @@
|
||||
# chuckjaz - Chuck Jazdzewski
|
||||
# gkalpak - George Kalpakas
|
||||
# IgorMinar - Igor Minar
|
||||
# jasonaden - Jason Aden
|
||||
# kara - Kara Erickson
|
||||
# matsko - Matias Niemelä
|
||||
# mhevery - Misko Hevery
|
||||
@ -19,6 +20,8 @@
|
||||
# tbosch - Tobias Bosch
|
||||
# vicb - Victor Berchet
|
||||
# vikerman - Vikram Subramanian
|
||||
# wardbell - Ward Bell
|
||||
# tinayuangao - Tina Gao
|
||||
|
||||
version: 2
|
||||
|
||||
@ -39,6 +42,7 @@ groups:
|
||||
- "aio/*"
|
||||
- "integration/*"
|
||||
- "modules/*"
|
||||
- "packages/*"
|
||||
- "tools/*"
|
||||
users:
|
||||
- IgorMinar
|
||||
@ -67,7 +71,9 @@ groups:
|
||||
- "aio/*"
|
||||
users:
|
||||
- IgorMinar #primary
|
||||
- mhevery
|
||||
- alexeagle
|
||||
- jasonaden
|
||||
- mhevery #fallback
|
||||
|
||||
integration:
|
||||
conditions:
|
||||
@ -84,7 +90,7 @@ groups:
|
||||
core:
|
||||
conditions:
|
||||
files:
|
||||
- "modules/@angular/core/*"
|
||||
- "packages/core/*"
|
||||
users:
|
||||
- tbosch #primary
|
||||
- mhevery
|
||||
@ -94,7 +100,7 @@ groups:
|
||||
compiler/animations:
|
||||
conditions:
|
||||
files:
|
||||
- "modules/@angular/compiler/src/animation/*"
|
||||
- "packages/compiler/src/animation/*"
|
||||
users:
|
||||
- matsko #primary
|
||||
- tbosch
|
||||
@ -104,7 +110,7 @@ groups:
|
||||
compiler/i18n:
|
||||
conditions:
|
||||
files:
|
||||
- "modules/@angular/compiler/src/i18n/*"
|
||||
- "packages/compiler/src/i18n/*"
|
||||
users:
|
||||
- vicb #primary
|
||||
- tbosch
|
||||
@ -114,7 +120,7 @@ groups:
|
||||
compiler:
|
||||
conditions:
|
||||
files:
|
||||
- "modules/@angular/compiler/*"
|
||||
- "packages/compiler/*"
|
||||
users:
|
||||
- tbosch #primary
|
||||
- vicb
|
||||
@ -126,7 +132,7 @@ groups:
|
||||
conditions:
|
||||
files:
|
||||
- "tools/@angular/tsc-wrapped/*"
|
||||
- "modules/@angular/compiler-cli/*"
|
||||
- "packages/compiler-cli/*"
|
||||
users:
|
||||
- alexeagle
|
||||
- chuckjaz
|
||||
@ -137,7 +143,7 @@ groups:
|
||||
common:
|
||||
conditions:
|
||||
files:
|
||||
- "modules/@angular/common/*"
|
||||
- "packages/common/*"
|
||||
users:
|
||||
- pkozlowski-opensource #primary
|
||||
- vicb
|
||||
@ -147,17 +153,17 @@ groups:
|
||||
forms:
|
||||
conditions:
|
||||
files:
|
||||
- "modules/@angular/forms/*"
|
||||
- "packages/forms/*"
|
||||
users:
|
||||
- kara #primary
|
||||
# needs secondary
|
||||
- tinayuangao #secondary
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
http:
|
||||
conditions:
|
||||
files:
|
||||
- "modules/@angular/http/*"
|
||||
- "packages/http/*"
|
||||
users:
|
||||
- vikerman #primary
|
||||
- alxhub
|
||||
@ -167,27 +173,28 @@ groups:
|
||||
language-service:
|
||||
conditions:
|
||||
files:
|
||||
- "modules/@angular/language-service/*"
|
||||
- "packages/language-service/*"
|
||||
users:
|
||||
- chuckjaz #primary
|
||||
# needs secondary
|
||||
- tbosch #secondary
|
||||
- vicb
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
router:
|
||||
conditions:
|
||||
files:
|
||||
- "modules/@angular/router/*"
|
||||
- "packages/router/*"
|
||||
users:
|
||||
- vicb #primary
|
||||
# needs secondary
|
||||
- jasonaden
|
||||
- vicb
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
upgrade:
|
||||
conditions:
|
||||
files:
|
||||
- "modules/@angular/upgrade/*"
|
||||
- "packages/upgrade/*"
|
||||
users:
|
||||
- petebacondarwin #primary
|
||||
- gkalpak
|
||||
@ -197,7 +204,7 @@ groups:
|
||||
platform-browser:
|
||||
conditions:
|
||||
files:
|
||||
- "modules/@angular/platform-browser/*"
|
||||
- "packages/platform-browser/*"
|
||||
users:
|
||||
- tbosch #primary
|
||||
- vicb #secondary
|
||||
@ -207,7 +214,7 @@ groups:
|
||||
platform-server:
|
||||
conditions:
|
||||
files:
|
||||
- "modules/@angular/platform-server/*"
|
||||
- "packages/platform-server/*"
|
||||
users:
|
||||
- vikerman #primary
|
||||
- alxhub
|
||||
@ -219,7 +226,7 @@ groups:
|
||||
platform-webworker:
|
||||
conditions:
|
||||
files:
|
||||
- "modules/@angular/platform-webworker/*"
|
||||
- "packages/platform-webworker/*"
|
||||
users:
|
||||
- vicb #primary
|
||||
- tbosch #secondary
|
||||
@ -231,7 +238,7 @@ groups:
|
||||
benchpress:
|
||||
conditions:
|
||||
files:
|
||||
- "modules/@angular/benchpress/*"
|
||||
- "packages/benchpress/*"
|
||||
users:
|
||||
- tbosch #primary
|
||||
# needs secondary
|
||||
@ -243,7 +250,8 @@ groups:
|
||||
files:
|
||||
- "aio/*"
|
||||
users:
|
||||
- IgorMinar
|
||||
- robwormald
|
||||
- petebacondarwin
|
||||
- IgorMinar #primary
|
||||
- petebacondarwin #secondary
|
||||
- gkalpak
|
||||
- wardbell
|
||||
- mhevery #fallback
|
||||
|
35
.travis.yml
35
.travis.yml
@ -10,11 +10,16 @@ addons:
|
||||
# needed to install g++ that is used by npms's native modules
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
# needed to install g++ that is used by npms's native modules
|
||||
- g++-4.8
|
||||
|
||||
# https://docs.travis-ci.com/user/jwt
|
||||
jwt:
|
||||
# SAUCE_ACCESS_KEY<=secret for NGBUILDS_IO_KEY to work around travis-ci/travis-ci#7223, unencrypted value in valentine as NGBUILDS_IO_KEY>
|
||||
# we alias NGBUILDS_IO_KEY to $SAUCE_ACCESS_KEY in env.sh and set the SAUCE_ACCESS_KEY there
|
||||
- secure: "L7nrZwkAtFtYrP2DykPXgZvEKjkv0J/TwQ/r2QGxFTaBq4VZn+2Dw0YS7uCxoMqYzDwH0aAOqxoutibVpk8Z/16nE3tNmU5RzltMd6Xmt3qU2f/JDQLMo6PSlBodnjOUsDHJgmtrcbjhqrx/znA237BkNUu6UZRT7mxhXIZpn0U="
|
||||
branches:
|
||||
except:
|
||||
- g3_v2_0
|
||||
- g3
|
||||
|
||||
cache:
|
||||
yarn: true
|
||||
@ -25,9 +30,9 @@ cache:
|
||||
|
||||
env:
|
||||
global:
|
||||
# GITHUB_TOKEN_ANGULAR
|
||||
# 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: "fq/U7VDMWO8O8SnAQkdbkoSe2X92PVqg4d044HmRYVmcf6YbO48+xeGJ8yOk0pCBwl3ISO4Q2ot0x546kxfiYBuHkZetlngZxZCtQiFT9kyId8ZKcYdXaIW9OVdw3Gh3tQyUwDucfkVhqcs52D6NZjyE2aWZ4/d1V4kWRO/LMgo="
|
||||
- secure: "rNqXoy2gqjbF5tBXlRBy+oiYntO3BtzcxZuEtlLMzNaTNzC4dyMOFub0GkzIPWwOzkARoEU9Kv+bC97fDVbCBUKeyzzEqxqddUKhzRxeaYjsefJ6XeTvBvDxwo7wDwyxZSuWdBeGAe4eARVHm7ypsd+AlvqxtzjyS27TK2BzdL4="
|
||||
# FIREBASE_TOKEN
|
||||
# This is needed for publishing builds to the "aio-staging" firebase site.
|
||||
# TODO(i): the token was generated using the iminar@google account, we should switch to a shared/role-base account.
|
||||
@ -35,6 +40,7 @@ env:
|
||||
matrix:
|
||||
# Order: a slower build first, so that we don't occupy an idle travis worker waiting for others to complete.
|
||||
- CI_MODE=e2e
|
||||
- CI_MODE=e2e_2
|
||||
- CI_MODE=js
|
||||
- CI_MODE=saucelabs_required
|
||||
- CI_MODE=browserstack_required
|
||||
@ -49,14 +55,19 @@ matrix:
|
||||
- env: "CI_MODE=saucelabs_optional"
|
||||
- env: "CI_MODE=browserstack_optional"
|
||||
|
||||
before_install:
|
||||
# source the env.sh script so that the exported variables are available to other scripts later on
|
||||
- source ./scripts/ci/env.sh print
|
||||
|
||||
install:
|
||||
- ./scripts/ci-lite/install.sh
|
||||
- ./scripts/ci/install.sh
|
||||
|
||||
script:
|
||||
- ./scripts/ci-lite/build.sh && ./scripts/ci-lite/test.sh
|
||||
|
||||
after_success:
|
||||
- ./scripts/ci-lite/deploy_aio_staging.sh
|
||||
|
||||
after_script:
|
||||
- ./scripts/ci-lite/cleanup.sh
|
||||
- ./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
|
||||
- ./scripts/ci/print-logs.sh
|
||||
|
298
CHANGELOG.md
298
CHANGELOG.md
@ -1,7 +1,275 @@
|
||||
<a name="4.0.0-rc.0"></a>
|
||||
<a name="4.0.2"></a>
|
||||
## [4.0.2](https://github.com/angular/angular/compare/4.0.1...4.0.2) (2017-04-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler:** fix inheritance for AOT with summaries ([#15583](https://github.com/angular/angular/issues/15583)) ([1864ccb](https://github.com/angular/angular/commit/1864ccb))
|
||||
* **language-service:** avoid throwing exceptions when reporting metadata errors ([0861fda](https://github.com/angular/angular/commit/0861fda))
|
||||
* **language-service:** detect when there isn't a tsconfig.json ([168a2eb](https://github.com/angular/angular/commit/168a2eb)), closes [#15874](https://github.com/angular/angular/issues/15874)
|
||||
* **language-service:** improve resilience to incomplete information ([e4277a0](https://github.com/angular/angular/commit/e4277a0))
|
||||
* **language-service:** initialize static reflector correctly ([5b99533](https://github.com/angular/angular/commit/5b99533)), closes [#15768](https://github.com/angular/angular/issues/15768)
|
||||
* **language-service:** parse extended i18n forms ([c9c7acd](https://github.com/angular/angular/commit/c9c7acd))
|
||||
* **language-service:** resolve any parameter types to any result ([feae7b6](https://github.com/angular/angular/commit/feae7b6))
|
||||
* **router:** fix query param parsing ([2f41b52](https://github.com/angular/angular/commit/2f41b52))
|
||||
* **router:** the preloader use the module from the loaded config ([978f809](https://github.com/angular/angular/commit/978f809))
|
||||
* **tsc-wrapped:** ensure valid path separators in metadata ([c10e50c](https://github.com/angular/angular/commit/c10e50c))
|
||||
|
||||
|
||||
|
||||
<a name="4.0.1"></a>
|
||||
## [4.0.1](https://github.com/angular/angular/compare/4.0.0...4.0.1) (2017-03-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** make sure style calculations are not computed too early ([#15540](https://github.com/angular/angular/issues/15540)) ([c828511](https://github.com/angular/angular/commit/c828511)), closes [#15507](https://github.com/angular/angular/issues/15507)
|
||||
* **compiler:** allow single quotes into named interpolations ([#15461](https://github.com/angular/angular/issues/15461)) ([a654875](https://github.com/angular/angular/commit/a654875)), closes [#15318](https://github.com/angular/angular/issues/15318)
|
||||
* **compiler:** ignore errors when evaluating base classes ([#15560](https://github.com/angular/angular/issues/15560)) ([a88413f](https://github.com/angular/angular/commit/a88413f)), closes [#15536](https://github.com/angular/angular/issues/15536)
|
||||
* **compiler:** throw when a component defines both template and templateUrl ([#15572](https://github.com/angular/angular/issues/15572)) ([902bb2f](https://github.com/angular/angular/commit/902bb2f)), closes [#15566](https://github.com/angular/angular/issues/15566)
|
||||
* **core:** check for undefined on normalizeDebugBindingValue ([#15503](https://github.com/angular/angular/issues/15503)) ([b8c0a97](https://github.com/angular/angular/commit/b8c0a97)), closes [#15494](https://github.com/angular/angular/issues/15494)
|
||||
* **core:** fix inheritance in JIT mode for TS 2.1 ([#15599](https://github.com/angular/angular/issues/15599)) ([ca66530](https://github.com/angular/angular/commit/ca66530)), closes [#15502](https://github.com/angular/angular/issues/15502)
|
||||
* **core:** fix the key/value differ ([#15539](https://github.com/angular/angular/issues/15539)) ([e72124c](https://github.com/angular/angular/commit/e72124c)), closes [#15457](https://github.com/angular/angular/issues/15457)
|
||||
* **core:** improve error msg for invalid KeyValueDiffer.diff arg ([#15489](https://github.com/angular/angular/issues/15489)) ([d74e4d0](https://github.com/angular/angular/commit/d74e4d0)), closes [#15402](https://github.com/angular/angular/issues/15402)
|
||||
* **core:** Update types for TypeScript nullability support ([#15472](https://github.com/angular/angular/issues/15472)) ([8c4b963](https://github.com/angular/angular/commit/8c4b963))
|
||||
* **language-service:** be resilient to invalidate ordering ([#15470](https://github.com/angular/angular/issues/15470)) ([a2c2b87](https://github.com/angular/angular/commit/a2c2b87)), closes [#15466](https://github.com/angular/angular/issues/15466)
|
||||
* **language-service:** correctly determine base members of types ([#15600](https://github.com/angular/angular/issues/15600)) ([0fe4985](https://github.com/angular/angular/commit/0fe4985)), closes [#15460](https://github.com/angular/angular/issues/15460)
|
||||
* **language-service:** don't require `reflect-metadata` module to be provided ([#15569](https://github.com/angular/angular/issues/15569)) ([bfa4f70](https://github.com/angular/angular/commit/bfa4f70)), closes [#15568](https://github.com/angular/angular/issues/15568)
|
||||
* **language-service:** guard access to `Symbol.members` ([#15529](https://github.com/angular/angular/issues/15529)) ([bf25e94](https://github.com/angular/angular/commit/bf25e94)), closes [#15528](https://github.com/angular/angular/issues/15528)
|
||||
* **language-service:** improve performance of `updateModuleAnalysis()` ([#15543](https://github.com/angular/angular/issues/15543)) ([5597fd3](https://github.com/angular/angular/commit/5597fd3))
|
||||
* **router:** should run CanActivate after CanDeactivate guards ([75478b2](https://github.com/angular/angular/commit/75478b2)), closes [#14059](https://github.com/angular/angular/issues/14059) [#15467](https://github.com/angular/angular/issues/15467)
|
||||
* **router:** shouldn't execute CanLoad when a route has been loaded ([2360676](https://github.com/angular/angular/commit/2360676)), closes [#14475](https://github.com/angular/angular/issues/14475) [#15438](https://github.com/angular/angular/issues/15438)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **router:** don't create new serializer every time UrlTree.toString is called ([#15565](https://github.com/angular/angular/issues/15565)) ([fd61145](https://github.com/angular/angular/commit/fd61145))
|
||||
|
||||
<a name="4.0.0"></a>
|
||||
# [4.0.0](https://github.com/angular/angular/compare/4.0.0-rc.6...4.0.0) invisible-makeover (2017-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler:** assume queries with no matches as static ([#15429](https://github.com/angular/angular/issues/15429)) ([c8ab5cb](https://github.com/angular/angular/commit/c8ab5cb)), closes [#15417](https://github.com/angular/angular/issues/15417)
|
||||
* **compiler:** correctly handle when `toString` is exported ([#15430](https://github.com/angular/angular/issues/15430)) ([0dda01e](https://github.com/angular/angular/commit/0dda01e)), closes [#15420](https://github.com/angular/angular/issues/15420)
|
||||
* **platform-browser:** setAttribute should work with xmlns namespace ([#14874](https://github.com/angular/angular/issues/14874)) ([92084f2](https://github.com/angular/angular/commit/92084f2)), closes [#14865](https://github.com/angular/angular/issues/14865)
|
||||
* **router:** should pass new data to Observable when query params change ([#15387](https://github.com/angular/angular/issues/15387)) ([08f2f08](https://github.com/angular/angular/commit/08f2f08)), closes [#15290](https://github.com/angular/angular/issues/15290)
|
||||
* prevent strictNullChecks support until [#15432](https://github.com/angular/angular/issues/15432) is fixed ([#15434](https://github.com/angular/angular/issues/15434)) ([b800a0c](https://github.com/angular/angular/commit/b800a0c))
|
||||
|
||||
|
||||
|
||||
<a name="4.0.0-rc.6"></a>
|
||||
# [4.0.0-rc.6](https://github.com/angular/angular/compare/4.0.0-rc.5...4.0.0-rc.6) (2017-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** correct the main entry path in package.json ([#15300](https://github.com/angular/angular/issues/15300)) ([2489e4b](https://github.com/angular/angular/commit/2489e4b))
|
||||
* **animations:** ensure empty animate() steps work at the end of a sequence ([#15328](https://github.com/angular/angular/issues/15328)) ([fbccd5c](https://github.com/angular/angular/commit/fbccd5c)), closes [#15310](https://github.com/angular/angular/issues/15310)
|
||||
* **animations:** ensure enter/leave cancellations work ([#15323](https://github.com/angular/angular/issues/15323)) ([9bf2fb4](https://github.com/angular/angular/commit/9bf2fb4)), closes [#15315](https://github.com/angular/angular/issues/15315)
|
||||
* **animations:** make sure easing values work with web-animations ([#15195](https://github.com/angular/angular/issues/15195)) ([f925910](https://github.com/angular/angular/commit/f925910)), closes [#15115](https://github.com/angular/angular/issues/15115)
|
||||
* **animations:** make sure non-transitioned leave operations cancel existing animations ([#15254](https://github.com/angular/angular/issues/15254)) ([a6fb78e](https://github.com/angular/angular/commit/a6fb78e)), closes [#15213](https://github.com/angular/angular/issues/15213)
|
||||
* **animations:** only process element nodes through the animation engine ([#15268](https://github.com/angular/angular/issues/15268)) ([80075af](https://github.com/angular/angular/commit/80075af)), closes [#15267](https://github.com/angular/angular/issues/15267)
|
||||
* **animations:** only treat view removals as `void` state transitions ([#15245](https://github.com/angular/angular/issues/15245)) ([c66437f](https://github.com/angular/angular/commit/c66437f)), closes [#15223](https://github.com/angular/angular/issues/15223)
|
||||
* **animations:** stringify boolean values as `1` and `0` ([#15311](https://github.com/angular/angular/issues/15311)) ([94da801](https://github.com/angular/angular/commit/94da801)), closes [#15247](https://github.com/angular/angular/issues/15247)
|
||||
* **compiler:** add an empty content for source file of non mapped code. ([#15246](https://github.com/angular/angular/issues/15246)) ([8415910](https://github.com/angular/angular/commit/8415910))
|
||||
* **compiler:** don’t call `check` if we don’t need to ([#15322](https://github.com/angular/angular/issues/15322)) ([764e90f](https://github.com/angular/angular/commit/764e90f))
|
||||
* **compiler:** look for flat module resources using declaration module path ([#15367](https://github.com/angular/angular/issues/15367)) ([90d2518](https://github.com/angular/angular/commit/90d2518)), closes [#15221](https://github.com/angular/angular/issues/15221)
|
||||
* **compiler:** only log template deprecation warning once ([#15364](https://github.com/angular/angular/issues/15364)) ([08d8675](https://github.com/angular/angular/commit/08d8675))
|
||||
* **compiler:** use attribute id to merge translations ([#15302](https://github.com/angular/angular/issues/15302)) ([1d7693c](https://github.com/angular/angular/commit/1d7693c)), closes [#15234](https://github.com/angular/angular/issues/15234)
|
||||
* **compiler-cli:** adding missing format xliff for the extractor ([#15386](https://github.com/angular/angular/issues/15386)) ([a50d79d](https://github.com/angular/angular/commit/a50d79d))
|
||||
* **core:** allow tree shaking of component factories and styles ([#15214](https://github.com/angular/angular/issues/15214)) ([2a0e55f](https://github.com/angular/angular/commit/2a0e55f)), closes [#15181](https://github.com/angular/angular/issues/15181)
|
||||
* **core:** don’t create a comment for components with empty template. ([#15260](https://github.com/angular/angular/issues/15260)) ([f8c075a](https://github.com/angular/angular/commit/f8c075a)), closes [#15143](https://github.com/angular/angular/issues/15143)
|
||||
* **core:** mark components for check when host events trigger. ([#15359](https://github.com/angular/angular/issues/15359)) ([64beae9](https://github.com/angular/angular/commit/64beae9)), closes [#15352](https://github.com/angular/angular/issues/15352)
|
||||
* **core:** only apply `WrappedValue` to the binding of the pipe ([#15257](https://github.com/angular/angular/issues/15257)) ([0c43535](https://github.com/angular/angular/commit/0c43535)), closes [#15116](https://github.com/angular/angular/issues/15116)
|
||||
* **core:** provide `NgModuleRef` in `ViewContainerRef.createComponent`. ([#15350](https://github.com/angular/angular/issues/15350)) ([431eb30](https://github.com/angular/angular/commit/431eb30)), closes [#15241](https://github.com/angular/angular/issues/15241)
|
||||
* **core:** stringify shouldn't throw when toString returns null/undefined ([#14975](https://github.com/angular/angular/issues/14975)) ([8e6995c](https://github.com/angular/angular/commit/8e6995c)), closes [#14948](https://github.com/angular/angular/issues/14948)
|
||||
* **core:** trigger host animations for elements that are removed. ([#15251](https://github.com/angular/angular/issues/15251)) ([0d3e314](https://github.com/angular/angular/commit/0d3e314)), closes [#14813](https://github.com/angular/angular/issues/14813) [#15193](https://github.com/angular/angular/issues/15193)
|
||||
* **core:** update peer dep on zone.js to ^0.8.5 ([#15365](https://github.com/angular/angular/issues/15365)) ([97149f9](https://github.com/angular/angular/commit/97149f9)), closes [#15185](https://github.com/angular/angular/issues/15185)
|
||||
* **forms:** make composition event buffering configurable ([#15256](https://github.com/angular/angular/issues/15256)) ([5efc860](https://github.com/angular/angular/commit/5efc860)), closes [#15079](https://github.com/angular/angular/issues/15079)
|
||||
* **platform-server:** interpret Native view encapsulation as Emulated on the server ([#15155](https://github.com/angular/angular/issues/15155)) ([de3d2ee](https://github.com/angular/angular/commit/de3d2ee))
|
||||
* **platform-server:** setup NoopAnimationsModule in ServerModule by default ([#15131](https://github.com/angular/angular/issues/15131)) ([5c5c2ae](https://github.com/angular/angular/commit/5c5c2ae)), closes [#15098](https://github.com/angular/angular/issues/15098) [#14784](https://github.com/angular/angular/issues/14784)
|
||||
* **platform-server:** throw a better error message for relative URLs ([#15357](https://github.com/angular/angular/issues/15357)) ([15a082c](https://github.com/angular/angular/commit/15a082c)), closes [#15349](https://github.com/angular/angular/issues/15349)
|
||||
* **tsc-wrapped:** emit flat module format correctly on Windows ([#15215](https://github.com/angular/angular/issues/15215)) ([6e9264a](https://github.com/angular/angular/commit/6e9264a)), closes [#15192](https://github.com/angular/angular/issues/15192)
|
||||
* **tsc-wrapped:** use windows friendly path normalization in bundler ([#15374](https://github.com/angular/angular/issues/15374)) ([c584997](https://github.com/angular/angular/commit/c584997)), closes [#15289](https://github.com/angular/angular/issues/15289)
|
||||
* **upgrade:** component injectors should not link the module injector tree ([#15385](https://github.com/angular/angular/issues/15385)) ([ea49a95](https://github.com/angular/angular/commit/ea49a95))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **core:** expose `inputs`, `outputs` and `ngContentSelectors` on `ComponentFactory`. ([#15214](https://github.com/angular/angular/issues/15214)) ([791534f](https://github.com/angular/angular/commit/791534f))
|
||||
* **router:** add `ParamMap.keys` to get a list of parameters ([d3eda7a](https://github.com/angular/angular/commit/d3eda7a))
|
||||
* **router:** introduce `ParamMap` to access parameters ([a755b71](https://github.com/angular/angular/commit/a755b71))
|
||||
* **tsc-wrapped:** record original location of flattened symbols ([#15367](https://github.com/angular/angular/issues/15367)) ([7354949](https://github.com/angular/angular/commit/7354949))
|
||||
* **upgrade:** use `ComponentFactory.inputs/outputs/ngContentSelectors` ([#15214](https://github.com/angular/angular/issues/15214)) ([9429032](https://github.com/angular/angular/commit/9429032))
|
||||
|
||||
|
||||
|
||||
<a name="4.0.0-rc.5"></a>
|
||||
# [4.0.0-rc.5](https://github.com/angular/angular/compare/4.0.0-rc.4...4.0.0-rc.5) (2017-03-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler-cli:** update the tsc-wrapped dependency version ([#15226](https://github.com/angular/angular/issues/15226)) ([7fb4528](https://github.com/angular/angular/commit/7fb4528))
|
||||
|
||||
|
||||
|
||||
<a name="4.0.0-rc.4"></a>
|
||||
# [4.0.0-rc.4](https://github.com/angular/angular/compare/4.0.0-rc.3...4.0.0-rc.4) (2017-03-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** always fire callbacks even for noop animations ([#15170](https://github.com/angular/angular/issues/15170)) ([3f38c6f](https://github.com/angular/angular/commit/3f38c6f))
|
||||
* **animations:** make sure easing values are applied to an empty animate() step ([#15174](https://github.com/angular/angular/issues/15174)) ([62d5543](https://github.com/angular/angular/commit/62d5543)), closes [#15115](https://github.com/angular/angular/issues/15115)
|
||||
* **animations:** support multiple state names per state() call ([#15147](https://github.com/angular/angular/issues/15147)) ([36ce0af](https://github.com/angular/angular/commit/36ce0af)), closes [#14732](https://github.com/angular/angular/issues/14732)
|
||||
* **compiler:** always use `ng://` prefix for sourcemap urls ([#15218](https://github.com/angular/angular/issues/15218)) ([994089d](https://github.com/angular/angular/commit/994089d))
|
||||
* **compiler:** fix utf8encode, move to sharted utils, add tests ([#15076](https://github.com/angular/angular/issues/15076)) ([959a03a](https://github.com/angular/angular/commit/959a03a))
|
||||
* **compiler:** generated code should pass `noUnusedLocals` check ([50ab06e](https://github.com/angular/angular/commit/50ab06e)), closes [#14797](https://github.com/angular/angular/issues/14797)
|
||||
* **compiler:** Improve error message for missing annotations ([#14724](https://github.com/angular/angular/issues/14724)) ([3c15916](https://github.com/angular/angular/commit/3c15916))
|
||||
* **compiler:** improve error msg for unexpected closing tags ([#14747](https://github.com/angular/angular/issues/14747)) ([5f9fb91](https://github.com/angular/angular/commit/5f9fb91)), closes [#6652](https://github.com/angular/angular/issues/6652)
|
||||
* **compiler:** make sourcemaps work in AOT mode ([492153a](https://github.com/angular/angular/commit/492153a))
|
||||
* **compiler:** only warn for `[@Injectable](https://github.com/Injectable)` classes with invalid args. ([5c34066](https://github.com/angular/angular/commit/5c34066)), closes [#15003](https://github.com/angular/angular/issues/15003)
|
||||
* **compiler:** shouldn't throw when Symbol is used as DI token ([#13701](https://github.com/angular/angular/issues/13701)) ([8b5c6b2](https://github.com/angular/angular/commit/8b5c6b2)), closes [#13314](https://github.com/angular/angular/issues/13314)
|
||||
* **compiler:** support interface types in injectable constuctors ([#14894](https://github.com/angular/angular/issues/14894)) ([b00fe20](https://github.com/angular/angular/commit/b00fe20)), closes [#12631](https://github.com/angular/angular/issues/12631)
|
||||
* **compiler:** warning prints "WARNING" instead of "ERROR" ([#15125](https://github.com/angular/angular/issues/15125)) ([3b1956b](https://github.com/angular/angular/commit/3b1956b))
|
||||
* **core:** don’t recreate `TemplateRef` when used as a reference. ([#15066](https://github.com/angular/angular/issues/15066)) ([df914ef](https://github.com/angular/angular/commit/df914ef)), closes [#14873](https://github.com/angular/angular/issues/14873)
|
||||
* **core:** don’t throw if queries change during change detection. ([06fc42b](https://github.com/angular/angular/commit/06fc42b)), closes [#14925](https://github.com/angular/angular/issues/14925)
|
||||
* **core:** ErrorHandler should not rethrow an error by default ([#15077](https://github.com/angular/angular/issues/15077)) ([#15208](https://github.com/angular/angular/issues/15208)) ([77fd91d](https://github.com/angular/angular/commit/77fd91d)), closes [#14949](https://github.com/angular/angular/issues/14949) [#15182](https://github.com/angular/angular/issues/15182) [#14316](https://github.com/angular/angular/issues/14316)
|
||||
* **core:** update peer dep on zone.js to ^0.8.4 ([#15209](https://github.com/angular/angular/issues/15209)) ([d2fbbb4](https://github.com/angular/angular/commit/d2fbbb4)), closes [#15180](https://github.com/angular/angular/issues/15180) [#15185](https://github.com/angular/angular/issues/15185)
|
||||
* **core:** use presence of .subscribe to detect observables rather then Symbol.observable ([#15171](https://github.com/angular/angular/issues/15171)) ([6e98757](https://github.com/angular/angular/commit/6e98757)), closes [#14298](https://github.com/angular/angular/issues/14298) [#14473](https://github.com/angular/angular/issues/14473) [#14926](https://github.com/angular/angular/issues/14926)
|
||||
* **forms:** ensure observable validators are properly canceled ([#15132](https://github.com/angular/angular/issues/15132)) ([26d4ce2](https://github.com/angular/angular/commit/26d4ce2))
|
||||
* **forms:** remove equalsTo validator ([#15050](https://github.com/angular/angular/issues/15050)) ([778f7d6](https://github.com/angular/angular/commit/778f7d6))
|
||||
* element injector vs module injector ([#15044](https://github.com/angular/angular/issues/15044)) ([13686bb](https://github.com/angular/angular/commit/13686bb)), closes [#12869](https://github.com/angular/angular/issues/12869) [#12889](https://github.com/angular/angular/issues/12889) [#13885](https://github.com/angular/angular/issues/13885) [#13870](https://github.com/angular/angular/issues/13870)
|
||||
* **http:** Make ResponseOptionsArgs an interface ([#14607](https://github.com/angular/angular/issues/14607)) ([#14623](https://github.com/angular/angular/issues/14623)) ([f1b33ab](https://github.com/angular/angular/commit/f1b33ab)), closes [#13708](https://github.com/angular/angular/issues/13708)
|
||||
* **platform-browser:** prevent clobbered elements from freezing the browser ([a4076c7](https://github.com/angular/angular/commit/a4076c7))
|
||||
* **platform-server:** correctly implement get href in parse5 adapter ([#15022](https://github.com/angular/angular/issues/15022)) ([80649ea](https://github.com/angular/angular/commit/80649ea))
|
||||
* **platform-server:** fix an exception when HostListener('window:scroll') is used on the server ([#15019](https://github.com/angular/angular/issues/15019)) ([4f7d62a](https://github.com/angular/angular/commit/4f7d62a))
|
||||
* correct UMD resolutions for platform-browser_animations ([#15190](https://github.com/angular/angular/issues/15190)) ([923d0c5](https://github.com/angular/angular/commit/923d0c5)), closes [#15114](https://github.com/angular/angular/issues/15114)
|
||||
* don't instantiate providers with ngOnDestroy eagerly. ([#15070](https://github.com/angular/angular/issues/15070)) ([2c5a671](https://github.com/angular/angular/commit/2c5a671)), closes [#14552](https://github.com/angular/angular/issues/14552)
|
||||
* fix path locally to empty.js ([#15073](https://github.com/angular/angular/issues/15073)) ([80112a9](https://github.com/angular/angular/commit/80112a9))
|
||||
* **platform-server:** fix get/set title in parse5 adapter ([#14965](https://github.com/angular/angular/issues/14965)) ([018e5c9](https://github.com/angular/angular/commit/018e5c9))
|
||||
* **platform-server:** handle styles with extra ':'s correctly ([#15189](https://github.com/angular/angular/issues/15189)) ([013d806](https://github.com/angular/angular/commit/013d806))
|
||||
* **platform-server:** support svg elements with namespaced attributes ([#15101](https://github.com/angular/angular/issues/15101)) ([f093501](https://github.com/angular/angular/commit/f093501))
|
||||
* **router:** fix query parameters with multiple values ([#15129](https://github.com/angular/angular/issues/15129)) ([029d0f2](https://github.com/angular/angular/commit/029d0f2)), closes [#14796](https://github.com/angular/angular/issues/14796)
|
||||
* **tsc-wrapped:** emit js files in all cases ([c0e05e6](https://github.com/angular/angular/commit/c0e05e6))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **core:** use flags in `Renderer2.setStyle` instead of booleans ([#15045](https://github.com/angular/angular/issues/15045)) ([ff71eff](https://github.com/angular/angular/commit/ff71eff))
|
||||
|
||||
### Features
|
||||
|
||||
* **common:** support `as` syntax in template/* bindings ([#15025](https://github.com/angular/angular/issues/15025)) ([c10c060](https://github.com/angular/angular/commit/c10c060)), closes [#15020](https://github.com/angular/angular/issues/15020)
|
||||
* **compiler-cli:** support metadata file aliases ([0ab49d4](https://github.com/angular/angular/commit/0ab49d4))
|
||||
* **core:** allow to provide multiple default testing modules ([#15054](https://github.com/angular/angular/issues/15054)) ([6c8638c](https://github.com/angular/angular/commit/6c8638c))
|
||||
* **core:** expose `inputs`, `outputs` and `ngContentSelectors` on `ComponentFactory`. ([1171f91](https://github.com/angular/angular/commit/1171f91))
|
||||
* **upgrade:** support multi-slot projection in upgrade/static ([#14282](https://github.com/angular/angular/issues/14282)) ([914797a](https://github.com/angular/angular/commit/914797a)), closes [#14261](https://github.com/angular/angular/issues/14261)
|
||||
* **upgrade:** use `ComponentFactory.inputs/outputs/ngContentSelectors` ([a3e32fb](https://github.com/angular/angular/commit/a3e32fb))
|
||||
* introduce source maps for templates ([#15011](https://github.com/angular/angular/issues/15011)) ([cdc882b](https://github.com/angular/angular/commit/cdc882b))
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Perviously, any provider that had an ngOnDestroy lifecycle hook would be created eagerly.
|
||||
|
||||
Now, only classes that are annotated with @Component, @Directive, @Pipe, @NgModule are eager. Providers only become eager if they are either directly or transitively injected into one of the above.
|
||||
|
||||
This also makes all `useValue` providers eager, which
|
||||
should have no observable impact other than code size.
|
||||
|
||||
**EXPECTED IMPACT**:
|
||||
Making providers eager was an incorrect behavior and never documented.
|
||||
Also, providers that are used by a directive / pipe / ngModule stay eager.
|
||||
So the impact should be rather small.
|
||||
|
||||
* DebugNode.source no longer returns the source location of a node.
|
||||
|
||||
Closes 14013
|
||||
|
||||
* core: (since v4 rc.1)
|
||||
- `Renderer2.setStyle` no longer takes booleans but rather a
|
||||
bit mask of flags.
|
||||
|
||||
|
||||
|
||||
<a name="2.4.10"></a>
|
||||
## [2.4.10](https://github.com/angular/angular/compare/2.4.9...2.4.10) (2017-03-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler:** fix decoding surrogate pairs ([#15154](https://github.com/angular/angular/issues/15154)) ([e5c9bbc](https://github.com/angular/angular/commit/e5c9bbc))
|
||||
* **router:** do not finish bootstrap until all the routes are resolved ([#15121](https://github.com/angular/angular/issues/15121)) ([34403cd](https://github.com/angular/angular/commit/34403cd))
|
||||
|
||||
|
||||
<a name="4.0.0-rc.3"></a>
|
||||
# [4.0.0-rc.3](https://github.com/angular/angular/compare/4.0.0-rc.2...4.0.0-rc.3) (2017-03-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler:** don’t throw for empty array literal in assignments ([#14878](https://github.com/angular/angular/issues/14878)) ([6cd3326](https://github.com/angular/angular/commit/6cd3326)), closes [#14782](https://github.com/angular/angular/issues/14782)
|
||||
* **compiler:** improve error message when a module imports itself ([#14646](https://github.com/angular/angular/issues/14646)) ([6bc6482](https://github.com/angular/angular/commit/6bc6482)), closes [#14644](https://github.com/angular/angular/issues/14644)
|
||||
* **core:** allow to use the `Renderer` outside of views. ([#14882](https://github.com/angular/angular/issues/14882)) ([ba4b6f5](https://github.com/angular/angular/commit/ba4b6f5)), closes [#14872](https://github.com/angular/angular/issues/14872)
|
||||
* **router:** do not finish bootstrap until all the routes are resolved ([#14762](https://github.com/angular/angular/issues/14762)) ([5df998d](https://github.com/angular/angular/commit/5df998d))
|
||||
* **upgrade:** populate upgraded component's view before creating the controller ([#14289](https://github.com/angular/angular/issues/14289)) ([07122f0](https://github.com/angular/angular/commit/07122f0)), closes [#13912](https://github.com/angular/angular/issues/13912)
|
||||
* throw for synthetic properties / listeners by default ([#14880](https://github.com/angular/angular/issues/14880)) ([3651d8d](https://github.com/angular/angular/commit/3651d8d))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
#### since 4.0 rc.1:
|
||||
- rename `RendererV2` to `Renderer2`
|
||||
- rename `RendererTypeV2` to `RendererType2`
|
||||
- rename `RendererFactoryV2` to `RendererFactory2`
|
||||
|
||||
<a name="2.4.9"></a>
|
||||
## [2.4.9](https://github.com/angular/angular/compare/2.4.8...2.4.9) (2017-03-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **http:** Make ResponseOptionsArgs an interface ([b658fa9](https://github.com/angular/angular/commit/b658fa9)), closes [#13708](https://github.com/angular/angular/issues/13708)
|
||||
* **router:** improve robustness ([#14602](https://github.com/angular/angular/issues/14602)) ([2a12346](https://github.com/angular/angular/commit/2a12346))
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* fix(router): do not finish bootstrap until all the routes are resolved ([#14327](https://github.com/angular/angular/issues/14327)) ([de36f8a](https://github.com/angular/angular/commit/de36f8a)), closes [#14681](https://github.com/angular/angular/issues/14681) [#14588](https://github.com/angular/angular/issues/14588)
|
||||
|
||||
|
||||
|
||||
<a name="4.0.0-rc.2"></a>
|
||||
## [4.0.0-rc.2](https://github.com/angular/angular/compare/4.0.0-rc.1...4.0.0-rc.2) (2017-03-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** make animations work in AOT ([#14775](https://github.com/angular/angular/issues/14775)) ([9560ad8](https://github.com/angular/angular/commit/9560ad8))
|
||||
* **compiler:** apply element bindings before host bindings ([#14823](https://github.com/angular/angular/issues/14823)) ([79fc1e3](https://github.com/angular/angular/commit/79fc1e3))
|
||||
* **compiler:** fix identifier names of `EMPTY_MAP` / `EMPTY_ARRAY` ([#14806](https://github.com/angular/angular/issues/14806)) ([5ba55b0](https://github.com/angular/angular/commit/5ba55b0))
|
||||
* **compiler:** quote `animation` in `RendererTypeV2` and skip if empty ([#14773](https://github.com/angular/angular/issues/14773)) ([77682a3](https://github.com/angular/angular/commit/77682a3))
|
||||
* **core:** call lifecycle hooks for siblings in declaration order ([d2e4256](https://github.com/angular/angular/commit/d2e4256))
|
||||
* **core:** fix `isComponentView()` and `isEmbeddedView()` tests ([#14789](https://github.com/angular/angular/issues/14789)) ([5753de5](https://github.com/angular/angular/commit/5753de5)), closes [#14778](https://github.com/angular/angular/issues/14778)
|
||||
* **language-service:** tolerate errors in decorators ([#14634](https://github.com/angular/angular/issues/14634)) ([6bae737](https://github.com/angular/angular/commit/6bae737)), closes [#14631](https://github.com/angular/angular/issues/14631)
|
||||
* **platform-server:** don't setup Testability and TestabilityRegistry on the server ([#14510](https://github.com/angular/angular/issues/14510)) ([47bdc2b](https://github.com/angular/angular/commit/47bdc2b))
|
||||
* **tsc-wrapped:** validate metadata in static members of a class ([#14772](https://github.com/angular/angular/issues/14772)) ([a6996a9](https://github.com/angular/angular/commit/a6996a9)), closes [#13481](https://github.com/angular/angular/issues/13481)
|
||||
* **upgrade:** fix upgrade component Closure optimization. ([#14801](https://github.com/angular/angular/issues/14801)) ([968995a](https://github.com/angular/angular/commit/968995a))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* delete pre-view-engine core, compiler, platform-browser, etc code ([#14788](https://github.com/angular/angular/issues/14788)) ([126fda2](https://github.com/angular/angular/commit/126fda2))
|
||||
* **compiler:** make identifiers for generated code small to improve dev size ([5caab71](https://github.com/angular/angular/commit/5caab71f7dc64b10f3544b2a3b2650e1666adbf1))
|
||||
|
||||
|
||||
|
||||
<a name="4.0.0-rc.1"></a>
|
||||
# [4.0.0-rc.1](https://github.com/angular/angular/compare/4.0.0-beta.8...4.0.0-rc.1) (2017-02-24)
|
||||
|
||||
We are excited to share RC0 with the community. This is a feature-complete pre-release of 4.0.0. Upgrade to get to know the new features to be released in 4.0.0, and to help us validate the release.
|
||||
We are excited to share 4.0.0-RC.1 with the community. This is a feature-complete pre-release of 4.0.0. Upgrade to get to know the new features to be released in 4.0.0, and to help us validate the release.
|
||||
|
||||
It’s important to note that this release has been tested extensively. All Angular applications within Google use the master branch of Angular, so every commit is tested and validated at scale prior to any release.
|
||||
|
||||
@ -18,7 +286,7 @@ We’ve made changes under to hood to what AOT generated code looks like. These
|
||||
Our template binding syntax now supports a couple helpful changes. You can now use an if/else style syntax, and assign local variables such as when unrolling an observable.
|
||||
|
||||
```
|
||||
<div #loading>Loading...</div>
|
||||
<ng-template #loading>Loading...</ng-template>
|
||||
<div *ngIf="userObservable | async; else loading; let user">
|
||||
{{ user.name }}
|
||||
</div>
|
||||
@ -41,8 +309,9 @@ Angular is now compliant with [TypeScript’s StrictNullChecks](https://www.type
|
||||
Universal, the project that allows developers to run Angular on a server, is now up to date with Angular again, and has been adopted by the Angular team. This release now includes the results of the work from the Universal team over the last few months. The majority of the Universal code is now in platform-server. To learn more about this change, take a look the new [`renderModuleFactory`](https://github.com/angular/angular/blob/56f232cdd70a352cb9151bc7cfe8981bc2710ea6/modules/%40angular/platform-server/src/utils.ts#L63-L72) method, or Rob Wormald’s [Demo Repository](https://github.com/robwormald/ng-universal-demo/). More documentation is forthcoming.
|
||||
|
||||
|
||||
### Flat ES Modules (ESM)
|
||||
We now ship flattened versions of our modules, this should help tree-shaking and help reduce the size of your generated bundles.
|
||||
<a name="flat-es-modules-esm"></a><!-- legacy anchor link, keep it here -->
|
||||
### Flat ES Modules (Flat ESM / FESM)
|
||||
We now ship flattened versions of our modules ("rolled up" version of our code in the EcmaScript Module format, see [example file](https://github.com/angular/core-builds/blob/85cbe3f8d6107af033b0f8b56456c181cbcb5eb7/%40angular/core.js)). This format should help tree-shaking, help reduce the size of your generated bundles, and speed up build, transpilation, and loading in the browser in certain scenarios.
|
||||
|
||||
|
||||
### ES2015 Builds
|
||||
@ -59,18 +328,19 @@ The following is a list of known issues that will be fixed in the next rcs.
|
||||
- legacy UMD bundles don't have correct RxJS mappings when running in ES5 mode without a module system
|
||||
|
||||
|
||||
## Installing RC.0
|
||||
## Installing RC.1
|
||||
We have two main ways to update. If you have an existing project, you should be able to run:
|
||||
|
||||
`npm install @angular/{common,compiler,compiler-cli,core,forms,http,platform-browser,platform-browser-dynamic,platform-server,router,animations}@next --save`
|
||||
On Linux/Mac: `npm install @angular/{common,compiler,compiler-cli,core,forms,http,platform-browser,platform-browser-dynamic,platform-server,router,animations}@next --save`
|
||||
On Windows: `npm install @angular/common@next @angular/compiler@next @angular/compiler-cli@next @angular/core@next @angular/forms@next @angular/http@next @angular/platform-browser@next @angular/platform-browser-dynamic@next @angular/platform-server@next @angular/router@next @angular/animations@next --save`
|
||||
|
||||
|
||||
Then run whatever `ng serve` or `npm start` command you normally use, and everything should work.
|
||||
|
||||
*Please ensure that you are using Typescript v2.1.6 or higher.*
|
||||
|
||||
*If you rely on Animations* you’ll also need to install the animations package and import the new `AnimationsModule` from `@angular/animations` in your root NgModule. Without this, your code will compile and run, but animations won’t activate.
|
||||
|
||||
*If you rely on Animations* you’ll also need to install the animations package `@angular/animations` and import the new `BrowserAnimationsModule` from `@angular/platform-browser/animations` in your root NgModule. Without this, your code will compile and run, but animations won’t activate.
|
||||
Imports from `@angular/core` were deprecated, use imports from the new package `import { trigger, state, style, transition, animate } from '@angular/animations';`.
|
||||
|
||||
## What's next?
|
||||
We have [three more release candidates scheduled](https://github.com/angular/angular/blob/master/docs/RELEASE_SCHEDULE.md) before our planned GA the week of March 22. In the meantime we'll be looking for your feedback, fixing bugs and working on docs.
|
||||
@ -103,7 +373,7 @@ We have [three more release candidates scheduled](https://github.com/angular/ang
|
||||
* **packaging:** properly build the core-testing bundle ([4301dce](https://github.com/angular/angular/commit/4301dce))
|
||||
* **platform-webworker:** integrate review feedback ([601fd3e](https://github.com/angular/angular/commit/601fd3e)), closes [#14581](https://github.com/angular/angular/issues/14581)
|
||||
* **router:** do not finish bootstrap until all the routes are resolved ([#14608](https://github.com/angular/angular/issues/14608)) ([2a191ca](https://github.com/angular/angular/commit/2a191ca)), closes [#12162](https://github.com/angular/angular/issues/12162) [#14155](https://github.com/angular/angular/issues/14155)
|
||||
* app ids for better <style> management for ssr ([#14632](https://github.com/angular/angular/issues/14632)) ([88bc143](https://github.com/angular/angular/commit/88bc143))
|
||||
* app ids for better style tag management for ssr ([#14632](https://github.com/angular/angular/issues/14632)) ([88bc143](https://github.com/angular/angular/commit/88bc143))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
@ -408,6 +678,12 @@ returned value being an array.
|
||||
* **tsc-wrapped:** Support of vinyl like config file was added ([#13987](https://github.com/angular/angular/issues/13987)) ([0c7726d](https://github.com/angular/angular/commit/0c7726d))
|
||||
* **upgrade:** Support ng-model in downgraded components ([#10578](https://github.com/angular/angular/issues/10578)) ([e21e9c5](https://github.com/angular/angular/commit/e21e9c5))
|
||||
|
||||
### DEPRECATIONS
|
||||
|
||||
* `OpaqueToken` is now deprecated, use `InjectionToken<T>` instead.
|
||||
* `Injector.get(token: any, notFoundValue?: any): any` is now deprecated
|
||||
use the same method which is now overloaded as
|
||||
`Injector.get<T>(token: Type<T>|InjectionToken<T>, notFoundValue?: T): T;`
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
@ -1971,7 +2247,7 @@ prefix using `animate-` must now be preixed using `bind-animate-`.
|
||||
|
||||
### ROUTER CHANGE LOG
|
||||
|
||||
[You can find the router changelog here.](https://github.com/angular/angular/blob/master/modules/@angular/router/CHANGELOG.md)
|
||||
[You can find the router changelog here.](https://github.com/angular/angular/blob/master/packages/router/CHANGELOG.md)
|
||||
|
||||
<a name="2.0.0-rc.4"></a>
|
||||
# [2.0.0-rc.4](https://github.com/angular/angular/compare/2.0.0-rc.3...2.0.0-rc.4) (2016-06-30)
|
||||
|
@ -147,7 +147,7 @@ To ensure consistency throughout the source code, keep these rules in mind as yo
|
||||
* All public API methods **must be documented**. (Details TBC).
|
||||
* We follow [Google's JavaScript Style Guide][js-style-guide], but wrap all code at
|
||||
**100 characters**. An automated formatter is available, see
|
||||
[DEVELOPER.md](DEVELOPER.md#clang-format).
|
||||
[DEVELOPER.md](docs/DEVELOPER.md#clang-format).
|
||||
|
||||
## <a name="commit"></a> Commit Message Guidelines
|
||||
|
||||
@ -263,7 +263,7 @@ changes to be accepted, the CLA must be signed. It's a quick process, we promise
|
||||
[coc]: https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md
|
||||
[commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#
|
||||
[corporate-cla]: http://code.google.com/legal/corporate-cla-v1.0.html
|
||||
[dev-doc]: https://github.com/angular/angular/blob/master/DEVELOPER.md
|
||||
[dev-doc]: https://github.com/angular/angular/blob/master/docs/DEVELOPER.md
|
||||
[github]: https://github.com/angular/angular
|
||||
[gitter]: https://gitter.im/angular/angular
|
||||
[individual-cla]: http://code.google.com/legal/individual-cla-v1.0.html
|
||||
|
@ -10,6 +10,7 @@
|
||||
"assets": [
|
||||
"assets",
|
||||
"content",
|
||||
"app/search/search-worker.js",
|
||||
"favicon.ico"
|
||||
],
|
||||
"index": "index.html",
|
||||
@ -18,6 +19,7 @@
|
||||
"test": "test.ts",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"prefix": "aio",
|
||||
"serviceWorker": true,
|
||||
"styles": [
|
||||
"styles.scss"
|
||||
],
|
||||
|
11
aio/.gitignore
vendored
Normal file
11
aio/.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
# Ignore node_modules
|
||||
node_modules
|
||||
|
||||
# Ignore npm/yarn debug log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# Ignore generated content
|
||||
/dist
|
||||
/src/content
|
||||
/.sass-cache
|
3
aio/aio-builds-setup/dockerbuild/.dockerignore
Normal file
3
aio/aio-builds-setup/dockerbuild/.dockerignore
Normal file
@ -0,0 +1,3 @@
|
||||
scripts-js/lib
|
||||
scripts-js/node_modules
|
||||
scripts-js/**/test
|
158
aio/aio-builds-setup/dockerbuild/Dockerfile
Normal file
158
aio/aio-builds-setup/dockerbuild/Dockerfile
Normal file
@ -0,0 +1,158 @@
|
||||
# Image metadata and config
|
||||
FROM debian:jessie
|
||||
|
||||
LABEL name="angular.io PR preview" \
|
||||
description="This image implements the PR preview functionality for angular.io." \
|
||||
vendor="Angular" \
|
||||
version="1.0"
|
||||
|
||||
VOLUME /aio-secrets
|
||||
VOLUME /var/www/aio-builds
|
||||
|
||||
EXPOSE 80 443
|
||||
|
||||
|
||||
# Build-time args and env vars
|
||||
ARG AIO_BUILDS_DIR=/var/www/aio-builds
|
||||
ARG TEST_AIO_BUILDS_DIR=/tmp/aio-builds
|
||||
ARG AIO_DOMAIN_NAME=ngbuilds.io
|
||||
ARG TEST_AIO_DOMAIN_NAME=$AIO_DOMAIN_NAME.localhost
|
||||
ARG AIO_GITHUB_ORGANIZATION=angular
|
||||
ARG TEST_AIO_GITHUB_ORGANIZATION=angular
|
||||
ARG AIO_GITHUB_TEAM_SLUGS=angular-core,aio-contributors
|
||||
ARG TEST_AIO_GITHUB_TEAM_SLUGS=angular-core,aio-contributors
|
||||
ARG AIO_NGINX_HOSTNAME=$AIO_DOMAIN_NAME
|
||||
ARG TEST_AIO_NGINX_HOSTNAME=$TEST_AIO_DOMAIN_NAME
|
||||
ARG AIO_NGINX_PORT_HTTP=80
|
||||
ARG TEST_AIO_NGINX_PORT_HTTP=8080
|
||||
ARG AIO_NGINX_PORT_HTTPS=443
|
||||
ARG TEST_AIO_NGINX_PORT_HTTPS=4433
|
||||
ARG AIO_REPO_SLUG=angular/angular
|
||||
ARG TEST_AIO_REPO_SLUG=test-repo/test-slug
|
||||
ARG AIO_UPLOAD_HOSTNAME=upload.localhost
|
||||
ARG TEST_AIO_UPLOAD_HOSTNAME=upload.localhost
|
||||
ARG AIO_UPLOAD_MAX_SIZE=20971520
|
||||
ARG TEST_AIO_UPLOAD_MAX_SIZE=20971520
|
||||
ARG AIO_UPLOAD_PORT=3000
|
||||
ARG TEST_AIO_UPLOAD_PORT=3001
|
||||
|
||||
ENV AIO_BUILDS_DIR=$AIO_BUILDS_DIR TEST_AIO_BUILDS_DIR=$TEST_AIO_BUILDS_DIR \
|
||||
AIO_DOMAIN_NAME=$AIO_DOMAIN_NAME TEST_AIO_DOMAIN_NAME=$TEST_AIO_DOMAIN_NAME \
|
||||
AIO_GITHUB_ORGANIZATION=$AIO_GITHUB_ORGANIZATION TEST_AIO_GITHUB_ORGANIZATION=$TEST_AIO_GITHUB_ORGANIZATION \
|
||||
AIO_GITHUB_TEAM_SLUGS=$AIO_GITHUB_TEAM_SLUGS TEST_AIO_GITHUB_TEAM_SLUGS=$TEST_AIO_GITHUB_TEAM_SLUGS \
|
||||
AIO_LOCALCERTS_DIR=/etc/ssl/localcerts TEST_AIO_LOCALCERTS_DIR=/etc/ssl/localcerts-test \
|
||||
AIO_NGINX_HOSTNAME=$AIO_NGINX_HOSTNAME TEST_AIO_NGINX_HOSTNAME=$TEST_AIO_NGINX_HOSTNAME \
|
||||
AIO_NGINX_LOGS_DIR=/var/log/aio/nginx TEST_AIO_NGINX_LOGS_DIR=/var/log/aio/nginx-test \
|
||||
AIO_NGINX_PORT_HTTP=$AIO_NGINX_PORT_HTTP TEST_AIO_NGINX_PORT_HTTP=$TEST_AIO_NGINX_PORT_HTTP \
|
||||
AIO_NGINX_PORT_HTTPS=$AIO_NGINX_PORT_HTTPS TEST_AIO_NGINX_PORT_HTTPS=$TEST_AIO_NGINX_PORT_HTTPS \
|
||||
AIO_REPO_SLUG=$AIO_REPO_SLUG TEST_AIO_REPO_SLUG=$TEST_AIO_REPO_SLUG \
|
||||
AIO_SCRIPTS_JS_DIR=/usr/share/aio-scripts-js \
|
||||
AIO_SCRIPTS_SH_DIR=/usr/share/aio-scripts-sh \
|
||||
AIO_UPLOAD_HOSTNAME=$AIO_UPLOAD_HOSTNAME TEST_AIO_UPLOAD_HOSTNAME=$TEST_AIO_UPLOAD_HOSTNAME \
|
||||
AIO_UPLOAD_MAX_SIZE=$AIO_UPLOAD_MAX_SIZE TEST_AIO_UPLOAD_MAX_SIZE=$TEST_AIO_UPLOAD_MAX_SIZE \
|
||||
AIO_UPLOAD_PORT=$AIO_UPLOAD_PORT TEST_AIO_UPLOAD_PORT=$TEST_AIO_UPLOAD_PORT \
|
||||
NODE_ENV=production
|
||||
|
||||
|
||||
# Create directory for logs
|
||||
RUN mkdir /var/log/aio
|
||||
|
||||
|
||||
# Add extra package sources
|
||||
RUN apt-get update -y && apt-get install -y curl
|
||||
RUN curl --silent --show-error --location https://deb.nodesource.com/setup_6.x | bash -
|
||||
RUN curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||
|
||||
|
||||
# Install packages
|
||||
RUN apt-get update -y && apt-get install -y \
|
||||
chkconfig \
|
||||
cron \
|
||||
dnsmasq \
|
||||
nano \
|
||||
nginx \
|
||||
nodejs \
|
||||
openssl \
|
||||
rsyslog \
|
||||
yarn
|
||||
RUN yarn global add pm2@2
|
||||
|
||||
|
||||
# Set up cronjobs
|
||||
COPY cronjobs/aio-builds-cleanup /etc/cron.d/
|
||||
RUN chmod 0744 /etc/cron.d/aio-builds-cleanup
|
||||
RUN crontab /etc/cron.d/aio-builds-cleanup
|
||||
RUN printenv | grep AIO_ >> /etc/environment
|
||||
|
||||
|
||||
# Set up dnsmasq
|
||||
COPY dnsmasq/dnsmasq.conf /etc/
|
||||
RUN sed -i "s|{{\$AIO_NGINX_HOSTNAME}}|$AIO_NGINX_HOSTNAME|g" /etc/dnsmasq.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_HOSTNAME|g" /etc/dnsmasq.conf
|
||||
RUN sed -i "s|{{\$TEST_AIO_NGINX_HOSTNAME}}|$TEST_AIO_NGINX_HOSTNAME|g" /etc/dnsmasq.conf
|
||||
RUN sed -i "s|{{\$TEST_AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_HOSTNAME|g" /etc/dnsmasq.conf
|
||||
|
||||
|
||||
# Set up SSL/TLS certificates
|
||||
COPY nginx/create-selfsigned-cert.sh /tmp/
|
||||
RUN chmod a+x /tmp/create-selfsigned-cert.sh
|
||||
RUN /tmp/create-selfsigned-cert.sh "selfcert-prod" "$AIO_NGINX_HOSTNAME" "$AIO_LOCALCERTS_DIR"
|
||||
RUN /tmp/create-selfsigned-cert.sh "selfcert-test" "$TEST_AIO_NGINX_HOSTNAME" "$TEST_AIO_LOCALCERTS_DIR"
|
||||
RUN rm /tmp/create-selfsigned-cert.sh
|
||||
RUN update-ca-certificates
|
||||
|
||||
|
||||
# Set up nginx (for production and testing)
|
||||
RUN rm /etc/nginx/sites-enabled/*
|
||||
|
||||
COPY nginx/aio-builds.conf /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$AIO_BUILDS_DIR|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_DOMAIN_NAME}}|$AIO_DOMAIN_NAME|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$AIO_LOCALCERTS_DIR|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_LOGS_DIR}}|$AIO_NGINX_LOGS_DIR|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$AIO_NGINX_PORT_HTTP|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$AIO_NGINX_PORT_HTTPS|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_HOSTNAME|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$AIO_UPLOAD_MAX_SIZE|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$AIO_UPLOAD_PORT|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN ln -s /etc/nginx/sites-available/aio-builds-prod.conf /etc/nginx/sites-enabled/aio-builds-prod.conf
|
||||
|
||||
COPY nginx/aio-builds.conf /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$TEST_AIO_BUILDS_DIR|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_DOMAIN_NAME}}|$TEST_AIO_DOMAIN_NAME|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$TEST_AIO_LOCALCERTS_DIR|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_LOGS_DIR}}|$TEST_AIO_NGINX_LOGS_DIR|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$TEST_AIO_NGINX_PORT_HTTP|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$TEST_AIO_NGINX_PORT_HTTPS|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_HOSTNAME|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$TEST_AIO_UPLOAD_MAX_SIZE|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$TEST_AIO_UPLOAD_PORT|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN ln -s /etc/nginx/sites-available/aio-builds-test.conf /etc/nginx/sites-enabled/aio-builds-test.conf
|
||||
|
||||
|
||||
# Set up pm2
|
||||
RUN pm2 startup systemv -u root > /dev/null
|
||||
RUN chkconfig pm2-root on
|
||||
|
||||
|
||||
# Set up the shell scripts
|
||||
COPY scripts-sh/ $AIO_SCRIPTS_SH_DIR/
|
||||
RUN chmod a+x $AIO_SCRIPTS_SH_DIR/*
|
||||
RUN find $AIO_SCRIPTS_SH_DIR -maxdepth 1 -type f -printf "%P\n" \
|
||||
| while read file; do ln -s $AIO_SCRIPTS_SH_DIR/$file /usr/local/bin/aio-${file%.*}; done
|
||||
|
||||
|
||||
# Set up the Node.js scripts
|
||||
COPY scripts-js/ $AIO_SCRIPTS_JS_DIR/
|
||||
WORKDIR $AIO_SCRIPTS_JS_DIR/
|
||||
RUN yarn install --production
|
||||
|
||||
|
||||
# Set up health check
|
||||
HEALTHCHECK --interval=5m CMD /usr/local/bin/aio-health-check
|
||||
|
||||
|
||||
# Go!
|
||||
WORKDIR /
|
||||
CMD aio-init && tail -f /dev/null
|
@ -0,0 +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
|
16
aio/aio-builds-setup/dockerbuild/dnsmasq/dnsmasq.conf
Normal file
16
aio/aio-builds-setup/dockerbuild/dnsmasq/dnsmasq.conf
Normal file
@ -0,0 +1,16 @@
|
||||
# Do not read /etc/resolv.conf. Get servers from this file instead.
|
||||
no-resolv
|
||||
server=8.8.8.8
|
||||
server=8.8.4.4
|
||||
|
||||
# Listen for DHCP and DNS requests only on this address.
|
||||
listen-address=127.0.0.1
|
||||
|
||||
# Force an IP addres for these domains.
|
||||
address=/{{$AIO_NGINX_HOSTNAME}}/127.0.0.1
|
||||
address=/{{$AIO_UPLOAD_HOSTNAME}}/127.0.0.1
|
||||
address=/{{$TEST_AIO_NGINX_HOSTNAME}}/127.0.0.1
|
||||
address=/{{$TEST_AIO_UPLOAD_HOSTNAME}}/127.0.0.1
|
||||
|
||||
# Run as root (required from inside docker container).
|
||||
user=root
|
87
aio/aio-builds-setup/dockerbuild/nginx/aio-builds.conf
Normal file
87
aio/aio-builds-setup/dockerbuild/nginx/aio-builds.conf
Normal file
@ -0,0 +1,87 @@
|
||||
# Redirect all HTTP traffic to HTTPS
|
||||
server {
|
||||
server_name _;
|
||||
|
||||
listen {{$AIO_NGINX_PORT_HTTP}} default_server;
|
||||
listen [::]:{{$AIO_NGINX_PORT_HTTP}};
|
||||
|
||||
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
|
||||
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
|
||||
|
||||
# Ideally we want 308 (permanent + keep original method),
|
||||
# but it is relatively new and not supported by some clients (e.g. cURL).
|
||||
return 307 https://$host:{{$AIO_NGINX_PORT_HTTPS}}$request_uri;
|
||||
}
|
||||
|
||||
# Serve PR-preview requests
|
||||
server {
|
||||
server_name "~^pr(?<pr>[1-9][0-9]*)-(?<sha>[0-9a-f]{40})\.";
|
||||
|
||||
listen {{$AIO_NGINX_PORT_HTTPS}} ssl;
|
||||
listen [::]:{{$AIO_NGINX_PORT_HTTPS}} ssl;
|
||||
|
||||
ssl_certificate {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.crt;
|
||||
ssl_certificate_key {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.key;
|
||||
|
||||
root {{$AIO_BUILDS_DIR}}/$pr/$sha;
|
||||
disable_symlinks on from=$document_root;
|
||||
index index.html;
|
||||
|
||||
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
|
||||
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
|
||||
|
||||
location "~/[^/]+\.[^/]+$" {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html =404;
|
||||
}
|
||||
}
|
||||
|
||||
# Handle all other requests
|
||||
server {
|
||||
server_name _;
|
||||
|
||||
listen {{$AIO_NGINX_PORT_HTTPS}} ssl default_server;
|
||||
listen [::]:{{$AIO_NGINX_PORT_HTTPS}} ssl;
|
||||
|
||||
ssl_certificate {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.crt;
|
||||
ssl_certificate_key {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.key;
|
||||
|
||||
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
|
||||
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
|
||||
|
||||
# Health check
|
||||
location "~^/health-check/?$" {
|
||||
add_header Content-Type text/plain;
|
||||
return 200 '';
|
||||
}
|
||||
|
||||
# Upload builds
|
||||
location "~^/create-build/(?<pr>[1-9][0-9]*)/(?<sha>[0-9a-f]{40})/?$" {
|
||||
if ($request_method != "POST") {
|
||||
add_header Allow "POST";
|
||||
return 405;
|
||||
}
|
||||
|
||||
client_body_temp_path /tmp/aio-create-builds;
|
||||
client_body_buffer_size 128K;
|
||||
client_max_body_size {{$AIO_UPLOAD_MAX_SIZE}};
|
||||
client_body_in_file_only on;
|
||||
|
||||
proxy_pass_request_headers on;
|
||||
proxy_set_header X-FILE $request_body_file;
|
||||
proxy_set_body off;
|
||||
proxy_redirect off;
|
||||
proxy_method GET;
|
||||
proxy_pass http://{{$AIO_UPLOAD_HOSTNAME}}:{{$AIO_UPLOAD_PORT}}$request_uri;
|
||||
|
||||
resolver 127.0.0.1;
|
||||
}
|
||||
|
||||
# Everything else
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
}
|
20
aio/aio-builds-setup/dockerbuild/nginx/create-selfsigned-cert.sh
Executable file
20
aio/aio-builds-setup/dockerbuild/nginx/create-selfsigned-cert.sh
Executable file
@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
set -eu -o pipefail
|
||||
|
||||
|
||||
# Variables
|
||||
confFile=/tmp/$1.conf
|
||||
domainName=$2
|
||||
outDir=$3
|
||||
|
||||
|
||||
# Create certificate
|
||||
cp /etc/ssl/openssl.cnf "$confFile"
|
||||
echo "[subjectAltName]" >> "$confFile"
|
||||
echo "subjectAltName = DNS:$domainName, DNS:*.$domainName" >> "$confFile"
|
||||
mkdir -p $outDir
|
||||
openssl req -days 365 -newkey rsa:2048 -nodes -sha256 -x509 \
|
||||
-config "$confFile" -extensions subjectAltName -subj "/CN=$domainName" \
|
||||
-out "$outDir/$domainName.crt" -keyout "$outDir/$domainName.key"
|
||||
chmod -R 400 "$outDir"
|
||||
cp "$outDir/$domainName.crt" /usr/local/share/ca-certificates
|
1
aio/aio-builds-setup/dockerbuild/scripts-js/.gitignore
vendored
Normal file
1
aio/aio-builds-setup/dockerbuild/scripts-js/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/dist/
|
@ -0,0 +1,71 @@
|
||||
// Imports
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as shell from 'shelljs';
|
||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||
|
||||
// Classes
|
||||
export class BuildCleaner {
|
||||
// Constructor
|
||||
constructor(protected buildsDir: string, protected repoSlug: string, protected githubToken: string) {
|
||||
assertNotMissingOrEmpty('buildsDir', buildsDir);
|
||||
assertNotMissingOrEmpty('repoSlug', repoSlug);
|
||||
assertNotMissingOrEmpty('githubToken', githubToken);
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
public cleanUp(): Promise<void> {
|
||||
return Promise.all([
|
||||
this.getExistingBuildNumbers(),
|
||||
this.getOpenPrNumbers(),
|
||||
]).then(([existingBuilds, openPrs]) => this.removeUnnecessaryBuilds(existingBuilds, openPrs));
|
||||
}
|
||||
|
||||
// Methods - Protected
|
||||
protected getExistingBuildNumbers(): Promise<number[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readdir(this.buildsDir, (err, files) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
const buildNumbers = files.
|
||||
map(Number). // Convert string to number
|
||||
filter(Boolean); // Ignore NaN (or 0), because they are not builds
|
||||
|
||||
resolve(buildNumbers);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected getOpenPrNumbers(): Promise<number[]> {
|
||||
const githubPullRequests = new GithubPullRequests(this.githubToken, this.repoSlug);
|
||||
|
||||
return githubPullRequests.
|
||||
fetchAll('open').
|
||||
then(prs => prs.map(pr => pr.number));
|
||||
}
|
||||
|
||||
protected removeDir(dir: string) {
|
||||
try {
|
||||
// Undocumented signature (see https://github.com/shelljs/shelljs/pull/663).
|
||||
(shell as any).chmod('-R', 'a+w', dir);
|
||||
shell.rm('-rf', dir);
|
||||
} catch (err) {
|
||||
console.error(`ERROR: Unable to remove '${dir}' due to:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
protected removeUnnecessaryBuilds(existingBuildNumbers: number[], openPrNumbers: number[]) {
|
||||
const toRemove = existingBuildNumbers.filter(num => !openPrNumbers.includes(num));
|
||||
|
||||
console.log(`Existing builds: ${existingBuildNumbers.length}`);
|
||||
console.log(`Open pull requests: ${openPrNumbers.length}`);
|
||||
console.log(`Removing ${toRemove.length} build(s): ${toRemove.join(', ')}`);
|
||||
|
||||
toRemove.
|
||||
map(num => path.join(this.buildsDir, String(num))).
|
||||
forEach(dir => this.removeDir(dir));
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
// Imports
|
||||
import {getEnvVar} from '../common/utils';
|
||||
import {BuildCleaner} from './build-cleaner';
|
||||
|
||||
// Constants
|
||||
const AIO_BUILDS_DIR = getEnvVar('AIO_BUILDS_DIR');
|
||||
const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN', true);
|
||||
const AIO_REPO_SLUG = getEnvVar('AIO_REPO_SLUG');
|
||||
|
||||
// Run
|
||||
_main();
|
||||
|
||||
// Functions
|
||||
function _main() {
|
||||
console.log(`[${new Date()}] - Cleaning up builds...`);
|
||||
|
||||
const buildCleaner = new BuildCleaner(AIO_BUILDS_DIR, AIO_REPO_SLUG, AIO_GITHUB_TOKEN);
|
||||
|
||||
buildCleaner.cleanUp().catch(err => {
|
||||
console.error('ERROR:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
// Imports
|
||||
import {IncomingMessage} from 'http';
|
||||
import * as https from 'https';
|
||||
import {assertNotMissingOrEmpty} from './utils';
|
||||
|
||||
// Constants
|
||||
const GITHUB_HOSTNAME = 'api.github.com';
|
||||
|
||||
// Interfaces - Types
|
||||
interface RequestParams {
|
||||
[key: string]: string | number;
|
||||
}
|
||||
|
||||
type RequestParamsOrNull = RequestParams | null;
|
||||
|
||||
// Classes
|
||||
export class GithubApi {
|
||||
protected requestHeaders: {[key: string]: string};
|
||||
|
||||
// Constructor
|
||||
constructor(githubToken: string) {
|
||||
assertNotMissingOrEmpty('githubToken', githubToken);
|
||||
|
||||
this.requestHeaders = {
|
||||
'Authorization': `token ${githubToken}`,
|
||||
'User-Agent': `Node/${process.versions.node}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
public get<T>(pathname: string, params?: RequestParamsOrNull): Promise<T> {
|
||||
const path = this.buildPath(pathname, params);
|
||||
return this.request<T>('get', path);
|
||||
}
|
||||
|
||||
public post<T>(pathname: string, params?: RequestParamsOrNull, data?: any): Promise<T> {
|
||||
const path = this.buildPath(pathname, params);
|
||||
return this.request<T>('post', path, data);
|
||||
}
|
||||
|
||||
// Methods - Protected
|
||||
protected buildPath(pathname: string, params?: RequestParamsOrNull): string {
|
||||
if (params == null) {
|
||||
return pathname;
|
||||
}
|
||||
|
||||
const search = (params === null) ? '' : this.serializeSearchParams(params);
|
||||
const joiner = search && '?';
|
||||
|
||||
return `${pathname}${joiner}${search}`;
|
||||
}
|
||||
|
||||
protected getPaginated<T>(pathname: string, baseParams: RequestParams = {}, currentPage: number = 0): Promise<T[]> {
|
||||
const perPage = 100;
|
||||
const params = {
|
||||
...baseParams,
|
||||
page: currentPage,
|
||||
per_page: perPage,
|
||||
};
|
||||
|
||||
return this.get<T[]>(pathname, params).then(items => {
|
||||
if (items.length < perPage) {
|
||||
return items;
|
||||
}
|
||||
|
||||
return this.getPaginated(pathname, baseParams, currentPage + 1).then(moreItems => [...items, ...moreItems]);
|
||||
});
|
||||
}
|
||||
|
||||
protected request<T>(method: string, path: string, data: any = null): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const options = {
|
||||
headers: {...this.requestHeaders},
|
||||
host: GITHUB_HOSTNAME,
|
||||
method,
|
||||
path,
|
||||
};
|
||||
|
||||
const onError = (statusCode: number, responseText: string) => {
|
||||
const url = `https://${GITHUB_HOSTNAME}${path}`;
|
||||
reject(`Request to '${url}' failed (status: ${statusCode}): ${responseText}`);
|
||||
};
|
||||
const onSuccess = (responseText: string) => {
|
||||
try { resolve(JSON.parse(responseText)); } catch (err) { reject(err); }
|
||||
};
|
||||
const onResponse = (res: IncomingMessage) => {
|
||||
const statusCode = res.statusCode || -1;
|
||||
const isSuccess = (200 <= statusCode) && (statusCode < 400);
|
||||
let responseText = '';
|
||||
|
||||
res.
|
||||
on('data', d => responseText += d).
|
||||
on('end', () => isSuccess ? onSuccess(responseText) : onError(statusCode, responseText)).
|
||||
on('error', reject);
|
||||
};
|
||||
|
||||
https.
|
||||
request(options, onResponse).
|
||||
on('error', reject).
|
||||
end(data && JSON.stringify(data));
|
||||
});
|
||||
}
|
||||
|
||||
protected serializeSearchParams(params: RequestParams): string {
|
||||
return Object.keys(params).
|
||||
filter(key => params[key] != null).
|
||||
map(key => `${key}=${encodeURIComponent(String(params[key]))}`).
|
||||
join('&');
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
// Imports
|
||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||
import {GithubApi} from './github-api';
|
||||
|
||||
// Interfaces - Types
|
||||
export interface PullRequest {
|
||||
number: number;
|
||||
user: {login: string};
|
||||
}
|
||||
|
||||
export type PullRequestState = 'all' | 'closed' | 'open';
|
||||
|
||||
// Classes
|
||||
export class GithubPullRequests extends GithubApi {
|
||||
// Constructor
|
||||
constructor(githubToken: string, protected repoSlug: string) {
|
||||
super(githubToken);
|
||||
assertNotMissingOrEmpty('repoSlug', repoSlug);
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
public addComment(pr: number, body: string): Promise<void> {
|
||||
if (!(pr > 0)) {
|
||||
throw new Error(`Invalid PR number: ${pr}`);
|
||||
} else if (!body) {
|
||||
throw new Error(`Invalid or empty comment body: ${body}`);
|
||||
}
|
||||
|
||||
return this.post<void>(`/repos/${this.repoSlug}/issues/${pr}/comments`, null, {body});
|
||||
}
|
||||
|
||||
public fetch(pr: number): Promise<PullRequest> {
|
||||
return this.get<PullRequest>(`/repos/${this.repoSlug}/pulls/${pr}`);
|
||||
}
|
||||
|
||||
public fetchAll(state: PullRequestState = 'all'): Promise<PullRequest[]> {
|
||||
console.log(`Fetching ${state} pull requests...`);
|
||||
|
||||
const pathname = `/repos/${this.repoSlug}/pulls`;
|
||||
const params = {state};
|
||||
|
||||
return this.getPaginated<PullRequest>(pathname, params);
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
// Imports
|
||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||
import {GithubApi} from './github-api';
|
||||
|
||||
// Interfaces - Types
|
||||
interface Team {
|
||||
id: number;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
interface TeamMembership {
|
||||
state: string;
|
||||
}
|
||||
|
||||
// Classes
|
||||
export class GithubTeams extends GithubApi {
|
||||
// Constructor
|
||||
constructor(githubToken: string, protected organization: string) {
|
||||
super(githubToken);
|
||||
assertNotMissingOrEmpty('organization', organization);
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
public fetchAll(): Promise<Team[]> {
|
||||
return this.getPaginated<Team>(`/orgs/${this.organization}/teams`);
|
||||
}
|
||||
|
||||
public isMemberById(username: string, teamIds: number[]): Promise<boolean> {
|
||||
const getMembership = (teamId: number) =>
|
||||
this.get<TeamMembership>(`/teams/${teamId}/memberships/${username}`).
|
||||
then(membership => membership.state === 'active').
|
||||
catch(() => false);
|
||||
const reduceFn = (promise: Promise<boolean>, teamId: number) =>
|
||||
promise.then(isMember => isMember || getMembership(teamId));
|
||||
|
||||
return teamIds.reduce(reduceFn, Promise.resolve(false));
|
||||
}
|
||||
|
||||
public isMemberBySlug(username: string, teamSlugs: string[]): Promise<boolean> {
|
||||
return this.fetchAll().
|
||||
then(teams => teams.filter(team => teamSlugs.includes(team.slug)).map(team => team.id)).
|
||||
then(teamIds => this.isMemberById(username, teamIds)).
|
||||
catch(() => false);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
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');
|
||||
const config = {
|
||||
helpers,
|
||||
random: true,
|
||||
spec_files: specFiles,
|
||||
stopSpecOnExpectationFailure: true,
|
||||
};
|
||||
|
||||
process.on('unhandledRejection', (reason: any) => console.log('Unhandled rejection:', reason));
|
||||
|
||||
const runner = new Jasmine();
|
||||
runner.loadConfig(config);
|
||||
runner.onComplete((passed: boolean) => process.exit(passed ? 0 : 1));
|
||||
runner.execute();
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
// Functions
|
||||
export const assertNotMissingOrEmpty = (name: string, value: string | null | undefined) => {
|
||||
if (!value) {
|
||||
throw new Error(`Missing or empty required parameter '${name}'!`);
|
||||
}
|
||||
};
|
||||
|
||||
export const getEnvVar = (name: string, isOptional = false): string => {
|
||||
const value = process.env[name];
|
||||
|
||||
if (!isOptional && !value) {
|
||||
console.error(`ERROR: Missing required environment variable '${name}'!`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return value || '';
|
||||
};
|
@ -0,0 +1,81 @@
|
||||
// Imports
|
||||
import * as cp from 'child_process';
|
||||
import {EventEmitter} from 'events';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as shell from 'shelljs';
|
||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||
import {CreatedBuildEvent} from './build-events';
|
||||
import {UploadError} from './upload-error';
|
||||
|
||||
// Classes
|
||||
export class BuildCreator extends EventEmitter {
|
||||
// Constructor
|
||||
constructor(protected buildsDir: string) {
|
||||
super();
|
||||
assertNotMissingOrEmpty('buildsDir', buildsDir);
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
public create(pr: string, sha: string, archivePath: string): Promise<any> {
|
||||
const prDir = path.join(this.buildsDir, pr);
|
||||
const shaDir = path.join(prDir, sha);
|
||||
let dirToRemoveOnError: string;
|
||||
|
||||
return Promise.
|
||||
all([this.exists(prDir), this.exists(shaDir)]).
|
||||
then(([prDirExisted, shaDirExisted]) => {
|
||||
if (shaDirExisted) {
|
||||
throw new UploadError(403, `Request to overwrite existing directory: ${shaDir}`);
|
||||
}
|
||||
|
||||
dirToRemoveOnError = prDirExisted ? shaDir : prDir;
|
||||
|
||||
return Promise.resolve().
|
||||
then(() => shell.mkdir('-p', shaDir)).
|
||||
then(() => this.extractArchive(archivePath, shaDir)).
|
||||
then(() => this.emit(CreatedBuildEvent.type, new CreatedBuildEvent(+pr, sha)));
|
||||
}).
|
||||
catch(err => {
|
||||
if (dirToRemoveOnError) {
|
||||
shell.rm('-rf', dirToRemoveOnError);
|
||||
}
|
||||
|
||||
if (!(err instanceof UploadError)) {
|
||||
err = new UploadError(500, `Error while uploading to directory: ${shaDir}\n${err}`);
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
// Methods - Protected
|
||||
protected exists(fileOrDir: string): Promise<boolean> {
|
||||
return new Promise(resolve => fs.access(fileOrDir, err => resolve(!err)));
|
||||
}
|
||||
|
||||
protected extractArchive(inputFile: string, outputDir: string): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const cmd = `tar --extract --gzip --directory "${outputDir}" --file "${inputFile}"`;
|
||||
|
||||
cp.exec(cmd, (err, _stdout, stderr) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.warn(stderr);
|
||||
}
|
||||
|
||||
try {
|
||||
// Undocumented signature (see https://github.com/shelljs/shelljs/pull/663).
|
||||
(shell as any).chmod('-R', 'a-w', outputDir);
|
||||
shell.rm('-f', inputFile);
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
// Classes
|
||||
export class BuildEvent {
|
||||
// Constructor
|
||||
constructor(public type: string, public pr: number, public sha: string) {}
|
||||
}
|
||||
|
||||
export class CreatedBuildEvent extends BuildEvent {
|
||||
// Properties - Public, Static
|
||||
public static type = 'build.created';
|
||||
|
||||
// Constructor
|
||||
constructor(pr: number, sha: string) {
|
||||
super(CreatedBuildEvent.type, pr, sha);
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
// Imports
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||
import {GithubTeams} from '../common/github-teams';
|
||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||
import {UploadError} from './upload-error';
|
||||
|
||||
// Interfaces - Types
|
||||
interface JwtPayload {
|
||||
slug: string;
|
||||
'pull-request': number;
|
||||
}
|
||||
|
||||
// Classes
|
||||
export class BuildVerifier {
|
||||
// Properties - Protected
|
||||
protected githubPullRequests: GithubPullRequests;
|
||||
protected githubTeams: GithubTeams;
|
||||
|
||||
// Constructor
|
||||
constructor(protected secret: string, githubToken: string, protected repoSlug: string, organization: string,
|
||||
protected allowedTeamSlugs: string[]) {
|
||||
assertNotMissingOrEmpty('secret', secret);
|
||||
assertNotMissingOrEmpty('githubToken', githubToken);
|
||||
assertNotMissingOrEmpty('repoSlug', repoSlug);
|
||||
assertNotMissingOrEmpty('organization', organization);
|
||||
assertNotMissingOrEmpty('allowedTeamSlugs', allowedTeamSlugs && allowedTeamSlugs.join(''));
|
||||
|
||||
this.githubPullRequests = new GithubPullRequests(githubToken, repoSlug);
|
||||
this.githubTeams = new GithubTeams(githubToken, organization);
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
public getPrAuthorTeamMembership(pr: number): Promise<{author: string, isMember: boolean}> {
|
||||
return Promise.resolve().
|
||||
then(() => this.githubPullRequests.fetch(pr)).
|
||||
then(prInfo => prInfo.user.login).
|
||||
then(author => this.githubTeams.isMemberBySlug(author, this.allowedTeamSlugs).
|
||||
then(isMember => ({author, isMember})));
|
||||
}
|
||||
|
||||
public verify(expectedPr: number, authHeader: string): Promise<void> {
|
||||
return Promise.resolve().
|
||||
then(() => this.extractJwtString(authHeader)).
|
||||
then(jwtString => this.verifyJwt(expectedPr, jwtString)).
|
||||
then(jwtPayload => this.verifyPr(jwtPayload['pull-request'])).
|
||||
catch(err => { throw new UploadError(403, `Error while verifying upload for PR ${expectedPr}: ${err}`); });
|
||||
}
|
||||
|
||||
// Methods - Protected
|
||||
protected extractJwtString(input: string): string {
|
||||
return input.replace(/^token +/i, '');
|
||||
}
|
||||
|
||||
protected verifyJwt(expectedPr: number, token: string): Promise<JwtPayload> {
|
||||
return new Promise((resolve, reject) => {
|
||||
jwt.verify(token, this.secret, {issuer: 'Travis CI, GmbH'}, (err, payload) => {
|
||||
if (err) {
|
||||
reject(err.message || err);
|
||||
} else if (payload.slug !== this.repoSlug) {
|
||||
reject(`jwt slug invalid. expected: ${this.repoSlug}`);
|
||||
} else if (payload['pull-request'] !== expectedPr) {
|
||||
reject(`jwt pull-request invalid. expected: ${expectedPr}`);
|
||||
} else {
|
||||
resolve(payload);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected verifyPr(pr: number): Promise<void> {
|
||||
return this.getPrAuthorTeamMembership(pr).
|
||||
then(({author, isMember}) => isMember ? Promise.resolve() : Promise.reject(
|
||||
`User '${author}' is not an active member of any of the following teams: ` +
|
||||
`${this.allowedTeamSlugs.join(', ')}`,
|
||||
));
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
// Imports
|
||||
import {getEnvVar} from '../common/utils';
|
||||
import {BuildVerifier} from './build-verifier';
|
||||
|
||||
// Run
|
||||
_main();
|
||||
|
||||
// Functions
|
||||
function _main() {
|
||||
const secret = 'unused';
|
||||
const githubToken = getEnvVar('AIO_GITHUB_TOKEN');
|
||||
const repoSlug = getEnvVar('AIO_REPO_SLUG');
|
||||
const organization = getEnvVar('AIO_GITHUB_ORGANIZATION');
|
||||
const allowedTeamSlugs = getEnvVar('AIO_GITHUB_TEAM_SLUGS').split(',');
|
||||
const pr = +getEnvVar('AIO_PREVERIFY_PR');
|
||||
|
||||
const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, organization, allowedTeamSlugs);
|
||||
|
||||
// Exit codes:
|
||||
// - 0: The PR author is a member.
|
||||
// - 1: The PR author is not a member.
|
||||
// - 2: An error occurred.
|
||||
buildVerifier.getPrAuthorTeamMembership(pr).
|
||||
then(({author, isMember}) => {
|
||||
if (isMember) {
|
||||
process.exit(0);
|
||||
} else {
|
||||
const errorMessage = `User '${author}' is not an active member of any of the following teams: ` +
|
||||
`${allowedTeamSlugs.join(', ')}`;
|
||||
onError(errorMessage, 1);
|
||||
}
|
||||
}).
|
||||
catch(err => onError(err, 2));
|
||||
}
|
||||
|
||||
function onError(err: string, exitCode: number) {
|
||||
console.error(err);
|
||||
process.exit(exitCode || 1);
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
// Imports
|
||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||
import {BuildVerifier} from './build-verifier';
|
||||
|
||||
// Run
|
||||
// TODO(gkalpak): Add e2e tests to cover these interactions as well.
|
||||
GithubPullRequests.prototype.addComment = () => Promise.resolve();
|
||||
BuildVerifier.prototype.verify = () => Promise.resolve();
|
||||
// tslint:disable-next-line: no-var-requires
|
||||
require('./index');
|
@ -0,0 +1,35 @@
|
||||
// TODO(gkalpak): Find more suitable way to run as `www-data`.
|
||||
process.setuid('www-data');
|
||||
|
||||
// Imports
|
||||
import {getEnvVar} from '../common/utils';
|
||||
import {uploadServerFactory} from './upload-server-factory';
|
||||
|
||||
// Constants
|
||||
const AIO_BUILDS_DIR = getEnvVar('AIO_BUILDS_DIR');
|
||||
const AIO_DOMAIN_NAME = getEnvVar('AIO_DOMAIN_NAME');
|
||||
const AIO_GITHUB_ORGANIZATION = getEnvVar('AIO_GITHUB_ORGANIZATION');
|
||||
const AIO_GITHUB_TEAM_SLUGS = getEnvVar('AIO_GITHUB_TEAM_SLUGS');
|
||||
const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN');
|
||||
const AIO_PREVIEW_DEPLOYMENT_TOKEN = getEnvVar('AIO_PREVIEW_DEPLOYMENT_TOKEN');
|
||||
const AIO_REPO_SLUG = getEnvVar('AIO_REPO_SLUG');
|
||||
const AIO_UPLOAD_HOSTNAME = getEnvVar('AIO_UPLOAD_HOSTNAME');
|
||||
const AIO_UPLOAD_PORT = +getEnvVar('AIO_UPLOAD_PORT');
|
||||
|
||||
// Run
|
||||
_main();
|
||||
|
||||
// Functions
|
||||
function _main() {
|
||||
uploadServerFactory.
|
||||
create({
|
||||
buildsDir: AIO_BUILDS_DIR,
|
||||
domainName: AIO_DOMAIN_NAME,
|
||||
githubOrganization: AIO_GITHUB_ORGANIZATION,
|
||||
githubTeamSlugs: AIO_GITHUB_TEAM_SLUGS.split(','),
|
||||
githubToken: AIO_GITHUB_TOKEN,
|
||||
repoSlug: AIO_REPO_SLUG,
|
||||
secret: AIO_PREVIEW_DEPLOYMENT_TOKEN,
|
||||
}).
|
||||
listen(AIO_UPLOAD_PORT, AIO_UPLOAD_HOSTNAME);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
// Classes
|
||||
export class UploadError extends Error {
|
||||
// Constructor
|
||||
constructor(public status: number = 500, message?: string) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, UploadError.prototype);
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
// Imports
|
||||
import * as express from 'express';
|
||||
import * as http from 'http';
|
||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||
import {BuildCreator} from './build-creator';
|
||||
import {CreatedBuildEvent} from './build-events';
|
||||
import {BuildVerifier} from './build-verifier';
|
||||
import {UploadError} from './upload-error';
|
||||
|
||||
// Constants
|
||||
const AUTHORIZATION_HEADER = 'AUTHORIZATION';
|
||||
const X_FILE_HEADER = 'X-FILE';
|
||||
|
||||
// Interfaces - Types
|
||||
interface UploadServerConfig {
|
||||
buildsDir: string;
|
||||
domainName: string;
|
||||
githubOrganization: string;
|
||||
githubTeamSlugs: string[];
|
||||
githubToken: string;
|
||||
repoSlug: string;
|
||||
secret: string;
|
||||
}
|
||||
|
||||
// Classes
|
||||
class UploadServerFactory {
|
||||
// Methods - Public
|
||||
public create({
|
||||
buildsDir,
|
||||
domainName,
|
||||
githubOrganization,
|
||||
githubTeamSlugs,
|
||||
githubToken,
|
||||
repoSlug,
|
||||
secret,
|
||||
}: UploadServerConfig): http.Server {
|
||||
assertNotMissingOrEmpty('domainName', domainName);
|
||||
|
||||
const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, githubOrganization, githubTeamSlugs);
|
||||
const buildCreator = this.createBuildCreator(buildsDir, githubToken, repoSlug, domainName);
|
||||
|
||||
const middleware = this.createMiddleware(buildVerifier, buildCreator);
|
||||
const httpServer = http.createServer(middleware);
|
||||
|
||||
httpServer.on('listening', () => {
|
||||
const info = httpServer.address();
|
||||
console.info(`Up and running (and listening on ${info.address}:${info.port})...`);
|
||||
});
|
||||
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
// Methods - Protected
|
||||
protected createBuildCreator(buildsDir: string, githubToken: string, repoSlug: string,
|
||||
domainName: string): BuildCreator {
|
||||
const buildCreator = new BuildCreator(buildsDir);
|
||||
const githubPullRequests = new GithubPullRequests(githubToken, repoSlug);
|
||||
|
||||
buildCreator.on(CreatedBuildEvent.type, ({pr, sha}: CreatedBuildEvent) => {
|
||||
const body = `The angular.io preview for ${sha.slice(0, 7)} is available [here][1].\n\n` +
|
||||
`[1]: https://pr${pr}-${sha}.${domainName}/`;
|
||||
|
||||
githubPullRequests.addComment(pr, body);
|
||||
});
|
||||
|
||||
return buildCreator;
|
||||
}
|
||||
|
||||
protected createMiddleware(buildVerifier: BuildVerifier, buildCreator: BuildCreator): express.Express {
|
||||
const middleware = express();
|
||||
|
||||
middleware.get(/^\/create-build\/([1-9][0-9]*)\/([0-9a-f]{40})\/?$/, (req, res) => {
|
||||
const pr = req.params[0];
|
||||
const sha = req.params[1];
|
||||
const archive = req.header(X_FILE_HEADER);
|
||||
const authHeader = req.header(AUTHORIZATION_HEADER);
|
||||
|
||||
if (!authHeader) {
|
||||
this.throwRequestError(401, `Missing or empty '${AUTHORIZATION_HEADER}' header`, req);
|
||||
} else if (!archive) {
|
||||
this.throwRequestError(400, `Missing or empty '${X_FILE_HEADER}' header`, req);
|
||||
}
|
||||
|
||||
buildVerifier.
|
||||
verify(+pr, authHeader).
|
||||
then(() => buildCreator.create(pr, sha, archive)).
|
||||
then(() => res.sendStatus(201)).
|
||||
catch(err => this.respondWithError(res, err));
|
||||
});
|
||||
middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200));
|
||||
middleware.get('*', req => this.throwRequestError(404, 'Unknown resource', req));
|
||||
middleware.all('*', req => this.throwRequestError(405, 'Unsupported method', req));
|
||||
middleware.use((err: any, _req: any, res: express.Response, _next: any) => this.respondWithError(res, err));
|
||||
|
||||
return middleware;
|
||||
}
|
||||
|
||||
protected respondWithError(res: express.Response, err: any) {
|
||||
if (!(err instanceof UploadError)) {
|
||||
err = new UploadError(500, String((err && err.message) || err));
|
||||
}
|
||||
|
||||
const statusText = http.STATUS_CODES[err.status] || '???';
|
||||
console.error(`Upload error: ${err.status} - ${statusText}`);
|
||||
console.error(err.message);
|
||||
|
||||
res.status(err.status).end(err.message);
|
||||
}
|
||||
|
||||
protected throwRequestError(status: number, error: string, req: express.Request) {
|
||||
throw new UploadError(status, `${error} in request: ${req.method} ${req.originalUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Exports
|
||||
export const uploadServerFactory = new UploadServerFactory();
|
@ -0,0 +1,191 @@
|
||||
// Imports
|
||||
import * as cp from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as http from 'http';
|
||||
import * as path from 'path';
|
||||
import * as shell from 'shelljs';
|
||||
import {getEnvVar} from '../common/utils';
|
||||
|
||||
// Constans
|
||||
const SERVER_USER = 'www-data';
|
||||
const TEST_AIO_BUILDS_DIR = getEnvVar('TEST_AIO_BUILDS_DIR');
|
||||
const TEST_AIO_NGINX_HOSTNAME = getEnvVar('TEST_AIO_NGINX_HOSTNAME');
|
||||
const TEST_AIO_NGINX_PORT_HTTP = +getEnvVar('TEST_AIO_NGINX_PORT_HTTP');
|
||||
const TEST_AIO_NGINX_PORT_HTTPS = +getEnvVar('TEST_AIO_NGINX_PORT_HTTPS');
|
||||
const TEST_AIO_UPLOAD_HOSTNAME = getEnvVar('TEST_AIO_UPLOAD_HOSTNAME');
|
||||
const TEST_AIO_UPLOAD_MAX_SIZE = +getEnvVar('TEST_AIO_UPLOAD_MAX_SIZE');
|
||||
const TEST_AIO_UPLOAD_PORT = +getEnvVar('TEST_AIO_UPLOAD_PORT');
|
||||
|
||||
// Interfaces - Types
|
||||
export interface CmdResult { success: boolean; err: Error; stdout: string; stderr: string; }
|
||||
export interface FileSpecs { content?: string; size?: number; }
|
||||
|
||||
export type CleanUpFn = () => void;
|
||||
export type TestSuiteFactory = (scheme: string, port: number) => void;
|
||||
export type VerifyCmdResultFn = (result: CmdResult) => void;
|
||||
|
||||
// Classes
|
||||
class Helper {
|
||||
// Properties - Public
|
||||
public get buildsDir() { return TEST_AIO_BUILDS_DIR; }
|
||||
public get nginxHostname() { return TEST_AIO_NGINX_HOSTNAME; }
|
||||
public get nginxPortHttp() { return TEST_AIO_NGINX_PORT_HTTP; }
|
||||
public get nginxPortHttps() { return TEST_AIO_NGINX_PORT_HTTPS; }
|
||||
public get serverUser() { return SERVER_USER; }
|
||||
public get uploadHostname() { return TEST_AIO_UPLOAD_HOSTNAME; }
|
||||
public get uploadPort() { return TEST_AIO_UPLOAD_PORT; }
|
||||
public get uploadMaxSize() { return TEST_AIO_UPLOAD_MAX_SIZE; }
|
||||
|
||||
// Properties - Protected
|
||||
protected cleanUpFns: CleanUpFn[] = [];
|
||||
protected portPerScheme: {[scheme: string]: number} = {
|
||||
http: this.nginxPortHttp,
|
||||
https: this.nginxPortHttps,
|
||||
};
|
||||
|
||||
// Constructor
|
||||
constructor() {
|
||||
shell.mkdir('-p', this.buildsDir);
|
||||
shell.exec(`chown -R ${this.serverUser} ${this.buildsDir}`);
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
public cleanUp() {
|
||||
while (this.cleanUpFns.length) {
|
||||
// Clean-up fns remove themselves from the list.
|
||||
this.cleanUpFns[0]();
|
||||
}
|
||||
|
||||
if (fs.readdirSync(this.buildsDir).length) {
|
||||
throw new Error(`Directory '${this.buildsDir}' is not empty after clean-up.`);
|
||||
}
|
||||
}
|
||||
|
||||
public createDummyArchive(pr: string, sha: string, archivePath: string): CleanUpFn {
|
||||
const inputDir = path.join(this.buildsDir, 'uploaded', pr, sha);
|
||||
const cmd1 = `tar --create --gzip --directory "${inputDir}" --file "${archivePath}" .`;
|
||||
const cmd2 = `chown ${this.serverUser} ${archivePath}`;
|
||||
|
||||
const cleanUpTemp = this.createDummyBuild(`uploaded/${pr}`, sha, true);
|
||||
shell.exec(cmd1);
|
||||
shell.exec(cmd2);
|
||||
cleanUpTemp();
|
||||
|
||||
return this.createCleanUpFn(() => shell.rm('-rf', archivePath));
|
||||
}
|
||||
|
||||
public createDummyBuild(pr: string, sha: string, force = false): CleanUpFn {
|
||||
const prDir = path.join(this.buildsDir, pr);
|
||||
const shaDir = path.join(prDir, sha);
|
||||
const idxPath = path.join(shaDir, 'index.html');
|
||||
const barPath = path.join(shaDir, 'foo', 'bar.js');
|
||||
|
||||
this.writeFile(idxPath, {content: `PR: ${pr} | SHA: ${sha} | File: /index.html`}, force);
|
||||
this.writeFile(barPath, {content: `PR: ${pr} | SHA: ${sha} | File: /foo/bar.js`}, force);
|
||||
shell.exec(`chown -R ${this.serverUser} ${prDir}`);
|
||||
|
||||
return this.createCleanUpFn(() => shell.rm('-rf', prDir));
|
||||
}
|
||||
|
||||
public deletePrDir(pr: string) {
|
||||
const prDir = path.join(this.buildsDir, pr);
|
||||
|
||||
if (fs.existsSync(prDir)) {
|
||||
// Undocumented signature (see https://github.com/shelljs/shelljs/pull/663).
|
||||
(shell as any).chmod('-R', 'a+w', prDir);
|
||||
shell.rm('-rf', prDir);
|
||||
}
|
||||
}
|
||||
|
||||
public readBuildFile(pr: string, sha: string, relFilePath: string): string {
|
||||
const absFilePath = path.join(this.buildsDir, pr, sha, relFilePath);
|
||||
return fs.readFileSync(absFilePath, 'utf8');
|
||||
}
|
||||
|
||||
public runCmd(cmd: string, opts: cp.ExecFileOptions = {}): Promise<CmdResult> {
|
||||
return new Promise(resolve => {
|
||||
const proc = cp.exec(cmd, opts, (err, stdout, stderr) => resolve({success: !err, err, stdout, stderr}));
|
||||
this.createCleanUpFn(() => proc.kill());
|
||||
});
|
||||
}
|
||||
|
||||
public runForAllSupportedSchemes(suiteFactory: TestSuiteFactory) {
|
||||
Object.keys(this.portPerScheme).forEach(scheme => suiteFactory(scheme, this.portPerScheme[scheme]));
|
||||
}
|
||||
|
||||
public verifyResponse(status: number | [number, string], regex = /^/): VerifyCmdResultFn {
|
||||
let statusCode: number;
|
||||
let statusText: string;
|
||||
|
||||
if (Array.isArray(status)) {
|
||||
statusCode = status[0];
|
||||
statusText = status[1];
|
||||
} else {
|
||||
statusCode = status;
|
||||
statusText = http.STATUS_CODES[statusCode];
|
||||
}
|
||||
|
||||
return (result: CmdResult) => {
|
||||
const [headers, body] = result.stdout.
|
||||
split(/(?:\r?\n){2,}/).
|
||||
map(s => s.trim()).
|
||||
slice(-2);
|
||||
|
||||
if (!result.success) {
|
||||
console.log('Stdout:', result.stdout);
|
||||
console.log('Stderr:', result.stderr);
|
||||
console.log('Error:', result.err);
|
||||
}
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(headers).toContain(`${statusCode} ${statusText}`);
|
||||
expect(body).toMatch(regex);
|
||||
};
|
||||
}
|
||||
|
||||
public writeBuildFile(pr: string, sha: string, relFilePath: string, content: string): CleanUpFn {
|
||||
const absFilePath = path.join(this.buildsDir, pr, sha, relFilePath);
|
||||
return this.writeFile(absFilePath, {content}, true);
|
||||
}
|
||||
|
||||
public writeFile(filePath: string, {content, size}: FileSpecs, force = false): CleanUpFn {
|
||||
if (!force && fs.existsSync(filePath)) {
|
||||
throw new Error(`Refusing to overwrite existing file '${filePath}'.`);
|
||||
}
|
||||
|
||||
let cleanUpTarget = filePath;
|
||||
while (!fs.existsSync(path.dirname(cleanUpTarget))) {
|
||||
cleanUpTarget = path.dirname(cleanUpTarget);
|
||||
}
|
||||
|
||||
shell.mkdir('-p', path.dirname(filePath));
|
||||
if (size) {
|
||||
// Create a file of the specified size.
|
||||
cp.execSync(`fallocate -l ${size} ${filePath}`);
|
||||
} else {
|
||||
// Create a file with the specified content.
|
||||
fs.writeFileSync(filePath, content || '');
|
||||
}
|
||||
shell.exec(`chown ${this.serverUser} ${filePath}`);
|
||||
|
||||
return this.createCleanUpFn(() => shell.rm('-rf', cleanUpTarget));
|
||||
}
|
||||
|
||||
// Methods - Protected
|
||||
protected createCleanUpFn(fn: Function): CleanUpFn {
|
||||
const cleanUpFn = () => {
|
||||
const idx = this.cleanUpFns.indexOf(cleanUpFn);
|
||||
if (idx !== -1) {
|
||||
this.cleanUpFns.splice(idx, 1);
|
||||
fn();
|
||||
}
|
||||
};
|
||||
|
||||
this.cleanUpFns.push(cleanUpFn);
|
||||
|
||||
return cleanUpFn;
|
||||
}
|
||||
}
|
||||
|
||||
// Exports
|
||||
export const helper = new Helper();
|
@ -0,0 +1,6 @@
|
||||
// Imports
|
||||
import {runTests} from '../common/run-tests';
|
||||
|
||||
// Run
|
||||
const specFiles = [`${__dirname}/**/*.e2e.js`];
|
||||
runTests(specFiles);
|
@ -0,0 +1,271 @@
|
||||
// Imports
|
||||
import * as path from 'path';
|
||||
import {helper as h} from './helper';
|
||||
|
||||
// Tests
|
||||
describe(`nginx`, () => {
|
||||
|
||||
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
|
||||
afterEach(() => h.cleanUp());
|
||||
|
||||
|
||||
it('should redirect HTTP to HTTPS', done => {
|
||||
const httpHost = `${h.nginxHostname}:${h.nginxPortHttp}`;
|
||||
const httpsHost = `${h.nginxHostname}:${h.nginxPortHttps}`;
|
||||
const urlMap = {
|
||||
[`http://${httpHost}/`]: `https://${httpsHost}/`,
|
||||
[`http://${httpHost}/foo`]: `https://${httpsHost}/foo`,
|
||||
[`http://foo.${httpHost}/`]: `https://foo.${httpsHost}/`,
|
||||
};
|
||||
|
||||
const verifyRedirection = (httpUrl: string) => h.runCmd(`curl -i ${httpUrl}`).then(result => {
|
||||
h.verifyResponse(307)(result);
|
||||
|
||||
const headers = result.stdout.split(/(?:\r?\n){2,}/)[0];
|
||||
expect(headers).toContain(`Location: ${urlMap[httpUrl]}`);
|
||||
});
|
||||
|
||||
Promise.
|
||||
all(Object.keys(urlMap).map(verifyRedirection)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
h.runForAllSupportedSchemes((scheme, port) => describe(`nginx (on ${scheme.toUpperCase()})`, () => {
|
||||
const hostname = h.nginxHostname;
|
||||
const host = `${hostname}:${port}`;
|
||||
const pr = '9';
|
||||
const sha9 = '9'.repeat(40);
|
||||
const sha0 = '0'.repeat(40);
|
||||
|
||||
|
||||
describe(`pr<pr>-<sha>.${host}/*`, () => {
|
||||
|
||||
beforeEach(() => {
|
||||
h.createDummyBuild(pr, sha9);
|
||||
h.createDummyBuild(pr, sha0);
|
||||
});
|
||||
|
||||
|
||||
it('should return /index.html', done => {
|
||||
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${origin}/index.html`).then(h.verifyResponse(200, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${origin}/`).then(h.verifyResponse(200, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${origin}`).then(h.verifyResponse(200, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should return /foo/bar.js', done => {
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`);
|
||||
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/bar.js`).
|
||||
then(h.verifyResponse(200, bodyRegex)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 403 for directories', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/`).then(h.verifyResponse(403)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo`).then(h.verifyResponse(403)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths to files', done => {
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz.css`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should rewrite to \'index.html\' for unknown paths that don\'t look like files', done => {
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz`).then(h.verifyResponse(200, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz/`).then(h.verifyResponse(200, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown PRs/SHAs', done => {
|
||||
const otherPr = 54321;
|
||||
const otherSha = '8'.repeat(40);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}9-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${otherPr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}9.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${otherSha}.${host}`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 if the subdomain format is wrong', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://xpr${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://prx${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://xx${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://p${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://r${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}_${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject PRs with leading zeros', done => {
|
||||
h.runCmd(`curl -iL ${scheme}://pr0${pr}-${sha9}.${host}`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
||||
const bodyRegex9 = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
const bodyRegex0 = new RegExp(`^PR: ${pr} | SHA: ${sha0} | File: /index\\.html$`);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-0${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}`).then(h.verifyResponse(200, bodyRegex9)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha0}.${host}`).then(h.verifyResponse(200, bodyRegex0)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/health-check`, () => {
|
||||
|
||||
it('should respond with 200', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-check`).then(h.verifyResponse(200)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-check/`).then(h.verifyResponse(200)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 if the path does not match exactly', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-check/foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-check-foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/health-checknfoo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foo/health-check`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foo-health-check`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foonhealth-check`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/create-build/<pr>/<sha>`, () => {
|
||||
|
||||
it('should disallow non-POST requests', done => {
|
||||
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it(`should reject files larger than ${h.uploadMaxSize}B (according to header)`, done => {
|
||||
const headers = `--header "Content-Length: ${1.5 * h.uploadMaxSize}"`;
|
||||
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`;
|
||||
|
||||
h.runCmd(`curl -iLX POST ${headers} ${url}`).
|
||||
then(h.verifyResponse([413, 'Request Entity Too Large'])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it(`should reject files larger than ${h.uploadMaxSize}B (without header)`, done => {
|
||||
const filePath = path.join(h.buildsDir, 'snapshot.tar.gz');
|
||||
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`;
|
||||
|
||||
h.writeFile(filePath, {size: 1.5 * h.uploadMaxSize});
|
||||
|
||||
h.runCmd(`curl -iLX POST --data-binary "@${filePath}" ${url}`).
|
||||
then(h.verifyResponse([413, 'Request Entity Too Large'])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should pass requests through to the upload server', done => {
|
||||
h.runCmd(`curl -iLX POST ${scheme}://${host}/create-build/${pr}/${sha9}`).
|
||||
then(h.verifyResponse(401, /Missing or empty 'AUTHORIZATION' header/)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths', done => {
|
||||
const cmdPrefix = `curl -iLX POST ${scheme}://${host}`;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`${cmdPrefix}/foo/create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/foo-create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/fooncreate-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build/foo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build-foo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-buildnfoo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build/pr${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build/${pr}/${sha9}42`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject PRs with leading zeros', done => {
|
||||
h.runCmd(`curl -iLX POST ${scheme}://${host}/create-build/0${pr}/${sha9}`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
||||
const cmdPrefix = `curl -iLX POST ${scheme}://${host}/create-build/${pr}`;
|
||||
const bodyRegex = /Missing or empty 'AUTHORIZATION' header/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`${cmdPrefix}/0${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/${sha0}`).then(h.verifyResponse(401, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/*`, () => {
|
||||
|
||||
it('should respond with 404 for unkown URLs (even if the resource exists)', done => {
|
||||
['index.html', 'foo.js', 'foo/index.html'].forEach(relFilePath => {
|
||||
const absFilePath = path.join(h.buildsDir, relFilePath);
|
||||
h.writeFile(absFilePath, {content: `File: /${relFilePath}`});
|
||||
});
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/index.html`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://foo.${host}/index.html`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://foo.${host}/`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://foo.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foo.js`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}/foo/index.html`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
});
|
@ -0,0 +1,84 @@
|
||||
// Imports
|
||||
import * as path from 'path';
|
||||
import {helper as h} from './helper';
|
||||
|
||||
// Tests
|
||||
h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme.toUpperCase()})`, () => {
|
||||
const hostname = h.nginxHostname;
|
||||
const host = `${hostname}:${port}`;
|
||||
const pr9 = '9';
|
||||
const sha9 = '9'.repeat(40);
|
||||
const sha0 = '0'.repeat(40);
|
||||
const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz');
|
||||
|
||||
const getFile = (pr: string, sha: string, file: string) =>
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha}.${host}/${file}`);
|
||||
const uploadBuild = (pr: string, sha: string, archive: string) => {
|
||||
const curlPost = 'curl -iLX POST --header "Authorization: Token FOO"';
|
||||
return h.runCmd(`${curlPost} --data-binary "@${archive}" ${scheme}://${host}/create-build/${pr}/${sha}`);
|
||||
};
|
||||
|
||||
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
|
||||
afterEach(() => {
|
||||
h.deletePrDir(pr9);
|
||||
h.cleanUp();
|
||||
});
|
||||
|
||||
|
||||
it('should be able to upload and serve a build for a new PR', done => {
|
||||
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
|
||||
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
||||
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
||||
|
||||
h.createDummyArchive(pr9, sha9, archivePath);
|
||||
|
||||
uploadBuild(pr9, sha9, archivePath).
|
||||
then(() => Promise.all([
|
||||
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
|
||||
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
|
||||
])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should be able to upload and serve a build for an existing PR', done => {
|
||||
const regexPrefix0 = `^PR: ${pr9} \\| SHA: ${sha0} \\| File:`;
|
||||
const idxContentRegex0 = new RegExp(`${regexPrefix0} \\/index\\.html$`);
|
||||
const barContentRegex0 = new RegExp(`${regexPrefix0} \\/foo\\/bar\\.js$`);
|
||||
|
||||
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
|
||||
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
||||
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
||||
|
||||
h.createDummyBuild(pr9, sha0);
|
||||
h.createDummyArchive(pr9, sha9, archivePath);
|
||||
|
||||
uploadBuild(pr9, sha9, archivePath).
|
||||
then(() => Promise.all([
|
||||
getFile(pr9, sha0, 'index.html').then(h.verifyResponse(200, idxContentRegex0)),
|
||||
getFile(pr9, sha0, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex0)),
|
||||
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
|
||||
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
|
||||
])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should not be able to overwrite a build', done => {
|
||||
const regexPrefix9 = `^PR: ${pr9} \\| SHA: ${sha9} \\| File:`;
|
||||
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
||||
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
||||
|
||||
h.createDummyBuild(pr9, sha9);
|
||||
h.createDummyArchive(pr9, sha9, archivePath);
|
||||
|
||||
uploadBuild(pr9, sha9, archivePath).
|
||||
then(h.verifyResponse(403)).
|
||||
then(() => Promise.all([
|
||||
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
|
||||
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
|
||||
])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
}));
|
@ -0,0 +1,266 @@
|
||||
// Imports
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {CmdResult, helper as h} from './helper';
|
||||
|
||||
// Tests
|
||||
describe('upload-server (on HTTP)', () => {
|
||||
const hostname = h.uploadHostname;
|
||||
const port = h.uploadPort;
|
||||
const host = `${hostname}:${port}`;
|
||||
const pr = '9';
|
||||
const sha9 = '9'.repeat(40);
|
||||
const sha0 = '0'.repeat(40);
|
||||
|
||||
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
|
||||
afterEach(() => h.cleanUp());
|
||||
|
||||
|
||||
describe(`${host}/create-build/<pr>/<sha>`, () => {
|
||||
const authorizationHeader = `--header "Authorization: Token FOO"`;
|
||||
const xFileHeader = `--header "X-File: ${h.buildsDir}/snapshot.tar.gz"`;
|
||||
const curl = `curl -iL ${authorizationHeader} ${xFileHeader}`;
|
||||
|
||||
|
||||
it('should disallow non-GET requests', done => {
|
||||
const url = `http://${host}/create-build/${pr}/${sha9}`;
|
||||
const bodyRegex = /^Unsupported method/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX POST ${url}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject requests without an \'AUTHORIZATION\' header', done => {
|
||||
const headers1 = '';
|
||||
const headers2 = '--header "AUTHORIXATION: "';
|
||||
const url = `http://${host}/create-build/${pr}/${sha9}`;
|
||||
const bodyRegex = /^Missing or empty 'AUTHORIZATION' header/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${headers1} ${url}`).then(h.verifyResponse(401, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${headers2} ${url}`).then(h.verifyResponse(401, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject requests without an \'X-FILE\' header', done => {
|
||||
const headers1 = authorizationHeader;
|
||||
const headers2 = `${authorizationHeader} --header "X-FILE: "`;
|
||||
const url = `http://${host}/create-build/${pr}/${sha9}`;
|
||||
const bodyRegex = /^Missing or empty 'X-FILE' header/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${headers1} ${url}`).then(h.verifyResponse(400, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${headers2} ${url}`).then(h.verifyResponse(400, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths', done => {
|
||||
const cmdPrefix = `${curl} http://${host}`;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`${cmdPrefix}/foo/create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/foo-create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/fooncreate-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build/foo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build-foo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-buildnfoo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build/pr${pr}/${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/create-build/${pr}/${sha9}42`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject PRs with leading zeros', done => {
|
||||
h.runCmd(`${curl} http://${host}/create-build/0${pr}/${sha9}`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/0${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`).then(h.verifyResponse(500)),
|
||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha0}`).then(h.verifyResponse(500)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should not overwrite existing builds', done => {
|
||||
h.createDummyBuild(pr, sha9);
|
||||
expect(h.readBuildFile(pr, sha9, 'index.html')).toContain('index.html');
|
||||
|
||||
h.writeBuildFile(pr, sha9, 'index.html', 'My content');
|
||||
expect(h.readBuildFile(pr, sha9, 'index.html')).toBe('My content');
|
||||
|
||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`).
|
||||
then(h.verifyResponse(403, /^Request to overwrite existing directory/)).
|
||||
then(() => expect(h.readBuildFile(pr, sha9, 'index.html')).toBe('My content')).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should delete the PR directory on error (for new PR)', done => {
|
||||
const prDir = path.join(h.buildsDir, pr);
|
||||
|
||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`).
|
||||
then(h.verifyResponse(500)).
|
||||
then(() => expect(fs.existsSync(prDir)).toBe(false)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should only delete the SHA directory on error (for existing PR)', done => {
|
||||
const prDir = path.join(h.buildsDir, pr);
|
||||
const shaDir = path.join(prDir, sha9);
|
||||
|
||||
h.createDummyBuild(pr, sha0);
|
||||
|
||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`).
|
||||
then(h.verifyResponse(500)).
|
||||
then(() => {
|
||||
expect(fs.existsSync(shaDir)).toBe(false);
|
||||
expect(fs.existsSync(prDir)).toBe(true);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
describe('on successful upload', () => {
|
||||
const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz');
|
||||
let uploadPromise: Promise<CmdResult>;
|
||||
|
||||
beforeEach(() => {
|
||||
h.createDummyArchive(pr, sha9, archivePath);
|
||||
uploadPromise = h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`);
|
||||
});
|
||||
afterEach(() => h.deletePrDir(pr));
|
||||
|
||||
|
||||
it('should respond with 201', done => {
|
||||
uploadPromise.then(h.verifyResponse(201)).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should extract the contents of the uploaded file', done => {
|
||||
uploadPromise.
|
||||
then(() => {
|
||||
expect(h.readBuildFile(pr, sha9, 'index.html')).toContain(`uploaded/${pr}`);
|
||||
expect(h.readBuildFile(pr, sha9, 'foo/bar.js')).toContain(`uploaded/${pr}`);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it(`should create files/directories owned by '${h.serverUser}'`, done => {
|
||||
const shaDir = path.join(h.buildsDir, pr, sha9);
|
||||
const idxPath = path.join(shaDir, 'index.html');
|
||||
const barPath = path.join(shaDir, 'foo', 'bar.js');
|
||||
|
||||
uploadPromise.
|
||||
then(() => Promise.all([
|
||||
h.runCmd(`find ${shaDir}`),
|
||||
h.runCmd(`find ${shaDir} -user ${h.serverUser}`),
|
||||
])).
|
||||
then(([{stdout: allFiles}, {stdout: userFiles}]) => {
|
||||
expect(userFiles).toBe(allFiles);
|
||||
expect(userFiles).toContain(shaDir);
|
||||
expect(userFiles).toContain(idxPath);
|
||||
expect(userFiles).toContain(barPath);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should delete the uploaded file', done => {
|
||||
expect(fs.existsSync(archivePath)).toBe(true);
|
||||
uploadPromise.
|
||||
then(() => expect(fs.existsSync(archivePath)).toBe(false)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should make the build directory non-writable', done => {
|
||||
const shaDir = path.join(h.buildsDir, pr, sha9);
|
||||
const idxPath = path.join(shaDir, 'index.html');
|
||||
const barPath = path.join(shaDir, 'foo', 'bar.js');
|
||||
|
||||
// See https://github.com/nodejs/node-v0.x-archive/issues/3045#issuecomment-4862588.
|
||||
const isNotWritable = (fileOrDir: string) => {
|
||||
const mode = fs.statSync(fileOrDir).mode;
|
||||
// tslint:disable-next-line: no-bitwise
|
||||
return !(mode & parseInt('222', 8));
|
||||
};
|
||||
|
||||
uploadPromise.
|
||||
then(() => {
|
||||
expect(isNotWritable(shaDir)).toBe(true);
|
||||
expect(isNotWritable(idxPath)).toBe(true);
|
||||
expect(isNotWritable(barPath)).toBe(true);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/health-check`, () => {
|
||||
|
||||
it('should respond with 200', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL http://${host}/health-check`).then(h.verifyResponse(200)),
|
||||
h.runCmd(`curl -iL http://${host}/health-check/`).then(h.verifyResponse(200)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 if the path does not match exactly', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL http://${host}/health-check/foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL http://${host}/health-check-foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL http://${host}/health-checknfoo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL http://${host}/foo/health-check`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL http://${host}/foo-health-check`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL http://${host}/foonhealth-check`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/*`, () => {
|
||||
|
||||
it('should respond with 404 for GET requests to unknown URLs', done => {
|
||||
const bodyRegex = /^Unknown resource/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL http://${host}/index.html`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iL http://${host}/`).then(h.verifyResponse(404, bodyRegex)),
|
||||
h.runCmd(`curl -iL http://${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 405 for non-GET requests to any URL', done => {
|
||||
const bodyRegex = /^Unsupported method/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iLX PUT http://${host}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX POST http://${host}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX PATCH http://${host}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX DELETE http://${host}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
43
aio/aio-builds-setup/dockerbuild/scripts-js/package.json
Normal file
43
aio/aio-builds-setup/dockerbuild/scripts-js/package.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "aio-scripts-js",
|
||||
"version": "1.0.0",
|
||||
"description": "Performing various tasks on PR build artifacts for angular.io.",
|
||||
"repository": "https://github.com/angular/angular.git",
|
||||
"author": "Angular",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"prebuild": "yarn run clean",
|
||||
"build": "tsc",
|
||||
"build-watch": "yarn run tsc -- --watch",
|
||||
"clean": "node --eval \"require('shelljs').rm('-rf', 'dist')\"",
|
||||
"dev": "concurrently --kill-others --raw --success first \"yarn run build-watch\" \"yarn run test-watch\"",
|
||||
"lint": "tslint --project tsconfig.json",
|
||||
"pre~~test-only": "yarn run lint",
|
||||
"~~test-only": "node dist/test",
|
||||
"pretest": "yarn run build",
|
||||
"test": "yarn run ~~test-only",
|
||||
"pretest-watch": "yarn run build",
|
||||
"test-watch": "nodemon --exec \"yarn run ~~test-only\" --watch dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.14.1",
|
||||
"jasmine": "^2.5.3",
|
||||
"jsonwebtoken": "^7.3.0",
|
||||
"shelljs": "^0.7.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.0.35",
|
||||
"@types/jasmine": "^2.5.43",
|
||||
"@types/jsonwebtoken": "^7.2.0",
|
||||
"@types/node": "^7.0.5",
|
||||
"@types/shelljs": "^0.7.0",
|
||||
"@types/supertest": "^2.0.0",
|
||||
"concurrently": "^3.3.0",
|
||||
"eslint": "^3.15.0",
|
||||
"eslint-plugin-jasmine": "^2.2.0",
|
||||
"nodemon": "^1.11.0",
|
||||
"supertest": "^3.0.0",
|
||||
"tslint": "^4.4.2",
|
||||
"typescript": "^2.1.6"
|
||||
}
|
||||
}
|
@ -0,0 +1,318 @@
|
||||
// Imports
|
||||
import * as fs from 'fs';
|
||||
import * as shell from 'shelljs';
|
||||
import {BuildCleaner} from '../../lib/clean-up/build-cleaner';
|
||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||
|
||||
// Tests
|
||||
describe('BuildCleaner', () => {
|
||||
let cleaner: BuildCleaner;
|
||||
|
||||
beforeEach(() => cleaner = new BuildCleaner('/foo/bar', 'baz/qux', '12345'));
|
||||
|
||||
|
||||
describe('constructor()', () => {
|
||||
|
||||
it('should throw if \'buildsDir\' is empty', () => {
|
||||
expect(() => new BuildCleaner('', '/baz/qux', '12345')).
|
||||
toThrowError('Missing or empty required parameter \'buildsDir\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'repoSlug\' is empty', () => {
|
||||
expect(() => new BuildCleaner('/foo/bar', '', '12345')).
|
||||
toThrowError('Missing or empty required parameter \'repoSlug\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'githubToken\' is empty', () => {
|
||||
expect(() => new BuildCleaner('/foo/bar', 'baz/qux', '')).
|
||||
toThrowError('Missing or empty required parameter \'githubToken\'!');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('cleanUp()', () => {
|
||||
let cleanerGetExistingBuildNumbersSpy: jasmine.Spy;
|
||||
let cleanerGetOpenPrNumbersSpy: jasmine.Spy;
|
||||
let cleanerRemoveUnnecessaryBuildsSpy: jasmine.Spy;
|
||||
let existingBuildsDeferred: {resolve: Function, reject: Function};
|
||||
let openPrsDeferred: {resolve: Function, reject: Function};
|
||||
let promise: Promise<void>;
|
||||
|
||||
beforeEach(() => {
|
||||
cleanerGetExistingBuildNumbersSpy = spyOn(cleaner as any, 'getExistingBuildNumbers').and.callFake(() => {
|
||||
return new Promise((resolve, reject) => existingBuildsDeferred = {resolve, reject});
|
||||
});
|
||||
cleanerGetOpenPrNumbersSpy = spyOn(cleaner as any, 'getOpenPrNumbers').and.callFake(() => {
|
||||
return new Promise((resolve, reject) => openPrsDeferred = {resolve, reject});
|
||||
});
|
||||
cleanerRemoveUnnecessaryBuildsSpy = spyOn(cleaner as any, 'removeUnnecessaryBuilds');
|
||||
|
||||
promise = cleaner.cleanUp();
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should get the existing builds', () => {
|
||||
expect(cleanerGetExistingBuildNumbersSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should get the open PRs', () => {
|
||||
expect(cleanerGetOpenPrNumbersSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should reject if \'getExistingBuildNumbers()\' rejects', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
existingBuildsDeferred.reject('Test');
|
||||
});
|
||||
|
||||
|
||||
it('should reject if \'getOpenPrNumbers()\' rejects', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
openPrsDeferred.reject('Test');
|
||||
});
|
||||
|
||||
|
||||
it('should reject if \'removeUnnecessaryBuilds()\' rejects', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
cleanerRemoveUnnecessaryBuildsSpy.and.returnValue(Promise.reject('Test'));
|
||||
existingBuildsDeferred.resolve();
|
||||
openPrsDeferred.resolve();
|
||||
});
|
||||
|
||||
|
||||
it('should pass existing builds and open PRs to \'removeUnnecessaryBuilds()\'', done => {
|
||||
promise.then(() => {
|
||||
expect(cleanerRemoveUnnecessaryBuildsSpy).toHaveBeenCalledWith('foo', 'bar');
|
||||
done();
|
||||
});
|
||||
|
||||
existingBuildsDeferred.resolve('foo');
|
||||
openPrsDeferred.resolve('bar');
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the value returned by \'removeUnnecessaryBuilds()\'', done => {
|
||||
promise.then(result => {
|
||||
expect(result).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
cleanerRemoveUnnecessaryBuildsSpy.and.returnValue(Promise.resolve('Test'));
|
||||
existingBuildsDeferred.resolve();
|
||||
openPrsDeferred.resolve();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
// Protected methods
|
||||
|
||||
describe('getExistingBuildNumbers()', () => {
|
||||
let fsReaddirSpy: jasmine.Spy;
|
||||
let readdirCb: (err: any, files?: string[]) => void;
|
||||
let promise: Promise<number[]>;
|
||||
|
||||
beforeEach(() => {
|
||||
fsReaddirSpy = spyOn(fs, 'readdir').and.callFake((_: string, cb: typeof readdirCb) => readdirCb = cb);
|
||||
promise = (cleaner as any).getExistingBuildNumbers();
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should get the contents of the builds directory', () => {
|
||||
expect(fsReaddirSpy).toHaveBeenCalled();
|
||||
expect(fsReaddirSpy.calls.argsFor(0)[0]).toBe('/foo/bar');
|
||||
});
|
||||
|
||||
|
||||
it('should reject if an error occurs while getting the files', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
readdirCb('Test');
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the returned files (as numbers)', done => {
|
||||
promise.then(result => {
|
||||
expect(result).toEqual([12, 34, 56]);
|
||||
done();
|
||||
});
|
||||
|
||||
readdirCb(null, ['12', '34', '56']);
|
||||
});
|
||||
|
||||
|
||||
it('should ignore files with non-numeric (or zero) names', done => {
|
||||
promise.then(result => {
|
||||
expect(result).toEqual([12, 34, 56]);
|
||||
done();
|
||||
});
|
||||
|
||||
readdirCb(null, ['12', 'foo', '34', 'bar', '56', '000']);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('getOpenPrNumbers()', () => {
|
||||
let prDeferred: {resolve: Function, reject: Function};
|
||||
let promise: Promise<number[]>;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(GithubPullRequests.prototype, 'fetchAll').and.callFake(() => {
|
||||
return new Promise((resolve, reject) => prDeferred = {resolve, reject});
|
||||
});
|
||||
|
||||
promise = (cleaner as any).getOpenPrNumbers();
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should fetch open PRs via \'GithubPullRequests\'', () => {
|
||||
expect(GithubPullRequests.prototype.fetchAll).toHaveBeenCalledWith('open');
|
||||
});
|
||||
|
||||
|
||||
it('should reject if an error occurs while fetching PRs', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
prDeferred.reject('Test');
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the numbers of the fetched PRs', done => {
|
||||
promise.then(prNumbers => {
|
||||
expect(prNumbers).toEqual([1, 2, 3]);
|
||||
done();
|
||||
});
|
||||
|
||||
prDeferred.resolve([{id: 0, number: 1}, {id: 1, number: 2}, {id: 2, number: 3}]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('removeDir()', () => {
|
||||
let shellChmodSpy: jasmine.Spy;
|
||||
let shellRmSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
shellChmodSpy = spyOn(shell, 'chmod');
|
||||
shellRmSpy = spyOn(shell, 'rm');
|
||||
});
|
||||
|
||||
|
||||
it('should remove the specified directory and its content', () => {
|
||||
(cleaner as any).removeDir('/foo/bar');
|
||||
expect(shellRmSpy).toHaveBeenCalledWith('-rf', '/foo/bar');
|
||||
});
|
||||
|
||||
|
||||
it('should make the directory and its content writable before removing', () => {
|
||||
shellRmSpy.and.callFake(() => expect(shellChmodSpy).toHaveBeenCalledWith('-R', 'a+w', '/foo/bar'));
|
||||
(cleaner as any).removeDir('/foo/bar');
|
||||
|
||||
expect(shellRmSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should catch errors and log them', () => {
|
||||
const consoleErrorSpy = spyOn(console, 'error');
|
||||
shellRmSpy.and.callFake(() => { throw 'Test'; });
|
||||
|
||||
(cleaner as any).removeDir('/foo/bar');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||
expect(consoleErrorSpy.calls.argsFor(0)[0]).toContain('Unable to remove \'/foo/bar\'');
|
||||
expect(consoleErrorSpy.calls.argsFor(0)[1]).toBe('Test');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('removeUnnecessaryBuilds()', () => {
|
||||
let consoleLogSpy: jasmine.Spy;
|
||||
let cleanerRemoveDirSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleLogSpy = spyOn(console, 'log');
|
||||
cleanerRemoveDirSpy = spyOn(cleaner as any, 'removeDir');
|
||||
});
|
||||
|
||||
|
||||
it('should log the number of existing builds, open PRs and builds to be removed', () => {
|
||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3], [3, 4, 5, 6]);
|
||||
|
||||
expect(console.log).toHaveBeenCalledWith('Existing builds: 3');
|
||||
expect(console.log).toHaveBeenCalledWith('Open pull requests: 4');
|
||||
expect(console.log).toHaveBeenCalledWith('Removing 2 build(s): 1, 2');
|
||||
});
|
||||
|
||||
|
||||
it('should construct full paths to directories (by prepending \'buildsDir\')', () => {
|
||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3], []);
|
||||
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/1');
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/2');
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/3');
|
||||
});
|
||||
|
||||
|
||||
it('should remove the builds that do not correspond to open PRs', () => {
|
||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], [2, 4]);
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(2);
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/1');
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/3');
|
||||
cleanerRemoveDirSpy.calls.reset();
|
||||
|
||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], [1, 2, 3, 4]);
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(0);
|
||||
cleanerRemoveDirSpy.calls.reset();
|
||||
|
||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], []);
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(4);
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/1');
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/2');
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/3');
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/4');
|
||||
cleanerRemoveDirSpy.calls.reset();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -0,0 +1,410 @@
|
||||
// Imports
|
||||
import {EventEmitter} from 'events';
|
||||
import {ClientRequest, IncomingMessage} from 'http';
|
||||
import * as https from 'https';
|
||||
import {GithubApi} from '../../lib/common/github-api';
|
||||
|
||||
// Tests
|
||||
describe('GithubApi', () => {
|
||||
let api: GithubApi;
|
||||
|
||||
beforeEach(() => api = new GithubApi('12345'));
|
||||
|
||||
|
||||
describe('constructor()', () => {
|
||||
|
||||
it('should throw if \'githubToken\' is missing or empty', () => {
|
||||
expect(() => new GithubApi('')).toThrowError('Missing or empty required parameter \'githubToken\'!');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('get()', () => {
|
||||
let apiBuildPathSpy: jasmine.Spy;
|
||||
let apiRequestSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
apiBuildPathSpy = spyOn(api as any, 'buildPath');
|
||||
apiRequestSpy = spyOn(api as any, 'request');
|
||||
});
|
||||
|
||||
|
||||
it('should call \'buildPath()\' with the pathname and params', () => {
|
||||
api.get('/foo', {bar: 'baz'});
|
||||
|
||||
expect(apiBuildPathSpy).toHaveBeenCalled();
|
||||
expect(apiBuildPathSpy.calls.argsFor(0)).toEqual(['/foo', {bar: 'baz'}]);
|
||||
});
|
||||
|
||||
|
||||
it('should call \'request()\' with the correct method', () => {
|
||||
api.get('/foo');
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalled();
|
||||
expect(apiRequestSpy.calls.argsFor(0)[0]).toBe('get');
|
||||
});
|
||||
|
||||
|
||||
it('should call \'request()\' with the correct path', () => {
|
||||
apiBuildPathSpy.and.returnValue('/foo/bar');
|
||||
api.get('foo');
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalled();
|
||||
expect(apiRequestSpy.calls.argsFor(0)[1]).toBe('/foo/bar');
|
||||
});
|
||||
|
||||
|
||||
it('should not pass data to \'request()\'', () => {
|
||||
(api.get as Function)('foo', {}, {});
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalled();
|
||||
expect(apiRequestSpy.calls.argsFor(0)[2]).toBeUndefined();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('post()', () => {
|
||||
let apiBuildPathSpy: jasmine.Spy;
|
||||
let apiRequestSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
apiBuildPathSpy = spyOn(api as any, 'buildPath');
|
||||
apiRequestSpy = spyOn(api as any, 'request');
|
||||
});
|
||||
|
||||
|
||||
it('should call \'buildPath()\' with the pathname and params', () => {
|
||||
api.post('/foo', {bar: 'baz'});
|
||||
|
||||
expect(apiBuildPathSpy).toHaveBeenCalled();
|
||||
expect(apiBuildPathSpy.calls.argsFor(0)).toEqual(['/foo', {bar: 'baz'}]);
|
||||
});
|
||||
|
||||
|
||||
it('should call \'request()\' with the correct method', () => {
|
||||
api.post('/foo');
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalled();
|
||||
expect(apiRequestSpy.calls.argsFor(0)[0]).toBe('post');
|
||||
});
|
||||
|
||||
|
||||
it('should call \'request()\' with the correct path', () => {
|
||||
apiBuildPathSpy.and.returnValue('/foo/bar');
|
||||
api.post('/foo');
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalled();
|
||||
expect(apiRequestSpy.calls.argsFor(0)[1]).toBe('/foo/bar');
|
||||
});
|
||||
|
||||
|
||||
it('should pass the data to \'request()\'', () => {
|
||||
api.post('/foo', {}, {bar: 'baz'});
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalled();
|
||||
expect(apiRequestSpy.calls.argsFor(0)[2]).toEqual({bar: 'baz'});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
// Protected methods
|
||||
|
||||
describe('buildPath()', () => {
|
||||
|
||||
it('should return the pathname if no params', () => {
|
||||
expect((api as any).buildPath('/foo')).toBe('/foo');
|
||||
expect((api as any).buildPath('/foo', undefined)).toBe('/foo');
|
||||
expect((api as any).buildPath('/foo', null)).toBe('/foo');
|
||||
});
|
||||
|
||||
|
||||
it('should append the params to the pathname', () => {
|
||||
expect((api as any).buildPath('/foo', {bar: 'baz'})).toBe('/foo?bar=baz');
|
||||
});
|
||||
|
||||
|
||||
it('should join the params with \'&\'', () => {
|
||||
expect((api as any).buildPath('/foo', {bar: 1, baz: 2})).toBe('/foo?bar=1&baz=2');
|
||||
});
|
||||
|
||||
|
||||
it('should ignore undefined/null params', () => {
|
||||
expect((api as any).buildPath('/foo', {bar: undefined, baz: null})).toBe('/foo');
|
||||
});
|
||||
|
||||
|
||||
it('should encode param values as URI components', () => {
|
||||
expect((api as any).buildPath('/foo', {bar: 'b a&z'})).toBe('/foo?bar=b%20a%26z');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('getPaginated()', () => {
|
||||
let deferreds: {resolve: Function, reject: Function}[];
|
||||
|
||||
beforeEach(() => {
|
||||
deferreds = [];
|
||||
spyOn(api, 'get').and.callFake(() => new Promise((resolve, reject) => deferreds.push({resolve, reject})));
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect((api as any).getPaginated()).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should call \'get()\' with the correct pathname and params', () => {
|
||||
(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});
|
||||
});
|
||||
|
||||
|
||||
it('should reject if the request fails', done => {
|
||||
(api as any).getPaginated('/foo/bar').catch((err: any) => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
deferreds[0].reject('Test');
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the returned items', done => {
|
||||
const items = [{id: 1}, {id: 2}];
|
||||
|
||||
(api as any).getPaginated('/foo/bar').then((data: any) => {
|
||||
expect(data).toEqual(items);
|
||||
done();
|
||||
});
|
||||
|
||||
deferreds[0].resolve(items);
|
||||
});
|
||||
|
||||
|
||||
it('should iteratively call \'get()\' to fetch all items', done => {
|
||||
// Create an array or 250 objects.
|
||||
const allItems = '.'.repeat(250).split('').map((_, i) => ({id: i}));
|
||||
const apiGetSpy = api.get as jasmine.Spy;
|
||||
|
||||
(api as any).getPaginated('/foo/bar', {baz: 'qux'}).then((data: any) => {
|
||||
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(data).toEqual(allItems);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
deferreds[0].resolve(allItems.slice(0, 100));
|
||||
setTimeout(() => {
|
||||
deferreds[1].resolve(allItems.slice(100, 200));
|
||||
setTimeout(() => {
|
||||
deferreds[2].resolve(allItems.slice(200));
|
||||
}, 0);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('request()', () => {
|
||||
let httpsRequestSpy: jasmine.Spy;
|
||||
let latestRequest: ClientRequest;
|
||||
|
||||
beforeEach(() => {
|
||||
const originalRequest = https.request;
|
||||
|
||||
httpsRequestSpy = spyOn(https, 'request').and.callFake((...args: any[]) => {
|
||||
latestRequest = originalRequest.apply(https, args);
|
||||
|
||||
spyOn(latestRequest, 'on').and.callThrough();
|
||||
spyOn(latestRequest, 'end');
|
||||
|
||||
return latestRequest;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect((api as any).request()).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should call \'https.request()\' with the correct options', () => {
|
||||
(api as any).request('method', 'path');
|
||||
|
||||
expect(httpsRequestSpy).toHaveBeenCalled();
|
||||
expect(httpsRequestSpy.calls.argsFor(0)[0]).toEqual(jasmine.objectContaining({
|
||||
headers: jasmine.objectContaining({
|
||||
'User-Agent': `Node/${process.versions.node}`,
|
||||
}),
|
||||
host: 'api.github.com',
|
||||
method: 'method',
|
||||
path: 'path',
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
it('should call specify an \'Authorization\' header if \'githubToken\' is present', () => {
|
||||
(api as any).request('method', 'path');
|
||||
|
||||
expect(httpsRequestSpy).toHaveBeenCalled();
|
||||
expect(httpsRequestSpy.calls.argsFor(0)[0].headers).toEqual(jasmine.objectContaining({
|
||||
Authorization: 'token 12345',
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
it('should reject on request error', done => {
|
||||
(api as any).request('method', 'path').catch((err: any) => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
latestRequest.emit('error', 'Test');
|
||||
});
|
||||
|
||||
|
||||
it('should send the request (i.e. call \'end()\')', () => {
|
||||
(api as any).request('method', 'path');
|
||||
expect(latestRequest.end).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should \'JSON.stringify\' and send the data along with the request', () => {
|
||||
(api as any).request('method', 'path');
|
||||
expect(latestRequest.end).toHaveBeenCalledWith(null);
|
||||
|
||||
(api as any).request('method', 'path', {key: 'value'});
|
||||
expect(latestRequest.end).toHaveBeenCalledWith('{"key":"value"}');
|
||||
});
|
||||
|
||||
|
||||
describe('onResponse', () => {
|
||||
let promise: Promise<void>;
|
||||
let respond: (statusCode: number) => IncomingMessage;
|
||||
|
||||
beforeEach(() => {
|
||||
promise = (api as any).request('method', 'path');
|
||||
|
||||
respond = (statusCode: number) => {
|
||||
const mockResponse = new EventEmitter() as IncomingMessage;
|
||||
mockResponse.statusCode = statusCode;
|
||||
|
||||
const onResponse = httpsRequestSpy.calls.argsFor(0)[1];
|
||||
onResponse(mockResponse);
|
||||
|
||||
return mockResponse;
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
it('should reject on response error', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
const res = respond(200);
|
||||
res.emit('error', 'Test');
|
||||
});
|
||||
|
||||
|
||||
it('should reject if returned statusCode is <200', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toContain('failed');
|
||||
expect(err).toContain('status: 199');
|
||||
done();
|
||||
});
|
||||
|
||||
const res = respond(199);
|
||||
res.emit('end');
|
||||
});
|
||||
|
||||
|
||||
it('should reject if returned statusCode is >=400', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toContain('failed');
|
||||
expect(err).toContain('status: 400');
|
||||
done();
|
||||
});
|
||||
|
||||
const res = respond(400);
|
||||
res.emit('end');
|
||||
});
|
||||
|
||||
|
||||
it('should include the response text in the rejection message', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toContain('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
const res = respond(500);
|
||||
res.emit('data', 'Test');
|
||||
res.emit('end');
|
||||
});
|
||||
|
||||
|
||||
it('should resolve if returned statusCode is <=200 <400', done => {
|
||||
promise.then(done);
|
||||
|
||||
const res = respond(200);
|
||||
res.emit('data', '{}');
|
||||
res.emit('end');
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the response text \'JSON.parsed\'', done => {
|
||||
promise.then(data => {
|
||||
expect(data).toEqual({foo: 'bar'});
|
||||
done();
|
||||
});
|
||||
|
||||
const res = respond(300);
|
||||
res.emit('data', '{"foo":"bar"}');
|
||||
res.emit('end');
|
||||
});
|
||||
|
||||
|
||||
it('should collect and concatenate the whole response text', done => {
|
||||
promise.then(data => {
|
||||
expect(data).toEqual({foo: 'bar', baz: 'qux'});
|
||||
done();
|
||||
});
|
||||
|
||||
const res = respond(300);
|
||||
res.emit('data', '{"foo":');
|
||||
res.emit('data', '"bar","baz"');
|
||||
res.emit('data', ':"qux"}');
|
||||
res.emit('end');
|
||||
});
|
||||
|
||||
|
||||
it('should reject if the response text is malformed JSON', done => {
|
||||
promise.catch(err => {
|
||||
expect(err).toEqual(jasmine.any(SyntaxError));
|
||||
done();
|
||||
});
|
||||
|
||||
const res = respond(300);
|
||||
res.emit('data', '}');
|
||||
res.emit('end');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -0,0 +1,117 @@
|
||||
// Imports
|
||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||
|
||||
// Tests
|
||||
describe('GithubPullRequests', () => {
|
||||
|
||||
describe('constructor()', () => {
|
||||
|
||||
it('should throw if \'githubToken\' is missing or empty', () => {
|
||||
expect(() => new GithubPullRequests('', 'foo/bar')).
|
||||
toThrowError('Missing or empty required parameter \'githubToken\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'repoSlug\' is missing or empty', () => {
|
||||
expect(() => new GithubPullRequests('12345', '')).
|
||||
toThrowError('Missing or empty required parameter \'repoSlug\'!');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('addComment()', () => {
|
||||
let prs: GithubPullRequests;
|
||||
let deferred: {resolve: Function, reject: Function};
|
||||
|
||||
beforeEach(() => {
|
||||
prs = new GithubPullRequests('12345', 'foo/bar');
|
||||
|
||||
spyOn(prs, 'post').and.callFake(() => new Promise((resolve, reject) => deferred = {resolve, reject}));
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect(prs.addComment(42, 'body')).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should throw if the PR number is invalid', () => {
|
||||
expect(() => prs.addComment(-1337, 'body')).toThrowError(`Invalid PR number: -1337`);
|
||||
expect(() => prs.addComment(NaN, 'body')).toThrowError(`Invalid PR number: NaN`);
|
||||
});
|
||||
|
||||
|
||||
it('should throw if the comment body is invalid or empty', () => {
|
||||
expect(() => prs.addComment(42, '')).toThrowError(`Invalid or empty comment body: `);
|
||||
});
|
||||
|
||||
|
||||
it('should call \'post()\' with the correct pathname, params and data', () => {
|
||||
prs.addComment(42, 'body');
|
||||
|
||||
expect(prs.post).toHaveBeenCalledWith('/repos/foo/bar/issues/42/comments', null, {body: 'body'});
|
||||
});
|
||||
|
||||
|
||||
it('should reject if the request fails', done => {
|
||||
prs.addComment(42, 'body').catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
deferred.reject('Test');
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the returned response', done => {
|
||||
prs.addComment(42, 'body').then(data => {
|
||||
expect(data).toEqual('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
deferred.resolve('Test');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('fetchAll()', () => {
|
||||
let prs: GithubPullRequests;
|
||||
let prsGetPaginatedSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
prs = new GithubPullRequests('12345', 'foo/bar');
|
||||
prsGetPaginatedSpy = spyOn(prs as any, 'getPaginated');
|
||||
spyOn(console, 'log');
|
||||
});
|
||||
|
||||
|
||||
it('should call \'getPaginated()\' with the correct pathname and params', () => {
|
||||
const expectedPathname = '/repos/foo/bar/pulls';
|
||||
|
||||
prs.fetchAll('all');
|
||||
prs.fetchAll('closed');
|
||||
prs.fetchAll('open');
|
||||
|
||||
expect(prsGetPaginatedSpy).toHaveBeenCalledTimes(3);
|
||||
expect(prsGetPaginatedSpy.calls.argsFor(0)).toEqual([expectedPathname, {state: 'all'}]);
|
||||
expect(prsGetPaginatedSpy.calls.argsFor(1)).toEqual([expectedPathname, {state: 'closed'}]);
|
||||
expect(prsGetPaginatedSpy.calls.argsFor(2)).toEqual([expectedPathname, {state: 'open'}]);
|
||||
});
|
||||
|
||||
|
||||
it('should default to \'all\' if no state is specified', () => {
|
||||
prs.fetchAll();
|
||||
expect(prsGetPaginatedSpy).toHaveBeenCalledWith('/repos/foo/bar/pulls', {state: 'all'});
|
||||
});
|
||||
|
||||
|
||||
it('should forward the value returned by \'getPaginated()\'', () => {
|
||||
prsGetPaginatedSpy.and.returnValue('Test');
|
||||
expect(prs.fetchAll()).toBe('Test');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -0,0 +1,232 @@
|
||||
// Imports
|
||||
import {GithubTeams} from '../../lib/common/github-teams';
|
||||
|
||||
// Tests
|
||||
describe('GithubTeams', () => {
|
||||
|
||||
describe('constructor()', () => {
|
||||
|
||||
it('should throw if \'githubToken\' is missing or empty', () => {
|
||||
expect(() => new GithubTeams('', 'org')).
|
||||
toThrowError('Missing or empty required parameter \'githubToken\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'organization\' is missing or empty', () => {
|
||||
expect(() => new GithubTeams('12345', '')).
|
||||
toThrowError('Missing or empty required parameter \'organization\'!');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('fetchAll()', () => {
|
||||
let teams: GithubTeams;
|
||||
let teamsGetPaginatedSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
teams = new GithubTeams('12345', 'foo');
|
||||
teamsGetPaginatedSpy = spyOn(teams as any, 'getPaginated');
|
||||
});
|
||||
|
||||
|
||||
it('should call \'getPaginated()\' with the correct pathname and params', () => {
|
||||
teams.fetchAll();
|
||||
expect(teamsGetPaginatedSpy).toHaveBeenCalledWith('/orgs/foo/teams');
|
||||
});
|
||||
|
||||
|
||||
it('should forward the value returned by \'getPaginated()\'', () => {
|
||||
teamsGetPaginatedSpy.and.returnValue('Test');
|
||||
expect(teams.fetchAll()).toBe('Test');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('isMemberById()', () => {
|
||||
let teams: GithubTeams;
|
||||
let teamsGetSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
teams = new GithubTeams('12345', 'foo');
|
||||
teamsGetSpy = spyOn(teams, 'get');
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect(teams.isMemberById('user', [1])).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with false if called with an empty array', done => {
|
||||
teams.isMemberById('user', []).then(isMember => {
|
||||
expect(isMember).toBe(false);
|
||||
expect(teamsGetSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should call \'get()\' with the correct pathname', done => {
|
||||
teamsGetSpy.and.returnValue(Promise.resolve(null));
|
||||
teams.isMemberById('user', [1]).then(() => {
|
||||
expect(teamsGetSpy).toHaveBeenCalledWith('/teams/1/memberships/user');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with false if \'get()\' rejects', done => {
|
||||
teamsGetSpy.and.returnValue(Promise.reject(null));
|
||||
teams.isMemberById('user', [1]).then(isMember => {
|
||||
expect(isMember).toBe(false);
|
||||
expect(teamsGetSpy).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with false if the membership is not active', done => {
|
||||
teamsGetSpy.and.returnValue(Promise.resolve({state: 'pending'}));
|
||||
teams.isMemberById('user', [1]).then(isMember => {
|
||||
expect(isMember).toBe(false);
|
||||
expect(teamsGetSpy).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with true if the membership is active', done => {
|
||||
teamsGetSpy.and.returnValue(Promise.resolve({state: 'active'}));
|
||||
teams.isMemberById('user', [1]).then(isMember => {
|
||||
expect(isMember).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should sequentially call \'get()\' until an active membership is found', done => {
|
||||
const trainedResponses: {[pathname: string]: Promise<{state: string}>} = {
|
||||
'/teams/1/memberships/user': Promise.resolve({state: 'pending'}),
|
||||
'/teams/2/memberships/user': Promise.reject(null),
|
||||
'/teams/3/memberships/user': Promise.resolve({state: 'active'}),
|
||||
};
|
||||
teamsGetSpy.and.callFake((pathname: string) => trainedResponses[pathname]);
|
||||
|
||||
teams.isMemberById('user', [1, 2, 3, 4]).then(isMember => {
|
||||
expect(isMember).toBe(true);
|
||||
|
||||
expect(teamsGetSpy).toHaveBeenCalledTimes(3);
|
||||
expect(teamsGetSpy.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user');
|
||||
expect(teamsGetSpy.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user');
|
||||
expect(teamsGetSpy.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with false if no active membership is found', done => {
|
||||
const trainedResponses: {[pathname: string]: Promise<{state: string}>} = {
|
||||
'/teams/1/memberships/user': Promise.resolve({state: 'pending'}),
|
||||
'/teams/2/memberships/user': Promise.reject(null),
|
||||
'/teams/3/memberships/user': Promise.resolve({state: 'not active'}),
|
||||
'/teams/4/memberships/user': Promise.reject(null),
|
||||
};
|
||||
teamsGetSpy.and.callFake((pathname: string) => trainedResponses[pathname]);
|
||||
|
||||
teams.isMemberById('user', [1, 2, 3, 4]).then(isMember => {
|
||||
expect(isMember).toBe(false);
|
||||
|
||||
expect(teamsGetSpy).toHaveBeenCalledTimes(4);
|
||||
expect(teamsGetSpy.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user');
|
||||
expect(teamsGetSpy.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user');
|
||||
expect(teamsGetSpy.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user');
|
||||
expect(teamsGetSpy.calls.argsFor(3)[0]).toBe('/teams/4/memberships/user');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('isMemberBySlug()', () => {
|
||||
let teams: GithubTeams;
|
||||
let teamsFetchAllSpy: jasmine.Spy;
|
||||
let teamsIsMemberByIdSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
teams = new GithubTeams('12345', 'foo');
|
||||
|
||||
const mockResponse = Promise.resolve([{id: 1, slug: 'team1'}, {id: 2, slug: 'team2'}]);
|
||||
teamsFetchAllSpy = spyOn(teams, 'fetchAll').and.returnValue(mockResponse);
|
||||
teamsIsMemberByIdSpy = spyOn(teams, 'isMemberById');
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect(teams.isMemberBySlug('user', ['team-slug'])).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should call \'fetchAll()\'', () => {
|
||||
teams.isMemberBySlug('user', ['team-slug']);
|
||||
expect(teamsFetchAllSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with false if \'fetchAll()\' rejects', done => {
|
||||
teamsFetchAllSpy.and.returnValue(Promise.reject(null));
|
||||
teams.isMemberBySlug('user', ['team-slug']).then(isMember => {
|
||||
expect(isMember).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should call \'isMemberById()\' with the correct params if no team is found', done => {
|
||||
teams.isMemberBySlug('user', ['no-match']).then(() => {
|
||||
expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('user', []);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should call \'isMemberById()\' with the correct params if teams are found', done => {
|
||||
const spy = teamsIsMemberByIdSpy;
|
||||
|
||||
Promise.all([
|
||||
teams.isMemberBySlug('user', ['team1']).then(() => expect(spy).toHaveBeenCalledWith('user', [1])),
|
||||
teams.isMemberBySlug('user', ['team2']).then(() => expect(spy).toHaveBeenCalledWith('user', [2])),
|
||||
teams.isMemberBySlug('user', ['team1', 'team2']).then(() => expect(spy).toHaveBeenCalledWith('user', [1, 2])),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with false if \'isMemberById()\' rejects', done => {
|
||||
teamsIsMemberByIdSpy.and.returnValue(Promise.reject(null));
|
||||
teams.isMemberBySlug('user', ['team1']).then(isMember => {
|
||||
expect(isMember).toBe(false);
|
||||
expect(teamsIsMemberByIdSpy).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the value \'isMemberById()\' resolves with', done => {
|
||||
teamsIsMemberByIdSpy.and.returnValues(Promise.resolve(false), Promise.resolve(true));
|
||||
|
||||
Promise.all([
|
||||
teams.isMemberBySlug('user', ['team1']).then(isMember => expect(isMember).toBe(false)),
|
||||
teams.isMemberBySlug('user', ['team1']).then(isMember => expect(isMember).toBe(true)),
|
||||
]).then(() => {
|
||||
expect(teamsIsMemberByIdSpy).toHaveBeenCalledTimes(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -0,0 +1,81 @@
|
||||
// Imports
|
||||
import {assertNotMissingOrEmpty, getEnvVar} from '../../lib/common/utils';
|
||||
|
||||
// Tests
|
||||
describe('utils', () => {
|
||||
|
||||
describe('assertNotMissingOrEmpty()', () => {
|
||||
|
||||
it('should throw if passed an empty value', () => {
|
||||
expect(() => assertNotMissingOrEmpty('foo', undefined)).
|
||||
toThrowError('Missing or empty required parameter \'foo\'!');
|
||||
expect(() => assertNotMissingOrEmpty('bar', null)).toThrowError('Missing or empty required parameter \'bar\'!');
|
||||
expect(() => assertNotMissingOrEmpty('baz', '')).toThrowError('Missing or empty required parameter \'baz\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should not throw if passed a non-empty value', () => {
|
||||
expect(() => assertNotMissingOrEmpty('foo', ' ')).not.toThrow();
|
||||
expect(() => assertNotMissingOrEmpty('bar', 'bar')).not.toThrow();
|
||||
expect(() => assertNotMissingOrEmpty('baz', 'b a z')).not.toThrow();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('getEnvVar()', () => {
|
||||
const emptyVar = '$$test_utils_getEnvVar_empty$$';
|
||||
const nonEmptyVar = '$$test_utils_getEnvVar_nonEmpty$$';
|
||||
const undefinedVar = '$$test_utils_getEnvVar_undefined$$';
|
||||
|
||||
beforeEach(() => {
|
||||
process.env[emptyVar] = '';
|
||||
process.env[nonEmptyVar] = 'foo';
|
||||
});
|
||||
afterEach(() => {
|
||||
delete process.env[emptyVar];
|
||||
delete process.env[nonEmptyVar];
|
||||
});
|
||||
|
||||
|
||||
it('should return an environment variable', () => {
|
||||
expect(getEnvVar(nonEmptyVar)).toBe('foo');
|
||||
});
|
||||
|
||||
|
||||
it('should exit with an error if the environment variable is not defined', () => {
|
||||
const consoleErrorSpy = spyOn(console, 'error');
|
||||
const processExitSpy = spyOn(process, 'exit');
|
||||
|
||||
getEnvVar(undefinedVar);
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||
expect(consoleErrorSpy.calls.argsFor(0)[0]).toContain(undefinedVar);
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
|
||||
it('should exit with an error if the environment variable is empty', () => {
|
||||
const consoleErrorSpy = spyOn(console, 'error');
|
||||
const processExitSpy = spyOn(process, 'exit');
|
||||
|
||||
getEnvVar(emptyVar);
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||
expect(consoleErrorSpy.calls.argsFor(0)[0]).toContain(emptyVar);
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
|
||||
it('should return an empty string if an undefined variable is optional', () => {
|
||||
expect(getEnvVar(undefinedVar, true)).toBe('');
|
||||
});
|
||||
|
||||
|
||||
it('should return an empty string if an empty variable is optional', () => {
|
||||
expect(getEnvVar(emptyVar, true)).toBe('');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -0,0 +1,6 @@
|
||||
declare namespace jasmine {
|
||||
export interface DoneFn extends Function {
|
||||
(): void;
|
||||
fail: (message: Error | string) => void;
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
// Imports
|
||||
import {runTests} from '../lib/common/run-tests';
|
||||
|
||||
// Run
|
||||
const specFiles = [`${__dirname}/**/*.spec.js`];
|
||||
const helpers = [`${__dirname}/helpers.js`];
|
||||
runTests(specFiles, helpers);
|
@ -0,0 +1,320 @@
|
||||
// Imports
|
||||
import * as cp from 'child_process';
|
||||
import {EventEmitter} from 'events';
|
||||
import * as fs from 'fs';
|
||||
import * as shell from 'shelljs';
|
||||
import {BuildCreator} from '../../lib/upload-server/build-creator';
|
||||
import {CreatedBuildEvent} from '../../lib/upload-server/build-events';
|
||||
import {UploadError} from '../../lib/upload-server/upload-error';
|
||||
import {expectToBeUploadError} from './helpers';
|
||||
|
||||
// Tests
|
||||
describe('BuildCreator', () => {
|
||||
const pr = '9';
|
||||
const sha = '9'.repeat(40);
|
||||
const archive = 'snapshot.tar.gz';
|
||||
const buildsDir = 'builds/dir';
|
||||
const prDir = `${buildsDir}/${pr}`;
|
||||
const shaDir = `${prDir}/${sha}`;
|
||||
let bc: BuildCreator;
|
||||
|
||||
beforeEach(() => bc = new BuildCreator(buildsDir));
|
||||
|
||||
|
||||
describe('constructor()', () => {
|
||||
|
||||
it('should throw if \'buildsDir\' is missing or empty', () => {
|
||||
expect(() => new BuildCreator('')).toThrowError('Missing or empty required parameter \'buildsDir\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should extend EventEmitter', () => {
|
||||
expect(bc).toEqual(jasmine.any(BuildCreator));
|
||||
expect(bc).toEqual(jasmine.any(EventEmitter));
|
||||
|
||||
expect(Object.getPrototypeOf(bc)).toBe(BuildCreator.prototype);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('create()', () => {
|
||||
let bcEmitSpy: jasmine.Spy;
|
||||
let bcExistsSpy: jasmine.Spy;
|
||||
let bcExtractArchiveSpy: jasmine.Spy;
|
||||
let shellMkdirSpy: jasmine.Spy;
|
||||
let shellRmSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
bcEmitSpy = spyOn(bc, 'emit');
|
||||
bcExistsSpy = spyOn(bc as any, 'exists');
|
||||
bcExtractArchiveSpy = spyOn(bc as any, 'extractArchive');
|
||||
shellMkdirSpy = spyOn(shell, 'mkdir');
|
||||
shellRmSpy = spyOn(shell, 'rm');
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', done => {
|
||||
const promise = bc.create(pr, sha, archive);
|
||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||
// to avoid running the actual `extractArchive()`.
|
||||
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should throw if the build does already exist', done => {
|
||||
bcExistsSpy.and.returnValue(true);
|
||||
bc.create(pr, sha, archive).catch(err => {
|
||||
expectToBeUploadError(err, 403, `Request to overwrite existing directory: ${shaDir}`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should create the build directory (and any parent directories)', done => {
|
||||
bc.create(pr, sha, archive).
|
||||
then(() => expect(shellMkdirSpy).toHaveBeenCalledWith('-p', shaDir)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should extract the archive contents into the build directory', done => {
|
||||
bc.create(pr, sha, archive).
|
||||
then(() => expect(bcExtractArchiveSpy).toHaveBeenCalledWith(archive, shaDir)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should emit a CreatedBuildEvent on success', done => {
|
||||
let emitted = false;
|
||||
|
||||
bcEmitSpy.and.callFake((type: string, evt: CreatedBuildEvent) => {
|
||||
expect(type).toBe(CreatedBuildEvent.type);
|
||||
expect(evt).toEqual(jasmine.any(CreatedBuildEvent));
|
||||
expect(evt.pr).toBe(+pr);
|
||||
expect(evt.sha).toBe(sha);
|
||||
|
||||
emitted = true;
|
||||
});
|
||||
|
||||
bc.create(pr, sha, archive).
|
||||
then(() => expect(emitted).toBe(true)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
describe('on error', () => {
|
||||
|
||||
it('should abort and skip further operations if it fails to create the directories', done => {
|
||||
shellMkdirSpy.and.throwError('');
|
||||
bc.create(pr, sha, archive).catch(() => {
|
||||
expect(shellMkdirSpy).toHaveBeenCalled();
|
||||
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if it fails to extract the archive', done => {
|
||||
bcExtractArchiveSpy.and.throwError('');
|
||||
bc.create(pr, sha, archive).catch(() => {
|
||||
expect(shellMkdirSpy).toHaveBeenCalled();
|
||||
expect(bcExtractArchiveSpy).toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should delete the PR directory (for new PR)', done => {
|
||||
bcExtractArchiveSpy.and.throwError('');
|
||||
bc.create(pr, sha, archive).catch(() => {
|
||||
expect(shellRmSpy).toHaveBeenCalledWith('-rf', prDir);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should delete the SHA directory (for existing PR)', done => {
|
||||
bcExistsSpy.and.callFake((path: string) => path !== shaDir);
|
||||
bcExtractArchiveSpy.and.throwError('');
|
||||
|
||||
bc.create(pr, sha, archive).catch(() => {
|
||||
expect(shellRmSpy).toHaveBeenCalledWith('-rf', shaDir);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should reject with an UploadError', done => {
|
||||
shellMkdirSpy.and.callFake(() => {throw 'Test'; });
|
||||
bc.create(pr, sha, archive).catch(err => {
|
||||
expectToBeUploadError(err, 500, `Error while uploading to directory: ${shaDir}\nTest`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should pass UploadError instances unmodified', done => {
|
||||
shellMkdirSpy.and.callFake(() => { throw new UploadError(543, 'Test'); });
|
||||
bc.create(pr, sha, archive).catch(err => {
|
||||
expectToBeUploadError(err, 543, 'Test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
// Protected methods
|
||||
|
||||
describe('exists()', () => {
|
||||
let fsAccessSpy: jasmine.Spy;
|
||||
let fsAccessCbs: Function[];
|
||||
|
||||
beforeEach(() => {
|
||||
fsAccessCbs = [];
|
||||
fsAccessSpy = spyOn(fs, 'access').and.callFake((_: string, cb: Function) => fsAccessCbs.push(cb));
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect((bc as any).exists('foo')).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should call \'fs.access()\' with the specified argument', () => {
|
||||
(bc as any).exists('foo');
|
||||
expect(fs.access).toHaveBeenCalledWith('foo', jasmine.any(Function));
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with \'true\' if \'fs.access()\' succeeds', done => {
|
||||
Promise.
|
||||
all([(bc as any).exists('foo'), (bc as any).exists('bar')]).
|
||||
then(results => expect(results).toEqual([true, true])).
|
||||
then(done);
|
||||
|
||||
fsAccessCbs[0]();
|
||||
fsAccessCbs[1](null);
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with \'false\' if \'fs.access()\' errors', done => {
|
||||
Promise.
|
||||
all([(bc as any).exists('foo'), (bc as any).exists('bar')]).
|
||||
then(results => expect(results).toEqual([false, false])).
|
||||
then(done);
|
||||
|
||||
fsAccessCbs[0]('Error');
|
||||
fsAccessCbs[1](new Error());
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('extractArchive()', () => {
|
||||
let consoleWarnSpy: jasmine.Spy;
|
||||
let shellChmodSpy: jasmine.Spy;
|
||||
let shellRmSpy: jasmine.Spy;
|
||||
let cpExecSpy: jasmine.Spy;
|
||||
let cpExecCbs: Function[];
|
||||
|
||||
beforeEach(() => {
|
||||
cpExecCbs = [];
|
||||
|
||||
consoleWarnSpy = spyOn(console, 'warn');
|
||||
shellChmodSpy = spyOn(shell, 'chmod');
|
||||
shellRmSpy = spyOn(shell, 'rm');
|
||||
cpExecSpy = spyOn(cp, 'exec').and.callFake((_: string, cb: Function) => cpExecCbs.push(cb));
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect((bc as any).extractArchive('foo', 'bar')).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should "gunzip" and "untar" the input file into the output directory', () => {
|
||||
const cmd = 'tar --extract --gzip --directory "output/dir" --file "input/file"';
|
||||
|
||||
(bc as any).extractArchive('input/file', 'output/dir');
|
||||
expect(cpExecSpy).toHaveBeenCalledWith(cmd, jasmine.any(Function));
|
||||
});
|
||||
|
||||
|
||||
it('should log (as a warning) any stderr output if extracting succeeded', done => {
|
||||
(bc as any).extractArchive('foo', 'bar').
|
||||
then(() => expect(consoleWarnSpy).toHaveBeenCalledWith('This is stderr')).
|
||||
then(done);
|
||||
|
||||
cpExecCbs[0](null, 'This is stdout', 'This is stderr');
|
||||
});
|
||||
|
||||
|
||||
it('should make the build directory non-writable', done => {
|
||||
(bc as any).extractArchive('foo', 'bar').
|
||||
then(() => expect(shellChmodSpy).toHaveBeenCalledWith('-R', 'a-w', 'bar')).
|
||||
then(done);
|
||||
|
||||
cpExecCbs[0]();
|
||||
});
|
||||
|
||||
|
||||
it('should delete the uploaded file on success', done => {
|
||||
(bc as any).extractArchive('input/file', 'output/dir').
|
||||
then(() => expect(shellRmSpy).toHaveBeenCalledWith('-f', 'input/file')).
|
||||
then(done);
|
||||
|
||||
cpExecCbs[0]();
|
||||
});
|
||||
|
||||
|
||||
describe('on error', () => {
|
||||
|
||||
it('should abort and skip further operations if it fails to extract the archive', done => {
|
||||
(bc as any).extractArchive('foo', 'bar').catch((err: any) => {
|
||||
expect(shellChmodSpy).not.toHaveBeenCalled();
|
||||
expect(shellRmSpy).not.toHaveBeenCalled();
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
cpExecCbs[0]('Test');
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if it fails to make non-writable', done => {
|
||||
(bc as any).extractArchive('foo', 'bar').catch((err: any) => {
|
||||
expect(shellChmodSpy).toHaveBeenCalled();
|
||||
expect(shellRmSpy).not.toHaveBeenCalled();
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
shellChmodSpy.and.callFake(() => { throw 'Test'; });
|
||||
cpExecCbs[0]();
|
||||
});
|
||||
|
||||
|
||||
it('should abort and reject if it fails to remove the uploaded file', done => {
|
||||
(bc as any).extractArchive('foo', 'bar').catch((err: any) => {
|
||||
expect(shellChmodSpy).toHaveBeenCalled();
|
||||
expect(shellRmSpy).toHaveBeenCalled();
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
shellRmSpy.and.callFake(() => { throw 'Test'; });
|
||||
cpExecCbs[0]();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -0,0 +1,61 @@
|
||||
// Imports
|
||||
import {BuildEvent, CreatedBuildEvent} from '../../lib/upload-server/build-events';
|
||||
|
||||
// Tests
|
||||
describe('BuildEvent', () => {
|
||||
let evt: BuildEvent;
|
||||
|
||||
beforeEach(() => evt = new BuildEvent('foo', 42, 'bar'));
|
||||
|
||||
|
||||
it('should have a \'type\' property', () => {
|
||||
expect(evt.type).toBe('foo');
|
||||
});
|
||||
|
||||
|
||||
it('should have a \'pr\' property', () => {
|
||||
expect(evt.pr).toBe(42);
|
||||
});
|
||||
|
||||
|
||||
it('should have a \'sha\' property', () => {
|
||||
expect(evt.sha).toBe('bar');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('CreatedBuildEvent', () => {
|
||||
let evt: CreatedBuildEvent;
|
||||
|
||||
beforeEach(() => evt = new CreatedBuildEvent(42, 'bar'));
|
||||
|
||||
|
||||
it('should have a static \'type\' property', () => {
|
||||
expect(CreatedBuildEvent.type).toBe('build.created');
|
||||
});
|
||||
|
||||
|
||||
it('should extend BuildEvent', () => {
|
||||
expect(evt).toEqual(jasmine.any(CreatedBuildEvent));
|
||||
expect(evt).toEqual(jasmine.any(BuildEvent));
|
||||
|
||||
expect(Object.getPrototypeOf(evt)).toBe(CreatedBuildEvent.prototype);
|
||||
});
|
||||
|
||||
|
||||
it('should automatically set the \'type\'', () => {
|
||||
expect(evt.type).toBe(CreatedBuildEvent.type);
|
||||
});
|
||||
|
||||
|
||||
it('should have a \'pr\' property', () => {
|
||||
expect(evt.pr).toBe(42);
|
||||
});
|
||||
|
||||
|
||||
it('should have a \'sha\' property', () => {
|
||||
expect(evt.sha).toBe('bar');
|
||||
});
|
||||
|
||||
});
|
@ -0,0 +1,261 @@
|
||||
// Imports
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||
import {GithubTeams} from '../../lib/common/github-teams';
|
||||
import {BuildVerifier} from '../../lib/upload-server/build-verifier';
|
||||
import {expectToBeUploadError} from './helpers';
|
||||
|
||||
// Tests
|
||||
describe('BuildVerifier', () => {
|
||||
const defaultConfig = {
|
||||
allowedTeamSlugs: ['team1', 'team2'],
|
||||
githubToken: 'githubToken',
|
||||
organization: 'organization',
|
||||
repoSlug: 'repo/slug',
|
||||
secret: 'secret',
|
||||
};
|
||||
let bv: BuildVerifier;
|
||||
|
||||
// Helpers
|
||||
const createBuildVerifier = (partialConfig: Partial<typeof defaultConfig> = {}) => {
|
||||
const cfg = {...defaultConfig, ...partialConfig};
|
||||
return new BuildVerifier(cfg.secret, cfg.githubToken, cfg.repoSlug, cfg.organization,
|
||||
cfg.allowedTeamSlugs);
|
||||
};
|
||||
|
||||
beforeEach(() => bv = createBuildVerifier());
|
||||
|
||||
|
||||
describe('constructor()', () => {
|
||||
|
||||
['secret', 'githubToken', 'repoSlug', 'organization', 'allowedTeamSlugs'].forEach(param => {
|
||||
it(`should throw if '${param}' is missing or empty`, () => {
|
||||
expect(() => createBuildVerifier({[param]: ''})).
|
||||
toThrowError(`Missing or empty required parameter '${param}'!`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'allowedTeamSlugs\' is an empty array', () => {
|
||||
expect(() => createBuildVerifier({allowedTeamSlugs: []})).
|
||||
toThrowError('Missing or empty required parameter \'allowedTeamSlugs\'!');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('verify()', () => {
|
||||
const pr = 9;
|
||||
const defaultJwt = {
|
||||
'exp': Math.floor(Date.now() / 1000) + 30,
|
||||
'iat': Math.floor(Date.now() / 1000) - 30,
|
||||
'iss': 'Travis CI, GmbH',
|
||||
'pull-request': pr,
|
||||
'slug': defaultConfig.repoSlug,
|
||||
};
|
||||
let bvGetPrAuthorTeamMembership: jasmine.Spy;
|
||||
|
||||
// Heleprs
|
||||
const createAuthHeader = (partialJwt: Partial<typeof defaultJwt> = {}, secret: string = defaultConfig.secret) =>
|
||||
`Token ${jwt.sign({...defaultJwt, ...partialJwt}, secret)}`;
|
||||
|
||||
beforeEach(() => {
|
||||
bvGetPrAuthorTeamMembership = spyOn(bv, 'getPrAuthorTeamMembership').
|
||||
and.returnValue(Promise.resolve({author: 'some-author', isMember: true}));
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', done => {
|
||||
const promise = bv.verify(pr, createAuthHeader());
|
||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||
// to avoid running the actual `bvGetPrAuthorTeamMembership()`.
|
||||
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should fail if the authorization header is invalid', done => {
|
||||
bv.verify(pr, 'foo').catch(err => {
|
||||
const errorMessage = 'Error while verifying upload for PR 9: jwt malformed';
|
||||
|
||||
expectToBeUploadError(err, 403, errorMessage);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should fail if the secret is invalid', done => {
|
||||
bv.verify(pr, createAuthHeader({}, 'foo')).catch(err => {
|
||||
const errorMessage = 'Error while verifying upload for PR 9: invalid signature';
|
||||
|
||||
expectToBeUploadError(err, 403, errorMessage);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should fail if the issuer is invalid', done => {
|
||||
bv.verify(pr, createAuthHeader({iss: 'not valid'})).catch(err => {
|
||||
const errorMessage = 'Error while verifying upload for PR 9: ' +
|
||||
`jwt issuer invalid. expected: ${defaultJwt.iss}`;
|
||||
|
||||
expectToBeUploadError(err, 403, errorMessage);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should fail if the token has expired', done => {
|
||||
bv.verify(pr, createAuthHeader({exp: 0})).catch(err => {
|
||||
const errorMessage = 'Error while verifying upload for PR 9: jwt expired';
|
||||
|
||||
expectToBeUploadError(err, 403, errorMessage);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should fail if the repo slug does not match', done => {
|
||||
bv.verify(pr, createAuthHeader({slug: 'foo/bar'})).catch(err => {
|
||||
const errorMessage = 'Error while verifying upload for PR 9: ' +
|
||||
`jwt slug invalid. expected: ${defaultConfig.repoSlug}`;
|
||||
|
||||
expectToBeUploadError(err, 403, errorMessage);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should fail if the PR does not match', done => {
|
||||
bv.verify(pr, createAuthHeader({'pull-request': 1337})).catch(err => {
|
||||
const errorMessage = 'Error while verifying upload for PR 9: ' +
|
||||
`jwt pull-request invalid. expected: ${pr}`;
|
||||
|
||||
expectToBeUploadError(err, 403, errorMessage);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should not fail if the token is valid', done => {
|
||||
bv.verify(pr, createAuthHeader()).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should not fail even if the token has been issued in the future', done => {
|
||||
const in30s = Math.floor(Date.now() / 1000) + 30;
|
||||
bv.verify(pr, createAuthHeader({iat: in30s})).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should call \'getPrAuthorTeamMembership()\' if the token is valid', done => {
|
||||
bv.verify(pr, createAuthHeader()).then(() => {
|
||||
expect(bvGetPrAuthorTeamMembership).toHaveBeenCalledWith(pr);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should fail if \'getPrAuthorTeamMembership()\' rejects', done => {
|
||||
bvGetPrAuthorTeamMembership.and.callFake(() => Promise.reject('Test'));
|
||||
bv.verify(pr, createAuthHeader()).catch(err => {
|
||||
expectToBeUploadError(err, 403, `Error while verifying upload for PR ${pr}: Test`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should fail if \'getPrAuthorTeamMembership()\' reports no membership', done => {
|
||||
const errorMessage = `Error while verifying upload for PR ${pr}: User 'test' is not an active member of any of ` +
|
||||
'the following teams: team1, team2';
|
||||
|
||||
bvGetPrAuthorTeamMembership.and.returnValue(Promise.resolve({author: 'test', isMember: false}));
|
||||
bv.verify(pr, createAuthHeader()).catch(err => {
|
||||
expectToBeUploadError(err, 403, errorMessage);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should succeed if everything checks outs', done => {
|
||||
bv.verify(pr, createAuthHeader()).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('getPrAuthorTeamMembership()', () => {
|
||||
const pr = 9;
|
||||
let prsFetchSpy: jasmine.Spy;
|
||||
let teamsIsMemberBySlugSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
prsFetchSpy = spyOn(GithubPullRequests.prototype, 'fetch').
|
||||
and.returnValue(Promise.resolve({user: {login: 'username'}}));
|
||||
|
||||
teamsIsMemberBySlugSpy = spyOn(GithubTeams.prototype, 'isMemberBySlug').
|
||||
and.returnValue(Promise.resolve(true));
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', done => {
|
||||
const promise = bv.getPrAuthorTeamMembership(pr);
|
||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||
// to avoid running the actual `GithubTeams#isMemberBySlug()`.
|
||||
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should fetch the corresponding PR', done => {
|
||||
bv.getPrAuthorTeamMembership(pr).then(() => {
|
||||
expect(prsFetchSpy).toHaveBeenCalledWith(pr);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should fail if fetching the PR errors', done => {
|
||||
prsFetchSpy.and.callFake(() => Promise.reject('Test'));
|
||||
bv.getPrAuthorTeamMembership(pr).catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should verify the PR author\'s membership in the specified teams', done => {
|
||||
bv.getPrAuthorTeamMembership(pr).then(() => {
|
||||
expect(teamsIsMemberBySlugSpy).toHaveBeenCalledWith('username', ['team1', 'team2']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should fail if verifying membership errors', done => {
|
||||
teamsIsMemberBySlugSpy.and.callFake(() => Promise.reject('Test'));
|
||||
bv.getPrAuthorTeamMembership(pr).catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should return the PR\'s author and whether they are members', done => {
|
||||
teamsIsMemberBySlugSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||
|
||||
Promise.all([
|
||||
bv.getPrAuthorTeamMembership(pr).then(({author, isMember}) => {
|
||||
expect(author).toBe('username');
|
||||
expect(isMember).toBe(true);
|
||||
}),
|
||||
bv.getPrAuthorTeamMembership(pr).then(({author, isMember}) => {
|
||||
expect(author).toBe('username');
|
||||
expect(isMember).toBe(false);
|
||||
}),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -0,0 +1,11 @@
|
||||
import {UploadError} from '../../lib/upload-server/upload-error';
|
||||
|
||||
export const expectToBeUploadError = (actual: UploadError, status?: number, message?: string) => {
|
||||
expect(actual).toEqual(jasmine.any(UploadError));
|
||||
if (status != null) {
|
||||
expect(actual.status).toBe(status);
|
||||
}
|
||||
if (message != null) {
|
||||
expect(actual.message).toBe(message);
|
||||
}
|
||||
};
|
@ -0,0 +1,39 @@
|
||||
// Imports
|
||||
import {UploadError} from '../../lib/upload-server/upload-error';
|
||||
|
||||
// Tests
|
||||
describe('UploadError', () => {
|
||||
let err: UploadError;
|
||||
|
||||
beforeEach(() => err = new UploadError(999, 'message'));
|
||||
|
||||
|
||||
it('should extend Error', () => {
|
||||
expect(err).toEqual(jasmine.any(UploadError));
|
||||
expect(err).toEqual(jasmine.any(Error));
|
||||
|
||||
expect(Object.getPrototypeOf(err)).toBe(UploadError.prototype);
|
||||
});
|
||||
|
||||
|
||||
it('should have a \'status\' property', () => {
|
||||
expect(err.status).toBe(999);
|
||||
});
|
||||
|
||||
|
||||
it('should have a \'message\' property', () => {
|
||||
expect(err.message).toBe('message');
|
||||
});
|
||||
|
||||
|
||||
it('should have a 500 \'status\' by default', () => {
|
||||
expect(new UploadError().status).toBe(500);
|
||||
});
|
||||
|
||||
|
||||
it('should have an empty \'message\' by default', () => {
|
||||
expect(new UploadError().message).toBe('');
|
||||
expect(new UploadError(999).message).toBe('');
|
||||
});
|
||||
|
||||
});
|
@ -0,0 +1,403 @@
|
||||
// Imports
|
||||
import * as express from 'express';
|
||||
import * as http from 'http';
|
||||
import * as supertest from 'supertest';
|
||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||
import {BuildCreator} from '../../lib/upload-server/build-creator';
|
||||
import {CreatedBuildEvent} from '../../lib/upload-server/build-events';
|
||||
import {BuildVerifier} from '../../lib/upload-server/build-verifier';
|
||||
import {uploadServerFactory as usf} from '../../lib/upload-server/upload-server-factory';
|
||||
|
||||
// Tests
|
||||
describe('uploadServerFactory', () => {
|
||||
const defaultConfig = {
|
||||
buildsDir: 'builds/dir',
|
||||
domainName: 'domain.name',
|
||||
githubOrganization: 'organization',
|
||||
githubTeamSlugs: ['team1', 'team2'],
|
||||
githubToken: '12345',
|
||||
repoSlug: 'repo/slug',
|
||||
secret: 'secret',
|
||||
};
|
||||
|
||||
// Helpers
|
||||
const createUploadServer = (partialConfig: Partial<typeof defaultConfig> = {}) =>
|
||||
usf.create({...defaultConfig, ...partialConfig});
|
||||
|
||||
|
||||
describe('create()', () => {
|
||||
let usfCreateMiddlewareSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
usfCreateMiddlewareSpy = spyOn(usf as any, 'createMiddleware').and.callThrough();
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'buildsDir\' is missing or empty', () => {
|
||||
expect(() => createUploadServer({buildsDir: ''})).
|
||||
toThrowError('Missing or empty required parameter \'buildsDir\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'domainName\' is missing or empty', () => {
|
||||
expect(() => createUploadServer({domainName: ''})).
|
||||
toThrowError('Missing or empty required parameter \'domainName\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'githubToken\' is missing or empty', () => {
|
||||
expect(() => createUploadServer({githubToken: ''})).
|
||||
toThrowError('Missing or empty required parameter \'githubToken\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'githubOrganization\' is missing or empty', () => {
|
||||
expect(() => createUploadServer({githubOrganization: ''})).
|
||||
toThrowError('Missing or empty required parameter \'organization\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'githubTeamSlugs\' is missing or empty', () => {
|
||||
expect(() => createUploadServer({githubTeamSlugs: []})).
|
||||
toThrowError('Missing or empty required parameter \'allowedTeamSlugs\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'repoSlug\' is missing or empty', () => {
|
||||
expect(() => createUploadServer({repoSlug: ''})).
|
||||
toThrowError('Missing or empty required parameter \'repoSlug\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'secret\' is missing or empty', () => {
|
||||
expect(() => createUploadServer({secret: ''})).
|
||||
toThrowError('Missing or empty required parameter \'secret\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should return an http.Server', () => {
|
||||
const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough();
|
||||
const server = createUploadServer();
|
||||
|
||||
expect(server).toBe(httpCreateServerSpy.calls.mostRecent().returnValue);
|
||||
});
|
||||
|
||||
|
||||
it('should create and use an appropriate BuildCreator', () => {
|
||||
const usfCreateBuildCreatorSpy = spyOn(usf as any, 'createBuildCreator').and.callThrough();
|
||||
|
||||
createUploadServer();
|
||||
const buildCreator: BuildCreator = usfCreateBuildCreatorSpy.calls.mostRecent().returnValue;
|
||||
|
||||
expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(jasmine.any(BuildVerifier), buildCreator);
|
||||
expect(usfCreateBuildCreatorSpy).toHaveBeenCalledWith('builds/dir', '12345', 'repo/slug', 'domain.name');
|
||||
});
|
||||
|
||||
|
||||
it('should create and use an appropriate middleware', () => {
|
||||
const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough();
|
||||
|
||||
createUploadServer();
|
||||
const middleware: express.Express = usfCreateMiddlewareSpy.calls.mostRecent().returnValue;
|
||||
const buildVerifier = jasmine.any(BuildVerifier);
|
||||
const buildCreator = jasmine.any(BuildCreator);
|
||||
|
||||
expect(httpCreateServerSpy).toHaveBeenCalledWith(middleware);
|
||||
expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(buildVerifier, buildCreator);
|
||||
});
|
||||
|
||||
|
||||
it('should log the server address info on \'listening\'', () => {
|
||||
const consoleInfoSpy = spyOn(console, 'info');
|
||||
const server = createUploadServer('builds/dir');
|
||||
server.address = () => ({address: 'foo', family: '', port: 1337});
|
||||
|
||||
expect(consoleInfoSpy).not.toHaveBeenCalled();
|
||||
|
||||
server.emit('listening');
|
||||
expect(consoleInfoSpy).toHaveBeenCalledWith('Up and running (and listening on foo:1337)...');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
// Protected methods
|
||||
|
||||
describe('createBuildCreator()', () => {
|
||||
let buildCreator: BuildCreator;
|
||||
|
||||
beforeEach(() => {
|
||||
buildCreator = (usf as any).createBuildCreator(
|
||||
defaultConfig.buildsDir,
|
||||
defaultConfig.githubToken,
|
||||
defaultConfig.repoSlug,
|
||||
defaultConfig.domainName,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it('should pass the \'buildsDir\' to the BuildCreator', () => {
|
||||
expect((buildCreator as any).buildsDir).toBe('builds/dir');
|
||||
});
|
||||
|
||||
|
||||
it('should post a comment on GitHub on \'build.created\'', () => {
|
||||
const prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment');
|
||||
const commentBody = 'The angular.io preview for 1234567 is available [here][1].\n\n' +
|
||||
'[1]: https://pr42-1234567890.domain.name/';
|
||||
|
||||
buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890'});
|
||||
|
||||
expect(prsAddCommentSpy).toHaveBeenCalledWith(42, commentBody);
|
||||
});
|
||||
|
||||
|
||||
it('should pass the correct \'githubToken\' and \'repoSlug\' to GithubPullRequests', () => {
|
||||
const prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment');
|
||||
|
||||
buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890'});
|
||||
const prs = prsAddCommentSpy.calls.mostRecent().object;
|
||||
|
||||
expect(prs).toEqual(jasmine.any(GithubPullRequests));
|
||||
expect((prs as any).repoSlug).toBe('repo/slug');
|
||||
expect((prs as any).requestHeaders.Authorization).toContain('12345');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('createMiddleware()', () => {
|
||||
let buildVerifier: BuildVerifier;
|
||||
let buildCreator: BuildCreator;
|
||||
let agent: supertest.SuperTest<supertest.Test>;
|
||||
|
||||
// Helpers
|
||||
const promisifyRequest = (req: supertest.Request) =>
|
||||
new Promise((resolve, reject) => req.end(err => err ? reject(err) : resolve()));
|
||||
const verifyRequests = (reqs: supertest.Request[], done: jasmine.DoneFn) =>
|
||||
Promise.all(reqs.map(promisifyRequest)).then(done, done.fail);
|
||||
|
||||
beforeEach(() => {
|
||||
buildVerifier = new BuildVerifier(
|
||||
defaultConfig.secret,
|
||||
defaultConfig.githubToken,
|
||||
defaultConfig.repoSlug,
|
||||
defaultConfig.githubOrganization,
|
||||
defaultConfig.githubTeamSlugs,
|
||||
);
|
||||
buildCreator = new BuildCreator(defaultConfig.buildsDir);
|
||||
agent = supertest.agent((usf as any).createMiddleware(buildVerifier, buildCreator));
|
||||
|
||||
spyOn(console, 'error');
|
||||
});
|
||||
|
||||
|
||||
describe('GET /create-build/<pr>/<sha>', () => {
|
||||
const pr = '9';
|
||||
const sha = '9'.repeat(40);
|
||||
let buildVerifierVerifySpy: jasmine.Spy;
|
||||
let buildCreatorCreateSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
buildVerifierVerifySpy = spyOn(buildVerifier, 'verify').and.returnValue(Promise.resolve());
|
||||
buildCreatorCreateSpy = spyOn(buildCreator, 'create').and.returnValue(Promise.resolve());
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 405 for non-GET requests', done => {
|
||||
verifyRequests([
|
||||
agent.put(`/create-build/${pr}/${sha}`).expect(405),
|
||||
agent.post(`/create-build/${pr}/${sha}`).expect(405),
|
||||
agent.patch(`/create-build/${pr}/${sha}`).expect(405),
|
||||
agent.delete(`/create-build/${pr}/${sha}`).expect(405),
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 401 for requests without an \'AUTHORIZATION\' header', done => {
|
||||
const url = `/create-build/${pr}/${sha}`;
|
||||
const responseBody = `Missing or empty 'AUTHORIZATION' header in request: GET ${url}`;
|
||||
|
||||
verifyRequests([
|
||||
agent.get(url).expect(401, responseBody),
|
||||
agent.get(url).set('AUTHORIZATION', '').expect(401, responseBody),
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 400 for requests without an \'X-FILE\' header', done => {
|
||||
const url = `/create-build/${pr}/${sha}`;
|
||||
const responseBody = `Missing or empty 'X-FILE' header in request: GET ${url}`;
|
||||
|
||||
const request1 = agent.get(url).set('AUTHORIZATION', 'foo');
|
||||
const request2 = agent.get(url).set('AUTHORIZATION', 'foo').set('X-FILE', '');
|
||||
|
||||
verifyRequests([
|
||||
request1.expect(400, responseBody),
|
||||
request2.expect(400, responseBody),
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths', done => {
|
||||
verifyRequests([
|
||||
agent.get(`/foo/create-build/${pr}/${sha}`).expect(404),
|
||||
agent.get(`/foo-create-build/${pr}/${sha}`).expect(404),
|
||||
agent.get(`/fooncreate-build/${pr}/${sha}`).expect(404),
|
||||
agent.get(`/create-build/foo/${pr}/${sha}`).expect(404),
|
||||
agent.get(`/create-build-foo/${pr}/${sha}`).expect(404),
|
||||
agent.get(`/create-buildnfoo/${pr}/${sha}`).expect(404),
|
||||
agent.get(`/create-build/pr${pr}/${sha}`).expect(404),
|
||||
agent.get(`/create-build/${pr}/${sha}42`).expect(404),
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
it('should call \'BuildVerifier#verify()\' with the correct arguments', done => {
|
||||
const req = agent.
|
||||
get(`/create-build/${pr}/${sha}`).
|
||||
set('AUTHORIZATION', 'foo').
|
||||
set('X-FILE', 'bar');
|
||||
|
||||
promisifyRequest(req).
|
||||
then(() => expect(buildVerifierVerifySpy).toHaveBeenCalledWith(9, 'foo')).
|
||||
then(done, done.fail);
|
||||
});
|
||||
|
||||
|
||||
it('should propagate errors from BuildVerifier', done => {
|
||||
buildVerifierVerifySpy.and.callFake(() => Promise.reject('Test'));
|
||||
|
||||
const req = agent.
|
||||
get(`/create-build/${pr}/${sha}`).
|
||||
set('AUTHORIZATION', 'foo').
|
||||
set('X-FILE', 'bar').
|
||||
expect(500, 'Test');
|
||||
|
||||
promisifyRequest(req).
|
||||
then(() => {
|
||||
expect(buildVerifierVerifySpy).toHaveBeenCalledWith(9, 'foo');
|
||||
expect(buildCreatorCreateSpy).not.toHaveBeenCalled();
|
||||
}).
|
||||
then(done, done.fail);
|
||||
});
|
||||
|
||||
|
||||
it('should call \'BuildCreator#create()\' with the correct arguments', done => {
|
||||
const req = agent.
|
||||
get(`/create-build/${pr}/${sha}`).
|
||||
set('AUTHORIZATION', 'foo').
|
||||
set('X-FILE', 'bar');
|
||||
|
||||
promisifyRequest(req).
|
||||
then(() => expect(buildCreatorCreateSpy).toHaveBeenCalledWith(pr, sha, 'bar')).
|
||||
then(done, done.fail);
|
||||
});
|
||||
|
||||
|
||||
it('should propagate errors from BuildCreator', done => {
|
||||
buildCreatorCreateSpy.and.callFake(() => Promise.reject('Test'));
|
||||
const req = agent.
|
||||
get(`/create-build/${pr}/${sha}`).
|
||||
set('AUTHORIZATION', 'foo').
|
||||
set('X-FILE', 'bar').
|
||||
expect(500, 'Test');
|
||||
|
||||
verifyRequests([req], done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 201 on successful upload', done => {
|
||||
const req = agent.
|
||||
get(`/create-build/${pr}/${sha}`).
|
||||
set('AUTHORIZATION', 'foo').
|
||||
set('X-FILE', 'bar').
|
||||
expect(201, http.STATUS_CODES[201]);
|
||||
|
||||
verifyRequests([req], done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject PRs with leading zeros', done => {
|
||||
verifyRequests([agent.get(`/create-build/0${pr}/${sha}`).expect(404)], done);
|
||||
});
|
||||
|
||||
|
||||
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
||||
const sha40 = '0'.repeat(40);
|
||||
const sha41 = `0${sha40}`;
|
||||
|
||||
const request40 = agent.get(`/create-build/${pr}/${sha40}`).set('AUTHORIZATION', 'foo').set('X-FILE', 'bar');
|
||||
const request41 = agent.get(`/create-build/${pr}/${sha41}`).set('AUTHORIZATION', 'baz').set('X-FILE', 'qux');
|
||||
|
||||
Promise.all([
|
||||
promisifyRequest(request40.expect(201)),
|
||||
promisifyRequest(request41.expect(404)),
|
||||
]).then(done, done.fail);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('GET /health-check', () => {
|
||||
|
||||
it('should respond with 200', done => {
|
||||
verifyRequests([
|
||||
agent.get('/health-check').expect(200),
|
||||
agent.get('/health-check/').expect(200),
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 405 for non-GET requests', done => {
|
||||
verifyRequests([
|
||||
agent.put('/health-check').expect(405),
|
||||
agent.post('/health-check').expect(405),
|
||||
agent.patch('/health-check').expect(405),
|
||||
agent.delete('/health-check').expect(405),
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 if the path does not match exactly', done => {
|
||||
verifyRequests([
|
||||
agent.get('/health-check/foo').expect(404),
|
||||
agent.get('/health-check-foo').expect(404),
|
||||
agent.get('/health-checknfoo').expect(404),
|
||||
agent.get('/foo/health-check').expect(404),
|
||||
agent.get('/foo-health-check').expect(404),
|
||||
agent.get('/foonhealth-check').expect(404),
|
||||
], done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('GET *', () => {
|
||||
|
||||
it('should respond with 404', done => {
|
||||
const responseBody = 'Unknown resource in request: GET /some/url';
|
||||
verifyRequests([agent.get('/some/url').expect(404, responseBody)], done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('ALL *', () => {
|
||||
|
||||
it('should respond with 405', done => {
|
||||
const responseFor = (method: string) => `Unsupported method in request: ${method.toUpperCase()} /some/url`;
|
||||
|
||||
verifyRequests([
|
||||
agent.put('/some/url').expect(405, responseFor('put')),
|
||||
agent.post('/some/url').expect(405, responseFor('post')),
|
||||
agent.patch('/some/url').expect(405, responseFor('patch')),
|
||||
agent.delete('/some/url').expect(405, responseFor('delete')),
|
||||
], done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
28
aio/aio-builds-setup/dockerbuild/scripts-js/tsconfig.json
Normal file
28
aio/aio-builds-setup/dockerbuild/scripts-js/tsconfig.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"alwaysStrict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"inlineSourceMap": true,
|
||||
"lib": [
|
||||
"es2016"
|
||||
],
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"outDir": "dist",
|
||||
"pretty": true,
|
||||
"rootDir": ".",
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
"target": "es5",
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"lib/**/*",
|
||||
"test/**/*"
|
||||
]
|
||||
}
|
15
aio/aio-builds-setup/dockerbuild/scripts-js/tslint.json
Normal file
15
aio/aio-builds-setup/dockerbuild/scripts-js/tslint.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "tslint:recommended",
|
||||
"rules": {
|
||||
"array-type": [true, "array"],
|
||||
"arrow-parens": [true, "ban-single-arg-parens"],
|
||||
"interface-name": [true, "never-prefix"],
|
||||
"max-classes-per-file": [true, 4],
|
||||
"no-consecutive-blank-lines": [true, 2],
|
||||
"no-console": false,
|
||||
"no-namespace": [true, "allow-declarations"],
|
||||
"no-string-literal": false,
|
||||
"quotemark": [true, "single"],
|
||||
"variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"]
|
||||
}
|
||||
}
|
2552
aio/aio-builds-setup/dockerbuild/scripts-js/yarn.lock
Normal file
2552
aio/aio-builds-setup/dockerbuild/scripts-js/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
8
aio/aio-builds-setup/dockerbuild/scripts-sh/clean-up.sh
Executable file
8
aio/aio-builds-setup/dockerbuild/scripts-sh/clean-up.sh
Executable file
@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
set -e -o pipefail
|
||||
|
||||
# Set up env variables
|
||||
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null)
|
||||
|
||||
# Run the clean-up
|
||||
node $AIO_SCRIPTS_JS_DIR/dist/lib/clean-up >> /var/log/aio/clean-up.log 2>&1
|
53
aio/aio-builds-setup/dockerbuild/scripts-sh/health-check.sh
Normal file
53
aio/aio-builds-setup/dockerbuild/scripts-sh/health-check.sh
Normal file
@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
set +e -o pipefail
|
||||
|
||||
|
||||
# Variables
|
||||
exitCode=0
|
||||
|
||||
|
||||
# Helpers
|
||||
function reportStatus {
|
||||
local lastExitCode=$?
|
||||
echo "$1: $([[ $lastExitCode -eq 0 ]] && echo OK || echo NOT OK)"
|
||||
[[ $lastExitCode -eq 0 ]] || exitCode=1
|
||||
}
|
||||
|
||||
|
||||
# Check services
|
||||
services=(
|
||||
rsyslog
|
||||
cron
|
||||
nginx
|
||||
pm2-root
|
||||
)
|
||||
for s in ${services[@]}; do
|
||||
service $s status > /dev/null
|
||||
reportStatus "Service '$s'"
|
||||
done
|
||||
|
||||
|
||||
# Check servers
|
||||
origins=(
|
||||
http://$AIO_UPLOAD_HOSTNAME:$AIO_UPLOAD_PORT
|
||||
http://$AIO_NGINX_HOSTNAME:$AIO_NGINX_PORT_HTTP
|
||||
https://$AIO_NGINX_HOSTNAME:$AIO_NGINX_PORT_HTTPS
|
||||
)
|
||||
for o in ${origins[@]}; do
|
||||
curl --fail --silent $o/health-check > /dev/null
|
||||
reportStatus "Server '$o'"
|
||||
done
|
||||
|
||||
|
||||
# Check resolution of external URLs
|
||||
origins=(
|
||||
https://google.com
|
||||
)
|
||||
for o in ${origins[@]}; do
|
||||
curl --fail --silent $o > /dev/null
|
||||
reportStatus "External URL '$o'"
|
||||
done
|
||||
|
||||
|
||||
# Exit
|
||||
exit $exitCode
|
18
aio/aio-builds-setup/dockerbuild/scripts-sh/init.sh
Executable file
18
aio/aio-builds-setup/dockerbuild/scripts-sh/init.sh
Executable file
@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
set -e -o pipefail
|
||||
|
||||
exec >> /var/log/aio/init.log
|
||||
exec 2>&1
|
||||
|
||||
# Start the services
|
||||
echo [`date`] - Starting services...
|
||||
mkdir -p $AIO_NGINX_LOGS_DIR
|
||||
mkdir -p $TEST_AIO_NGINX_LOGS_DIR
|
||||
|
||||
service rsyslog start
|
||||
service cron start
|
||||
service dnsmasq start
|
||||
service nginx start
|
||||
service pm2-root start
|
||||
aio-upload-server-prod start
|
||||
echo [`date`] - Services started successfully.
|
15
aio/aio-builds-setup/dockerbuild/scripts-sh/upload-server-prod.sh
Executable file
15
aio/aio-builds-setup/dockerbuild/scripts-sh/upload-server-prod.sh
Executable file
@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
set -e -o pipefail
|
||||
|
||||
# Set up env variables for production
|
||||
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null)
|
||||
export AIO_PREVIEW_DEPLOYMENT_TOKEN=$(head -c -1 /aio-secrets/PREVIEW_DEPLOYMENT_TOKEN 2>/dev/null)
|
||||
|
||||
# Start the upload-server instance
|
||||
# TODO(gkalpak): Ideally, the upload server should be run as a non-privileged user.
|
||||
# (Currently, there doesn't seem to be a straight forward way.)
|
||||
action=$([ "$1" == "stop" ] && echo "stop" || echo "start")
|
||||
pm2 $action $AIO_SCRIPTS_JS_DIR/dist/lib/upload-server \
|
||||
--log /var/log/aio/upload-server-prod.log \
|
||||
--name aio-upload-server-prod \
|
||||
${@:2}
|
@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
set -e -o pipefail
|
||||
|
||||
# Set up env variables for testing
|
||||
export AIO_BUILDS_DIR=$TEST_AIO_BUILDS_DIR
|
||||
export AIO_DOMAIN_NAME=$TEST_AIO_DOMAIN_NAME
|
||||
export AIO_GITHUB_ORGANIZATION=$TEST_AIO_GITHUB_ORGANIZATION
|
||||
export AIO_GITHUB_TEAM_SLUGS=$TEST_AIO_GITHUB_TEAM_SLUGS
|
||||
export AIO_PREVIEW_DEPLOYMENT_TOKEN=$TEST_AIO_PREVIEW_DEPLOYMENT_TOKEN
|
||||
export AIO_REPO_SLUG=$TEST_AIO_REPO_SLUG
|
||||
export AIO_UPLOAD_HOSTNAME=$TEST_AIO_UPLOAD_HOSTNAME
|
||||
export AIO_UPLOAD_PORT=$TEST_AIO_UPLOAD_PORT
|
||||
|
||||
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/TEST_GITHUB_TOKEN 2>/dev/null || echo "TEST_GITHUB_TOKEN")
|
||||
export AIO_PREVIEW_DEPLOYMENT_TOKEN=$(head -c -1 /aio-secrets/TEST_PREVIEW_DEPLOYMENT_TOKEN 2>/dev/null || echo "TEST_PREVIEW_DEPLOYMENT_TOKEN")
|
||||
|
||||
# Start the upload-server instance
|
||||
# TODO(gkalpak): Ideally, the upload server should be run as a non-privileged user.
|
||||
# (Currently, there doesn't seem to be a straight forward way.)
|
||||
appName=aio-upload-server-test
|
||||
if [[ "$1" == "stop" ]]; then
|
||||
pm2 delete $appName
|
||||
else
|
||||
pm2 start $AIO_SCRIPTS_JS_DIR/dist/lib/upload-server/index-test.js \
|
||||
--log /var/log/aio/upload-server-test.log \
|
||||
--name $appName \
|
||||
--no-autorestart \
|
||||
${@:2}
|
||||
fi
|
40
aio/aio-builds-setup/dockerbuild/scripts-sh/verify-setup.sh
Normal file
40
aio/aio-builds-setup/dockerbuild/scripts-sh/verify-setup.sh
Normal file
@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
set -e -o pipefail
|
||||
|
||||
logFile=/var/log/aio/verify-setup.log
|
||||
uploadServerLogFile=/var/log/aio/upload-server-verify-setup.log
|
||||
|
||||
exec 3>&1
|
||||
exec >> $logFile
|
||||
exec 2>&1
|
||||
|
||||
echo "[`date`] - Starting verification..."
|
||||
|
||||
# Helpers
|
||||
function countdown {
|
||||
message=$1
|
||||
secs=$2
|
||||
while [ $secs -gt 0 ]; do
|
||||
echo -ne "$message in $secs...\033[0K\r"
|
||||
sleep 1
|
||||
: $((secs--))
|
||||
done
|
||||
echo -ne "\033[0K\r"
|
||||
}
|
||||
|
||||
function onExit {
|
||||
aio-upload-server-test stop
|
||||
echo -e "Full logs in '$logFile'.\n" > /dev/fd/3
|
||||
}
|
||||
|
||||
# Setup EXIT trap
|
||||
trap 'onExit' EXIT
|
||||
|
||||
# Start an upload-server instance for testing
|
||||
aio-upload-server-test start --log $uploadServerLogFile
|
||||
|
||||
# Give the upload-server some time to start :(
|
||||
countdown "Starting" 5 > /dev/fd/3
|
||||
|
||||
# Run the tests
|
||||
node $AIO_SCRIPTS_JS_DIR/dist/lib/verify-setup | tee /dev/fd/3
|
28
aio/aio-builds-setup/docs/_TOC.md
Normal file
28
aio/aio-builds-setup/docs/_TOC.md
Normal file
@ -0,0 +1,28 @@
|
||||
# VM Setup Instructions
|
||||
|
||||
|
||||
## Overview
|
||||
- [General overview](overview--general.md)
|
||||
- [Security model](overview--security-model.md)
|
||||
- [Available Commands](overview--scripts-and-commands.md)
|
||||
|
||||
|
||||
## Setting up the VM
|
||||
- [Set up secrets](vm-setup--set-up-secrets.md)
|
||||
- [Set up docker](vm-setup--set-up-docker.md)
|
||||
- [Attach persistent disk](vm-setup--attach-persistent-disk.md)
|
||||
- [Create host directories and files](vm-setup--create-host-dirs-and-files.md)
|
||||
- [Create docker image](vm-setup--create-docker-image.md)
|
||||
|
||||
|
||||
## Configuring the docker image
|
||||
- [Available environment variables](image-config--environment-variables.md)
|
||||
|
||||
|
||||
## Starting the docker container
|
||||
- [Create docker image](vm-setup--start-docker-container.md)
|
||||
|
||||
|
||||
## Miscellaneous
|
||||
- [Debug docker container](misc--debug-docker-container.md)
|
||||
- [Integrate with CI](misc--integrate-with-ci.md)
|
@ -0,0 +1,52 @@
|
||||
# Image config - Environment variables
|
||||
|
||||
|
||||
Below is a list of environment variables that can be configured when creating the docker image (as
|
||||
described [here](vm-setup--create-docker-image.md)). An up-to-date list of the configurable
|
||||
environment variables and their default values can be found in the
|
||||
[Dockerfile](../dockerbuild/Dockerfile).
|
||||
|
||||
**Note:**
|
||||
Each variable has a `TEST_` prefixed counterpart, which is used for testing purposes. In most cases
|
||||
you don't need to specify values for those.
|
||||
|
||||
- `AIO_BUILDS_DIR`:
|
||||
The directory (inside the container) where the uploaded build artifacts are kept.
|
||||
|
||||
- `AIO_DOMAIN_NAME`:
|
||||
The domain name of the server.
|
||||
|
||||
- `AIO_GITHUB_ORGANIZATION`:
|
||||
The GitHub organization whose teams arew whitelisted for accepting uploads.
|
||||
See also `AIO_GITHUB_TEAM_SLUGS`.
|
||||
|
||||
- `AIO_GITHUB_TEAM_SLUGS`:
|
||||
A comma-separated list of teams, whose authors are allowed to upload PRs.
|
||||
See also `AIO_GITHUB_ORGANIZATION`.
|
||||
|
||||
- `AIO_NGINX_HOSTNAME`:
|
||||
The internal hostname for accessing the nginx server. This is mostly used for performing a
|
||||
periodic health-check.
|
||||
|
||||
- `AIO_NGINX_PORT_HTTP`:
|
||||
The port number on which nginx listens for HTTP connections. This should be mapped to the
|
||||
corresponding port on the host VM (as described [here](vm-setup--start-docker-container.md)).
|
||||
|
||||
- `AIO_NGINX_PORT_HTTPS`:
|
||||
The port number on which nginx listens for HTTPS connections. This should be mapped to the
|
||||
corresponding port on the host VM (as described [here](vm-setup--start-docker-container.md)).
|
||||
|
||||
- `AIO_REPO_SLUG`:
|
||||
The repository slug (in the form `<user>/<repo>`) for which PRs will be uploaded.
|
||||
|
||||
- `AIO_UPLOAD_HOSTNAME`:
|
||||
The internal hostname for accessing the Node.js upload-server. This is used by nginx for
|
||||
delegating upload requests and also for performing a periodic health-check.
|
||||
|
||||
- `AIO_UPLOAD_MAX_SIZE`:
|
||||
The maximum allowed size for the uploaded gzip archive containing the build artifacts. Files
|
||||
larger than this will be rejected.
|
||||
|
||||
- `AIO_UPLOAD_PORT`:
|
||||
The port number on which the Node.js upload-server listens for HTTP connections. This is used by
|
||||
nginx for delegating upload requests and also for performing a periodic health-check.
|
12
aio/aio-builds-setup/docs/misc--debug-docker-container.md
Normal file
12
aio/aio-builds-setup/docs/misc--debug-docker-container.md
Normal file
@ -0,0 +1,12 @@
|
||||
# Miscellaneous - Debug docker container
|
||||
|
||||
|
||||
TODO (gkalpak): Add docs. Mention:
|
||||
- `aio-health-check`
|
||||
- `aio-verify-setup`
|
||||
- Test nginx accessible at:
|
||||
- `http://$TEST_AIO_NGINX_HOTNAME:$TEST_AIO_NGINX_PORT_HTTP`
|
||||
- `https://$TEST_AIO_NGINX_HOTNAME:$TEST_AIO_NGINX_PORT_HTTPS`
|
||||
- Test upload-server accessible at:
|
||||
- `http://$TEST_AIO_UPLOAD_HOTNAME:$TEST_AIO_UPLOAD_PORT`
|
||||
- Local DNS (via dnsmasq) maps the above hostnames to 127.0.0.1
|
12
aio/aio-builds-setup/docs/misc--integrate-with-ci.md
Normal file
12
aio/aio-builds-setup/docs/misc--integrate-with-ci.md
Normal file
@ -0,0 +1,12 @@
|
||||
# Miscellaneous - Integrate with CI
|
||||
|
||||
|
||||
TODO (gkalpak): Add docs. Mention:
|
||||
- Travis' JWT addon (+ limitations).
|
||||
Relevant files: `.travis.yml`
|
||||
- Testing on CI.
|
||||
Relevant files: `ci/test-aio.sh`, `aio/aio-builds-setup/scripts/test.sh`
|
||||
- Preverifying on CI.
|
||||
Relevant files: `ci/deploy.sh`, `aio/aio-builds-setup/scripts/travis-preverify-pr.sh`
|
||||
- Deploying from CI.
|
||||
Relevant files: `ci/deploy.sh`, `aio/scripts/deploy-preview.sh`
|
84
aio/aio-builds-setup/docs/overview--general.md
Normal file
84
aio/aio-builds-setup/docs/overview--general.md
Normal file
@ -0,0 +1,84 @@
|
||||
# Overview - General
|
||||
|
||||
|
||||
## Objective
|
||||
Whenever a PR job is run on Travis, we want to build `angular.io` and upload the build artifacts to
|
||||
a publicly accessible server so that collaborators (developers, designers, authors, etc) can preview
|
||||
the changes without having to checkout and build the app locally.
|
||||
|
||||
|
||||
## Source code
|
||||
In order to make it easier to administer the server and version-control the setup, we are using
|
||||
[docker](https://www.docker.com) to run a container on a VM. The Dockerfile and all other files
|
||||
necessary for creating the docker container are stored (and versioned) along with the angular.io
|
||||
project's source code (currently part of the angular/angular repo) in the `aio-builds-setup/`
|
||||
directory.
|
||||
|
||||
|
||||
## Setup
|
||||
The VM is hosted on [Google Compute Engine](https://cloud.google.com/compute/). The host OS is
|
||||
debian:jessie. For more info how to set up the host VM take a look at the "Setting up the VM"
|
||||
section in [TOC](_TOC.md).
|
||||
|
||||
|
||||
## Security model
|
||||
Since we are managing a public server, it is important to take appropriate measures in order to
|
||||
prevent abuse. For more details on the challenges and the chosen approach take a look at the
|
||||
[security model](overview--security-model.md).
|
||||
|
||||
|
||||
## The 10000 feet view
|
||||
This section gives a brief summary of the several operations performed on CI and by the docker
|
||||
container:
|
||||
|
||||
|
||||
### On CI (Travis)
|
||||
- Build job completes successfully (i.e. build succeeds and tests pass).
|
||||
- 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 inside the angular.io project directory
|
||||
(currently `aio/`).
|
||||
- The CI script checks whether the author of the PR is a member of one of the whitelisted GitHub
|
||||
teams (and therefore allowed to upload).
|
||||
**Note:**
|
||||
For security reasons, the same checks will be performed on the server as well. This is an optional
|
||||
step with the purpose of:
|
||||
1. Avoiding the wasted overhead associated with uploads that are going to be rejected (e.g.
|
||||
building the artifacts, sending them to the server, running checks on the server, etc).
|
||||
2. Avoiding failing the build (due to an error response from the server) or requiring additional
|
||||
logic for detecting the reasons of the failure.
|
||||
- The CI script gzip and upload the build artifacts to the server.
|
||||
|
||||
More info on how to set things up on CI can be found [here](misc--integrate-with-ci.md).
|
||||
|
||||
|
||||
### Uploading build artifacts
|
||||
- nginx receives upload request.
|
||||
- nginx checks that the uploaded gzip archive does not exceed the specified max file size, stores it
|
||||
in a temporary location and passes the filepath to the Node.js upload-server.
|
||||
- The upload-server verifies that the uploaded file is not trying to overwrite an existing build,
|
||||
and runs several checks to determine whether the request should be accepted (more details can be
|
||||
found [here](overview--security-model.md)).
|
||||
- The upload-server deploys the artifacts to a sub-directory named after the PR number and SHA:
|
||||
`<PR>/<SHA>/`
|
||||
- The upload-server posts a comment on the corresponding PR on GitHub mentioning the SHA and the
|
||||
the link where the preview can be found.
|
||||
|
||||
|
||||
### Serving build artifacts
|
||||
- nginx receives a request for an uploaded resource on a subdomain corresponding to the PR and SHA.
|
||||
E.g.: `pr<PR>-<SHA>.ngbuilds.io/path/to/resurce`
|
||||
- nginx maps the subdomain to the correct sub-direcory and serves the resource.
|
||||
E.g.: `/<PR>/<SHA>/path/to/resource`
|
||||
|
||||
|
||||
### 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.
|
||||
|
||||
|
||||
### Health-check
|
||||
The docker service runs a periodic health-check that verifies the running conditions of the
|
||||
container. This includes verifying the status of specific system services, the responsiveness of
|
||||
nginx and the upload-server and internet connectivity.
|
55
aio/aio-builds-setup/docs/overview--scripts-and-commands.md
Normal file
55
aio/aio-builds-setup/docs/overview--scripts-and-commands.md
Normal file
@ -0,0 +1,55 @@
|
||||
# Overview - Scripts and Commands
|
||||
|
||||
|
||||
This is an overview of the available scripts and commands.
|
||||
|
||||
|
||||
## Scripts
|
||||
The scripts are located inside `<aio-builds-setup-dir>/scripts/`. The following scripts are
|
||||
available:
|
||||
|
||||
- `build.sh`:
|
||||
Can be used for creating a preconfigured docker image.
|
||||
See [here](vm-setup--create-docker-image.md) for more info.
|
||||
|
||||
- `test.sh`
|
||||
Can be used for running the tests for `<aio-builds-setup-dir>/dockerbuild/scripts-js/`. This is
|
||||
useful for CI integration. See [here](misc--integrate-with-ci.md) for more info.
|
||||
|
||||
- `travis-preverify-pr.sh`
|
||||
Can be used for "preverifying" a PR before uploading the artifacts to the server. It checks that
|
||||
the author of the PR a member of one of the specified GitHub teams and therefore allowed to upload
|
||||
build artifacts. This is useful for CI integration. See [here](misc--integrate-with-ci.md) for
|
||||
more info.
|
||||
|
||||
|
||||
## Commands
|
||||
The following commands are available globally from inside the docker container. They are either used
|
||||
by the container to perform its various operations or can be used ad-hoc, mainly for testing
|
||||
purposes. Each command is backed by a corresponding script inside
|
||||
`<aio-builds-setup-dir>/dockerbuild/scripts-sh/`.
|
||||
|
||||
- `aio-clean-up`:
|
||||
Cleans up the builds directory by removing the artifacts that do not correspond to an open PR.
|
||||
_It is run as a daily cronjob._
|
||||
|
||||
- `aio-health-check`:
|
||||
Runs a basic health-check, verifying that the necessary services are running, the servers are
|
||||
responding and there is a working internet connection.
|
||||
_It is used periodically by docker for determining the container's health status._
|
||||
|
||||
- `aio-init`:
|
||||
Initializes the container (mainly by starting the necessary services).
|
||||
_It is run (by default) when starting the container._
|
||||
|
||||
- `aio-upload-server-prod`:
|
||||
Spins up a Node.js upload-server instance.
|
||||
_It is used in `aio-init` (see above) during initialization._
|
||||
|
||||
- `aio-upload-server-test`:
|
||||
Spins up a Node.js upload-server instance for tests.
|
||||
_It is used in `aio-verify-setup` (see below) for running tests._
|
||||
|
||||
- `aio-verify-setup`:
|
||||
Runs a suite of e2e-like tests, mainly verifying the correct (inter)operation of nginx and the
|
||||
Node.js upload-server.
|
116
aio/aio-builds-setup/docs/overview--security-model.md
Normal file
116
aio/aio-builds-setup/docs/overview--security-model.md
Normal file
@ -0,0 +1,116 @@
|
||||
# Overview - Security model
|
||||
|
||||
|
||||
Whenever a PR job is run on Travis, we want to build `angular.io` and upload the build artifacts to
|
||||
a publicly accessible server so that collaborators (developers, designers, authors, etc) can preview
|
||||
the changes without having to checkout and build the app locally.
|
||||
|
||||
This document discusses the security considerations associated with uploading build artifacts as
|
||||
part of the CI setup and serving them publicly.
|
||||
|
||||
|
||||
## Security objectives
|
||||
|
||||
- **Prevent uploading arbitrary content to our servers.**
|
||||
Since there is no restriction on who can submit a PR, we cannot allow any PR's build artifacts to
|
||||
be uploaded.
|
||||
|
||||
- **Prevent overwriting other peoples uploaded content.**
|
||||
There needs to be a mechanism in place to ensure that the uploaded content does indeed correspond
|
||||
to the PR indicated by its URL.
|
||||
|
||||
- **Prevent arbitrary access on the server.**
|
||||
Since the PR author has full access over the build artifacts that would be uploaded, we must
|
||||
ensure that the uploaded files will not enable arbitrary access to the server or expose sensitive
|
||||
info.
|
||||
|
||||
|
||||
## Issues / Caveats
|
||||
|
||||
- Because the PR author can change the scripts run on CI, any security mechanisms must be immune to
|
||||
such changes.
|
||||
|
||||
- For security reasons, encrypted Travis variables are not available to PRs, so we can't rely on
|
||||
them to implement security.
|
||||
|
||||
|
||||
## Implemented approach
|
||||
|
||||
|
||||
### In a nutshell
|
||||
The implemented approach can be broken up to the following sub-tasks:
|
||||
|
||||
1. Verify which PR the uploaded artifacts correspond to.
|
||||
2. Determine the author of the PR.
|
||||
3. Check whether the PR author is a member of some whitelisted GitHub team.
|
||||
4. Deploy the artifacts to the corresponding PR's directory.
|
||||
5. Prevent overwriting previously deployed artifacts (which ensures that the guarantees established
|
||||
during deployment will remain valid until the artifacts are removed).
|
||||
6. Prevent uploaded files from accessing anything outside their directory.
|
||||
|
||||
|
||||
### Implementation details
|
||||
This section describes how each of the aforementioned sub-tasks is accomplished:
|
||||
|
||||
1. **Verify which PR the uploaded artifacts correspond to.**
|
||||
|
||||
We are taking advantage of Travis' [JWT addon](https://docs.travis-ci.com/user/jwt). By sharing
|
||||
a secret between Travis (which keeps it private but uses it to sign a JWT) and the server (which
|
||||
uses it to verify the authenticity of the JWT), we can accomplish the following:
|
||||
a. Verify that the upload request comes from Travis.
|
||||
b. Determine the PR that these artifacts correspond to (since Travis puts that information into
|
||||
the JWT, without the PR author being able to modify it).
|
||||
|
||||
_Note:_
|
||||
_There are currently certain limitation in the implementation of the JWT addon._
|
||||
_See the next section for more details._
|
||||
|
||||
2. **Determine the author of the PR.**
|
||||
|
||||
Once we have securely associated the uploaded artifaacts to a PR, we retrieve the PR's metadata -
|
||||
including the author's username - using the [GitHub API](https://developer.github.com/v3/).
|
||||
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 author is a member of some whitelisted GitHub team.**
|
||||
|
||||
Again using the GitHub API, we can verify the author's membership in one of the
|
||||
whitelisted/trusted GitHub teams. For this operation, we need a PErsonal Access Token with the
|
||||
`read:org` scope issued by a user that can "see" the specified GitHub organization.
|
||||
Here too, we use token by @mary-poppins.
|
||||
|
||||
4. **Deploy the artifacts to the corresponding PR's directory.**
|
||||
|
||||
With the preceeding steps, we have verified that the uploaded artifacts have been uploaded by
|
||||
Travis and correspond to a PR whose author is a member of a trusted team. 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 and Travis to the build artifacts.
|
||||
|
||||
5. **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 upload (currently a Node.js
|
||||
Express server) rejects uploads that target an existing directory.
|
||||
_Note: A PR can contain multiple uploads; one for each SHA that was built on Travis._
|
||||
|
||||
6. **Prevent uploaded files from accessing anything outside their directory.**
|
||||
|
||||
Nginx (which is used to serve the uploaded artifacts) has been configured to not follow symlinks
|
||||
outside of the directory where the build artifacts are stored.
|
||||
|
||||
|
||||
## Assumptions / Things to keep in mind
|
||||
|
||||
- Each trusted PR author has full control over the content that is uploaded for their PRs. Part of
|
||||
the security model relies on the trustworthiness of these authors.
|
||||
|
||||
- If anyone gets access to the `PREVIEW_DEPLOYMENT_TOKEN` (a.k.a. `NGBUILDS_IO_KEY` on
|
||||
angular/angular) variable generated for each Travis job, they will be able to impersonate the
|
||||
corresponding PR's author on the preview server for as long as the token is valid (currently 90
|
||||
mins). Because of this, the value of the `PREVIEW_DEPLOYMENT_TOKEN` should not be made publicly
|
||||
accessible (e.g. by printing it on the Travis job log).
|
||||
|
||||
- Travis does only allow specific whitelisted property names to be used with the JWT addon. The only
|
||||
known such property at the time is `SAUCE_ACCESS_KEY` (used for integration with SauceLabs). In
|
||||
order to be able to actually use the JWT addon we had to name the encrypted variable
|
||||
`SAUCE_ACCESS_KEY` (which we later re-assign to `NGBUILDS_IO_KEY`).
|
@ -0,0 +1,20 @@
|
||||
# VM setup - Attach persistent disk
|
||||
|
||||
|
||||
## Create `aio-builds` persistent disk (if not already exists)
|
||||
- Follow instructions [here](https://cloud.google.com/compute/docs/disks/add-persistent-disk#create_disk).
|
||||
- `sudo mkfs.ext4 -F -E lazy_itable_init=0,lazy_journal_init=0,discard /dev/disk/by-id/google-aio-builds`
|
||||
|
||||
|
||||
## Mount disk
|
||||
- `sudo mkdir -p /mnt/disks/aio-builds`
|
||||
- `sudo mount -o discard,defaults /dev/disk/by-id/google-aio-builds /mnt/disks/aio-builds`
|
||||
- `sudo chmod a+w /mnt/disks/aio-builds`
|
||||
|
||||
|
||||
## Mount disk on boot
|
||||
- Run:
|
||||
```
|
||||
echo UUID=`sudo blkid -s UUID -o value /dev/disk/by-id/google-aio-builds` \
|
||||
/mnt/disks/aio-builds ext4 discard,defaults,nofail 0 2 | sudo tee -a /etc/fstab
|
||||
```
|
32
aio/aio-builds-setup/docs/vm-setup--create-docker-image.md
Normal file
32
aio/aio-builds-setup/docs/vm-setup--create-docker-image.md
Normal file
@ -0,0 +1,32 @@
|
||||
# VM setup - Create docker image
|
||||
|
||||
|
||||
## Checkout repository
|
||||
- `git clone <repo-url>`
|
||||
|
||||
|
||||
## Build docker image
|
||||
- `<aio-builds-setup-dir>/scripts/build.sh [<name>[:<tag>] [--build-arg <NAME>=<value> ...]]`
|
||||
- You can overwrite the default environment variables inside the image, by passing new values using
|
||||
`--build-arg`.
|
||||
|
||||
**Note:** The build script has to execute docker commands with `sudo`.
|
||||
|
||||
|
||||
## Example
|
||||
The following commands would create a docker image from GitHub repo `foo/bar` to be deployed on the
|
||||
`foobar-builds.io` domain and accepting PR deployments from authors that are members of the
|
||||
`bar-core` and `bar-docs-authors` teams of organization `foo`:
|
||||
|
||||
- `git clone https://github.com/foo/bar.git foobar`
|
||||
- Run:
|
||||
```
|
||||
./foobar/aio-builds-setup/scripts/build.sh foobar-builds \
|
||||
--build-arg AIO_REPO_SLUG=foo/bar \
|
||||
--build-arg AIO_DOMAIN_NAME=foobar-builds.io \
|
||||
--build-arg AIO_GITHUB_ORGANIZATION=foo \
|
||||
--build-arg AIO_GITHUB_TEMA_SLUGS=bar-core,bar-docs-authors
|
||||
```
|
||||
|
||||
A full list of the available environment variables can be found
|
||||
[here](image-config--environment-variables.md).
|
@ -0,0 +1,74 @@
|
||||
# VM setup - Create host directories and files
|
||||
|
||||
|
||||
## Create directory with secrets
|
||||
For security reasons, sensitive info (such as tokens and passwords) are not hardcoded into the
|
||||
docker image, nor passed as environment variables at runtime. They are passed to the docker
|
||||
container from the host VM as files inside a directory. Each file's name is the name of the variable
|
||||
and the file content is the value. These are read from inside the running container when necessary.
|
||||
|
||||
More info on how to create `secrets` directory and files can be found
|
||||
[here](vm-setup--set-up-secrets.md).
|
||||
|
||||
|
||||
## Create directory for build artifacts
|
||||
The uploaded build artifacts should be kept on a directory outside the docker container, so it is
|
||||
easier to replace the container without losing the uploaded builds. For portability across VMs a
|
||||
persistent disk can be used (as described [here](vm-setup--attach-persistent-disk.md)).
|
||||
|
||||
**Note:** The directories created inside that directory will be owned by user `www-data`.
|
||||
|
||||
|
||||
## Create SSL certificates (Optional for dev)
|
||||
The host VM can attach a directory containing the SSL certificate and key to be used by the nginx
|
||||
server for serving the uploaded build artifacts. More info on how to attach the directory when
|
||||
starting the container can be found [here](vm-setup--start-docker-container.md).
|
||||
|
||||
In order for the container to be able to find the certificate and key, they should be named
|
||||
`<DOMAIN_NAME>.crt` and `<DOMAIN_NAME>.key` respectively. For example, for a domain name
|
||||
`ngbuild.io`, nginx will look for files `ngbuilds.io.crt` and `ngbuilds.io.key`. More info on how to
|
||||
specify the domain name see [here](vm-setup--create-docker-image.md).
|
||||
|
||||
If no directory is attached, nginx will use an internal self-signed certificate. This is convenient
|
||||
during development, but is not suitable for production.
|
||||
|
||||
**Note:**
|
||||
Since nginx needs to be able to serve requests for both the main domain as well as any subdomain
|
||||
(e.g. `ngbuilds.io/` and `foo-bar.ngbuilds.io/`), the provided certificate needs to be a wildcard
|
||||
certificate covering both the domain and subdomains.
|
||||
|
||||
|
||||
## Create directory for logs (Optional)
|
||||
Optionally, a logs directory can pe passed to the docker container for storing non-system-related
|
||||
logs. If not provided, the logs are kept locally on the container and will be lost whenever the
|
||||
container is replaced (e.g. when updating to use a newer version of the docker image).
|
||||
|
||||
The following files log files are kept in this directory:
|
||||
|
||||
- `clean-up.log`:
|
||||
Output of the `aio-clean-up` command, run as a cronjob for cleaning up the build artifacts of
|
||||
closed PRs.
|
||||
|
||||
- `init.log`:
|
||||
Output of the `aio-init` command, run (by default) when starting the container.
|
||||
|
||||
- `nginx/{access,error}.log`:
|
||||
The access and error logs produced by the nginx server while serving "production" files.
|
||||
|
||||
- `nginx-test/{access,error}.log`:
|
||||
The access and error logs produced by the nginx server while serving "test" files. This is only
|
||||
used when running tests locally from inside the container, e.g. with the `aio-verify-setup`
|
||||
command. (See [here](overview--scripts-and-commands.md) for more info.)
|
||||
|
||||
- `upload-server-{prod,test,verify-setup}-*.log`:
|
||||
The logs produced by the Node.js upload-server while serving either:
|
||||
- `-prod`: "Production" files (g.g during normal operation).
|
||||
- `-test`: "Test" files (e.g. when a test instance is started with the `aio-upload-server-test`
|
||||
command).
|
||||
- `-verify-setup`: "Test" files, but while running `aio-verify-setup`.
|
||||
|
||||
(See [here](overview--scripts-and-commands.md) for more info the commands mentioned above.)
|
||||
|
||||
- `verify-setup.log`:
|
||||
The output of the `aio-verify-setup` command (e.g. Jasmine output), except for upload-server
|
||||
output which is logged to `upload-server-verify-setup-*.log` (see above).
|
35
aio/aio-builds-setup/docs/vm-setup--set-up-docker.md
Normal file
35
aio/aio-builds-setup/docs/vm-setup--set-up-docker.md
Normal file
@ -0,0 +1,35 @@
|
||||
# VM Setup - Set up docker
|
||||
|
||||
|
||||
## Install docker
|
||||
|
||||
_Debian (jessie):_
|
||||
- `sudo apt-get update`
|
||||
- `sudo apt-get install -y apt-transport-https ca-certificates curl git software-properties-common`
|
||||
- `curl -fsSL https://apt.dockerproject.org/gpg | sudo apt-key add -`
|
||||
- `apt-key fingerprint 58118E89F3A912897C070ADBF76221572C52609D`
|
||||
- `sudo add-apt-repository "deb https://apt.dockerproject.org/repo/ debian-$(lsb_release -cs) main"`
|
||||
- `sudo apt-get update`
|
||||
- `sudo apt-get -y install docker-engine`
|
||||
|
||||
_Ubuntu (16.04):_
|
||||
- `sudo apt-get update`
|
||||
- `sudo apt-get install -y curl git linux-image-extra-$(uname -r) linux-image-extra-virtual`
|
||||
- `sudo apt-get install -y apt-transport-https ca-certificates`
|
||||
- `curl -fsSL https://yum.dockerproject.org/gpg | sudo apt-key add -`
|
||||
- `apt-key fingerprint 58118E89F3A912897C070ADBF76221572C52609D`
|
||||
- `sudo add-apt-repository "deb https://apt.dockerproject.org/repo/ ubuntu-$(lsb_release -cs) main"`
|
||||
- `sudo apt-get update`
|
||||
- `sudo apt-get -y install docker-engine`
|
||||
|
||||
|
||||
## Start the docker
|
||||
- `sudo service docker start`
|
||||
|
||||
|
||||
## Test docker
|
||||
- `sudo docker run hello-world`
|
||||
|
||||
|
||||
## Start docker on boot
|
||||
- `sudo systemctl enable docker`
|
52
aio/aio-builds-setup/docs/vm-setup--set-up-secrets.md
Normal file
52
aio/aio-builds-setup/docs/vm-setup--set-up-secrets.md
Normal file
@ -0,0 +1,52 @@
|
||||
# VM Setup - Set up secrets
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
Necessary secrets:
|
||||
|
||||
1. `GITHUB_TOKEN`
|
||||
- Used for:
|
||||
- Retrieving open PRs without rate-limiting.
|
||||
- Retrieving PR author.
|
||||
- Retrieving members of the `angular-core` team.
|
||||
- Posting comments with preview links on PRs.
|
||||
|
||||
2. `PREVIEW_DEPLOYMENT_TOKEN`
|
||||
- Used for:
|
||||
- Decoding the JWT tokens received with `/create-build` requests.
|
||||
|
||||
**Note:**
|
||||
`TEST_GITHUB_TOKEN` and `TEST_PREVIEW_DEPLOYMENT_TOKEN` can also be created similar to their
|
||||
non-TEST counterparts and they will be loaded when running `aio-verify-setup`, but it is currently
|
||||
not clear if/how they can be used in tests.
|
||||
|
||||
|
||||
## Create secrets
|
||||
|
||||
1. `GITHUB_TOKEN`
|
||||
- Visit https://github.com/settings/tokens.
|
||||
- Generate new token with the `public_repo` scope.
|
||||
|
||||
2. `PREVIEW_DEPLOYMENT_TOKEN`
|
||||
- Just generate a hard-to-guess character sequence.
|
||||
- Add it to `.travis.yml` under `addons -> jwt -> secure`.
|
||||
Can be added automatically with: `travis encrypt --add addons.jwt PREVIEW_DEPLOYMENT_TOKEN=<access-key>`
|
||||
|
||||
**Note:**
|
||||
Due to [travis-ci/travis-ci#7223](https://github.com/travis-ci/travis-ci/issues/7223) it is not
|
||||
currently possible to use the JWT addon (as described above) for anything other than the
|
||||
`SAUCE_ACCESS_KEY` variable. You can get creative, though...
|
||||
|
||||
**WARNING**
|
||||
TO avoid arbitrary uploads, make sure the `PREVIEW_DEPLOYMENT_TOKEN` is NOT printed in the Travis log.
|
||||
|
||||
|
||||
## Save secrets on the VM
|
||||
|
||||
- `sudo mkdir /aio-secrets`
|
||||
- `sudo touch /aio-secrets/GITHUB_TOKEN`
|
||||
- Insert `<github-token>` into `/aio-secrets/GITHUB_TOKEN`.
|
||||
- `sudo touch /aio-secrets/PREVIEW_DEPLOYMENT_TOKEN`
|
||||
- Insert `<access-token>` into `/aio-secrets/PREVIEW_DEPLOYMENT_TOKEN`.
|
||||
- `sudo chmod 400 /aio-secrets/*`
|
@ -0,0 +1,92 @@
|
||||
# VM setup - Start docker container
|
||||
|
||||
|
||||
## The `docker run` command
|
||||
Once everything has been setup and configured, a docker container can be started with the following
|
||||
command:
|
||||
|
||||
```
|
||||
sudo docker run \
|
||||
-d \
|
||||
--dns 127.0.0.1 \
|
||||
--name <instance-name> \
|
||||
-p 80:80 \
|
||||
-p 443:443 \
|
||||
--restart unless-stopped \
|
||||
[-v <host-cert-dir>:/etc/ssl/localcerts:ro] \
|
||||
-v <host-secrets-dir>:/aio-secrets:ro \
|
||||
-v <host-builds-dir>:/var/www/aio-builds \
|
||||
[-v <host-logs-dir>:/var/log/aio] \
|
||||
<name>[:<tag>]
|
||||
```
|
||||
|
||||
Below is the same command with inline comments explaining each option. The aPI docs for `docker run`
|
||||
can be found [here](https://docs.docker.com/engine/reference/run/).
|
||||
|
||||
```
|
||||
sudo docker run \
|
||||
|
||||
# Start as a daemon.
|
||||
-d \
|
||||
|
||||
# Use the local DNS server.
|
||||
# (This is necessary for mapping internal URLs, e.g. for the Node.js upload-server.)
|
||||
--dns 127.0.0.1 \
|
||||
|
||||
# USe `<instance-name>` as an alias for the container.
|
||||
# Useful for running `docker` commands, e.g.: `docker stop <instance-name>`
|
||||
--name <instance-name> \
|
||||
|
||||
# Map ports of the hosr VM (left) to ports of the docker container (right)
|
||||
-p 80:80 \
|
||||
-p 443:443 \
|
||||
|
||||
# Automatically restart the container (unless it was explicitly stopped by the user).
|
||||
# (This ensures that the container will be automatically started on boot.)
|
||||
--restart unless-stopped \
|
||||
|
||||
# The directory the contains the SSL certificates.
|
||||
# (See [here](vm-setup--create-host-dirs-and-files.md) for more info.)
|
||||
# If not provided, the container will use self-signed certificates.
|
||||
[-v <host-cert-dir>:/etc/ssl/localcerts:ro] \
|
||||
|
||||
# The directory the contains the secrets (e.g. GitHub token, JWT secret, etc).
|
||||
# (See [here](vm-setup--set-up-secrets.md) for more info.)
|
||||
-v <host-secrets-dir>:/aio-secrets:ro \
|
||||
|
||||
# The uploaded build artifacts will stored to and served from this directory.
|
||||
# (If you are using a persistent disk - as described [here](vm-setup--attach-persistent-disk.md) -
|
||||
# this will be a directory inside the disk.)
|
||||
-v <host-builds-dir>:/var/www/aio-builds \
|
||||
|
||||
# The directory where the logs are being kept.
|
||||
# (See [here](vm-setup--create-host-dirs-and-files.md) for more info.)
|
||||
# If not provided, the logs will be kept inside the container, which means they will be lost
|
||||
# whenever a new container is created.
|
||||
[-v <host-logs-dir>:/var/log/aio] \
|
||||
|
||||
# The name of the docker image to use (and an optional tag; defaults to `latest`).
|
||||
# (See [here](vm-setup--create-docker-image.md) for instructions on how to create the iamge.)
|
||||
<name>[:<tag>]
|
||||
```
|
||||
|
||||
|
||||
## Example
|
||||
The following command would start a docker container based on the previously created `foobar-builds`
|
||||
docker image, alias it as 'foobar-builds-1' and map predefined directories on the host VM to be used
|
||||
by the container for accesing secrets and SSL certificates and keeping the build artifacts and logs.
|
||||
|
||||
```
|
||||
sudo docker run \
|
||||
-d \
|
||||
--dns 127.0.0.1 \
|
||||
--name foobar-builds-1 \
|
||||
-p 80:80 \
|
||||
-p 443:443 \
|
||||
--restart unless-stopped \
|
||||
-v /etc/ssl/localcerts:/etc/ssl/localcerts:ro \
|
||||
-v /foobar-secrets:/aio-secrets:ro \
|
||||
-v /mnt/disks/foobar-builds:/var/www/aio-builds \
|
||||
-v /foobar-logs:/var/log/aio \
|
||||
foobar-builds
|
||||
```
|
16
aio/aio-builds-setup/scripts/build.sh
Executable file
16
aio/aio-builds-setup/scripts/build.sh
Executable file
@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
set -eux -o pipefail
|
||||
|
||||
# Set up env
|
||||
source "`dirname $0`/env.sh"
|
||||
readonly defaultImageNameAndTag="aio-builds:latest"
|
||||
|
||||
# Build `scripts-js/`
|
||||
cd "$SCRIPTS_JS_DIR"
|
||||
yarn install
|
||||
yarn run build
|
||||
cd -
|
||||
|
||||
# Create docker image
|
||||
readonly nameAndOptionalTag=${1:-$defaultImageNameAndTag}
|
||||
sudo docker build --tag $nameAndOptionalTag ${@:2} $DOCKERBUILD_DIR
|
5
aio/aio-builds-setup/scripts/env.sh
Executable file
5
aio/aio-builds-setup/scripts/env.sh
Executable file
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
readonly THIS_DIR=$(cd $(dirname $0); pwd)
|
||||
readonly DOCKERBUILD_DIR="$THIS_DIR/../dockerbuild"
|
||||
readonly SCRIPTS_JS_DIR="$DOCKERBUILD_DIR/scripts-js"
|
11
aio/aio-builds-setup/scripts/test.sh
Executable file
11
aio/aio-builds-setup/scripts/test.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
set -eux -o pipefail
|
||||
|
||||
# Set up env
|
||||
source "`dirname $0`/env.sh"
|
||||
|
||||
# Test `scripts-js/`
|
||||
cd "$SCRIPTS_JS_DIR"
|
||||
yarn install
|
||||
yarn test
|
||||
cd -
|
13
aio/aio-builds-setup/scripts/travis-preverify-pr.sh
Executable file
13
aio/aio-builds-setup/scripts/travis-preverify-pr.sh
Executable file
@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
set -eux -o pipefail
|
||||
|
||||
# Set up env
|
||||
source "`dirname $0`/env.sh"
|
||||
|
||||
# Preverify PR
|
||||
AIO_GITHUB_ORGANIZATION="angular" \
|
||||
AIO_GITHUB_TEAM_SLUGS="angular-core,aio-contributors" \
|
||||
AIO_GITHUB_TOKEN=$(echo ${GITHUB_TEAM_MEMBERSHIP_CHECK_KEY} | rev) \
|
||||
AIO_REPO_SLUG=$TRAVIS_REPO_SLUG \
|
||||
AIO_PREVERIFY_PR=$TRAVIS_PULL_REQUEST \
|
||||
node "$SCRIPTS_JS_DIR/dist/lib/upload-server/index-preverify-pr"
|
@ -1,3 +0,0 @@
|
||||
module.exports = (gulp) => () => {
|
||||
// TODO:(petebd): hook up with whatever builds need doing for the webapp
|
||||
};
|
@ -1,24 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
generate: (gulp) => () => {
|
||||
const path = require('path');
|
||||
const Dgeni = require('dgeni');
|
||||
const angularDocsPackage = require(path.resolve(__dirname, '../transforms/angular.io-package'));
|
||||
const dgeni = new Dgeni([angularDocsPackage]);
|
||||
return dgeni.generate();
|
||||
},
|
||||
|
||||
test: (gulp) => () => {
|
||||
const execSync = require('child_process').execSync;
|
||||
execSync(
|
||||
'node ../dist/tools/cjs-jasmine/index-tools ../../transforms/**/*.spec.js',
|
||||
{stdio: ['inherit', 'inherit', 'inherit']});
|
||||
}
|
||||
};
|
@ -1,18 +0,0 @@
|
||||
@cheatsheetSection
|
||||
Bootstrapping
|
||||
@cheatsheetIndex 0
|
||||
@description
|
||||
{@target ts}`import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';`{@endtarget}
|
||||
{@target js}Available from the `ng.platformBrowserDynamic` namespace{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`platformBrowserDynamic().bootstrapModule(AppModule);`|`platformBrowserDynamic().bootstrapModule`
|
||||
syntax(js):
|
||||
`document.addEventListener('DOMContentLoaded', function() {
|
||||
ng.platformBrowserDynamic
|
||||
.platformBrowserDynamic()
|
||||
.bootstrapModule(app.AppModule);
|
||||
});`|`platformBrowserDynamic().bootstrapModule`
|
||||
description:
|
||||
Bootstraps the app, using the root component from the specified `NgModule`. {@target js}Must be wrapped in the event listener to fire when the page loads.{@endtarget}
|
@ -1,34 +0,0 @@
|
||||
@cheatsheetSection
|
||||
Built-in directives
|
||||
@cheatsheetIndex 3
|
||||
@description
|
||||
{@target ts}`import { CommonModule } from '@angular/common';`{@endtarget}
|
||||
{@target js}Available using the `ng.common.CommonModule` module{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<section *ngIf="showSection">`|`*ngIf`
|
||||
description:
|
||||
Removes or recreates a portion of the DOM tree based on the `showSection` expression.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<li *ngFor="let item of list">`|`*ngFor`
|
||||
description:
|
||||
Turns the li element and its contents into a template, and uses that to instantiate a view for each item in list.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<div [ngSwitch]="conditionExpression">
|
||||
<ng-template [ngSwitchCase]="case1Exp">...</ng-template>
|
||||
<ng-template ngSwitchCase="case2LiteralString">...</ng-template>
|
||||
<ng-template ngSwitchDefault>...</ng-template>
|
||||
</div>`|`[ngSwitch]`|`[ngSwitchCase]`|`ngSwitchCase`|`ngSwitchDefault`
|
||||
description:
|
||||
Conditionally swaps the contents of the div by selecting one of the embedded templates based on the current value of `conditionExpression`.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<div [ngClass]="{active: isActive, disabled: isDisabled}">`|`[ngClass]`
|
||||
description:
|
||||
Binds the presence of CSS classes on the element to the truthiness of the associated map values. The right-hand expression should return {class-name: true/false} map.
|
@ -1,49 +0,0 @@
|
||||
@cheatsheetSection
|
||||
Class decorators
|
||||
@cheatsheetIndex 5
|
||||
@description
|
||||
{@target ts}`import { Directive, ... } from '@angular/core';`{@endtarget}
|
||||
{@target js}Available from the `ng.core` namespace{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@Component({...})
|
||||
class MyComponent() {}`|`@Component({...})`
|
||||
syntax(js):
|
||||
`var MyComponent = ng.core.Component({...}).Class({...})`|`ng.core.Component({...})`
|
||||
description:
|
||||
Declares that a class is a component and provides metadata about the component.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@Directive({...})
|
||||
class MyDirective() {}`|`@Directive({...})`
|
||||
syntax(js):
|
||||
`var MyDirective = ng.core.Directive({...}).Class({...})`|`ng.core.Directive({...})`
|
||||
description:
|
||||
Declares that a class is a directive and provides metadata about the directive.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@Pipe({...})
|
||||
class MyPipe() {}`|`@Pipe({...})`
|
||||
syntax(js):
|
||||
`var MyPipe = ng.core.Pipe({...}).Class({...})`|`ng.core.Pipe({...})`
|
||||
description:
|
||||
Declares that a class is a pipe and provides metadata about the pipe.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@Injectable()
|
||||
class MyService() {}`|`@Injectable()`
|
||||
syntax(js):
|
||||
`var OtherService = ng.core.Class(
|
||||
{constructor: function() { }});
|
||||
var MyService = ng.core.Class(
|
||||
{constructor: [OtherService, function(otherService) { }]});`|`var MyService = ng.core.Class({constructor: [OtherService, function(otherService) { }]});`
|
||||
description:
|
||||
{@target ts}Declares that a class has dependencies that should be injected into the constructor when the dependency injector is creating an instance of this class.
|
||||
{@endtarget}
|
||||
{@target js}
|
||||
Declares a service to inject into a class by providing an array with the services, with the final item being the function to receive the injected services.
|
||||
{@endtarget}
|
@ -1,38 +0,0 @@
|
||||
@cheatsheetSection
|
||||
Component configuration
|
||||
@cheatsheetIndex 7
|
||||
@description
|
||||
{@target js}`ng.core.Component` extends `ng.core.Directive`,
|
||||
so the `ng.core.Directive` configuration applies to components as well{@endtarget}
|
||||
{@target ts}`@Component` extends `@Directive`,
|
||||
so the `@Directive` configuration applies to components as well{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`moduleId: module.id`|`moduleId:`
|
||||
description:
|
||||
If set, the `templateUrl` and `styleUrl` are resolved relative to the component.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`viewProviders: [MyService, { provide: ... }]`|`viewProviders:`
|
||||
syntax(js):
|
||||
`viewProviders: [MyService, { provide: ... }]`|`viewProviders:`
|
||||
description:
|
||||
List of dependency injection providers scoped to this component's view.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`template: 'Hello {{name}}'
|
||||
templateUrl: 'my-component.html'`|`template:`|`templateUrl:`
|
||||
description:
|
||||
Inline template or external template URL of the component's view.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`styles: ['.primary {color: red}']
|
||||
styleUrls: ['my-component.css']`|`styles:`|`styleUrls:`
|
||||
description:
|
||||
List of inline CSS styles or external stylesheet URLs for styling the component’s view.
|
@ -1,30 +0,0 @@
|
||||
@cheatsheetSection
|
||||
Dependency injection configuration
|
||||
@cheatsheetIndex 10
|
||||
@description
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`{ provide: MyService, useClass: MyMockService }`|`provide`|`useClass`
|
||||
syntax(js):
|
||||
`{ provide: MyService, useClass: MyMockService }`|`provide`|`useClass`
|
||||
description:
|
||||
Sets or overrides the provider for `MyService` to the `MyMockService` class.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`{ provide: MyService, useFactory: myFactory }`|`provide`|`useFactory`
|
||||
syntax(js):
|
||||
`{ provide: MyService, useFactory: myFactory }`|`provide`|`useFactory`
|
||||
description:
|
||||
Sets or overrides the provider for `MyService` to the `myFactory` factory function.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`{ provide: MyValue, useValue: 41 }`|`provide`|`useValue`
|
||||
syntax(js):
|
||||
`{ provide: MyValue, useValue: 41 }`|`provide`|`useValue`
|
||||
description:
|
||||
Sets or overrides the provider for `MyValue` to the value `41`.
|
@ -1,86 +0,0 @@
|
||||
@cheatsheetSection
|
||||
Class field decorators for directives and components
|
||||
@cheatsheetIndex 8
|
||||
@description
|
||||
{@target ts}`import { Input, ... } from '@angular/core';`{@endtarget}
|
||||
{@target js}Available from the `ng.core` namespace{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@Input() myProperty;`|`@Input()`
|
||||
syntax(js):
|
||||
`ng.core.Input(myProperty, myComponent);`|`ng.core.Input(`|`);`
|
||||
description:
|
||||
Declares an input property that you can update via property binding (example:
|
||||
`<my-cmp [myProperty]="someExpression">`).
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@Output() myEvent = new EventEmitter();`|`@Output()`
|
||||
syntax(js):
|
||||
`myEvent = new ng.core.EventEmitter();
|
||||
ng.core.Output(myEvent, myComponent);`|`ng.core.Output(`|`);`
|
||||
description:
|
||||
Declares an output property that fires events that you can subscribe to with an event binding (example: `<my-cmp (myEvent)="doSomething()">`).
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@HostBinding('class.valid') isValid;`|`@HostBinding('class.valid')`
|
||||
syntax(js):
|
||||
`ng.core.HostBinding('class.valid',
|
||||
'isValid', myComponent);`|`ng.core.HostBinding('class.valid', 'isValid'`|`);`
|
||||
description:
|
||||
Binds a host element property (here, the CSS class `valid`) to a directive/component property (`isValid`).
|
||||
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@HostListener('click', ['$event']) onClick(e) {...}`|`@HostListener('click', ['$event'])`
|
||||
syntax(js):
|
||||
`ng.core.HostListener('click',
|
||||
['$event'], onClick(e) {...}, myComponent);`|`ng.core.HostListener('click', ['$event'], onClick(e)`|`);`
|
||||
description:
|
||||
Subscribes to a host element event (`click`) with a directive/component method (`onClick`), optionally passing an argument (`$event`).
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@ContentChild(myPredicate) myChildComponent;`|`@ContentChild(myPredicate)`
|
||||
syntax(js):
|
||||
`ng.core.ContentChild(myPredicate,
|
||||
'myChildComponent', myComponent);`|`ng.core.ContentChild(myPredicate,`|`);`
|
||||
description:
|
||||
Binds the first result of the component content query (`myPredicate`) to a property (`myChildComponent`) of the class.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@ContentChildren(myPredicate) myChildComponents;`|`@ContentChildren(myPredicate)`
|
||||
syntax(js):
|
||||
`ng.core.ContentChildren(myPredicate,
|
||||
'myChildComponents', myComponent);`|`ng.core.ContentChildren(myPredicate,`|`);`
|
||||
description:
|
||||
Binds the results of the component content query (`myPredicate`) to a property (`myChildComponents`) of the class.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@ViewChild(myPredicate) myChildComponent;`|`@ViewChild(myPredicate)`
|
||||
syntax(js):
|
||||
`ng.core.ViewChild(myPredicate,
|
||||
'myChildComponent', myComponent);`|`ng.core.ViewChild(myPredicate,`|`);`
|
||||
description:
|
||||
Binds the first result of the component view query (`myPredicate`) to a property (`myChildComponent`) of the class. Not available for directives.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@ViewChildren(myPredicate) myChildComponents;`|`@ViewChildren(myPredicate)`
|
||||
syntax(js):
|
||||
`ng.core.ViewChildren(myPredicate,
|
||||
'myChildComponents', myComponent);`|`ng.core.ViewChildren(myPredicate,`|`);`
|
||||
description:
|
||||
Binds the results of the component view query (`myPredicate`) to a property (`myChildComponents`) of the class. Not available for directives.
|
@ -1,23 +0,0 @@
|
||||
@cheatsheetSection
|
||||
Directive configuration
|
||||
@cheatsheetIndex 6
|
||||
@description
|
||||
{@target ts}`@Directive({ property1: value1, ... })`{@endtarget}
|
||||
{@target js}`ng.core.Directive({ property1: value1, ... }).Class({...})`{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`selector: '.cool-button:not(a)'`|`selector:`
|
||||
description:
|
||||
Specifies a CSS selector that identifies this directive within a template. Supported selectors include `element`,
|
||||
`[attribute]`, `.class`, and `:not()`.
|
||||
|
||||
Does not support parent-child relationship selectors.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`providers: [MyService, { provide: ... }]`|`providers:`
|
||||
syntax(js):
|
||||
`providers: [MyService, { provide: ... }]`|`providers:`
|
||||
description:
|
||||
List of dependency injection providers for this directive and its children.
|
@ -1,12 +0,0 @@
|
||||
@cheatsheetSection
|
||||
Forms
|
||||
@cheatsheetIndex 4
|
||||
@description
|
||||
{@target ts}`import { FormsModule } from '@angular/forms';`{@endtarget}
|
||||
{@target js}Available using the `ng.forms.FormsModule` module{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<input [(ngModel)]="userName">`|`[(ngModel)]`
|
||||
description:
|
||||
Provides two-way data-binding, parsing, and validation for form controls.
|
@ -1,86 +0,0 @@
|
||||
@cheatsheetSection
|
||||
Directive and component change detection and lifecycle hooks
|
||||
@cheatsheetIndex 9
|
||||
@description
|
||||
{@target ts}(implemented as class methods){@endtarget}
|
||||
{@target js}(implemented as component properties){@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`constructor(myService: MyService, ...) { ... }`|`constructor(myService: MyService, ...)`
|
||||
syntax(js):
|
||||
`constructor: function(MyService, ...) { ... }`|`constructor: function(MyService, ...)`
|
||||
description:
|
||||
Called before any other lifecycle hook. Use it to inject dependencies, but avoid any serious work here.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngOnChanges(changeRecord) { ... }`|`ngOnChanges(changeRecord)`
|
||||
syntax(js):
|
||||
`ngOnChanges: function(changeRecord) { ... }`|`ngOnChanges: function(changeRecord)`
|
||||
description:
|
||||
Called after every change to input properties and before processing content or child views.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngOnInit() { ... }`|`ngOnInit()`
|
||||
syntax(js):
|
||||
`ngOnInit: function() { ... }`|`ngOnInit: function()`
|
||||
description:
|
||||
Called after the constructor, initializing input properties, and the first call to `ngOnChanges`.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngDoCheck() { ... }`|`ngDoCheck()`
|
||||
syntax(js):
|
||||
`ngDoCheck: function() { ... }`|`ngDoCheck: function()`
|
||||
description:
|
||||
Called every time that the input properties of a component or a directive are checked. Use it to extend change detection by performing a custom check.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngAfterContentInit() { ... }`|`ngAfterContentInit()`
|
||||
syntax(js):
|
||||
`ngAfterContentInit: function() { ... }`|`ngAfterContentInit: function()`
|
||||
description:
|
||||
Called after `ngOnInit` when the component's or directive's content has been initialized.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngAfterContentChecked() { ... }`|`ngAfterContentChecked()`
|
||||
syntax(js):
|
||||
`ngAfterContentChecked: function() { ... }`|`ngAfterContentChecked: function()`
|
||||
description:
|
||||
Called after every check of the component's or directive's content.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngAfterViewInit() { ... }`|`ngAfterViewInit()`
|
||||
syntax(js):
|
||||
`ngAfterViewInit: function() { ... }`|`ngAfterViewInit: function()`
|
||||
description:
|
||||
Called after `ngAfterContentInit` when the component's view has been initialized. Applies to components only.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngAfterViewChecked() { ... }`|`ngAfterViewChecked()`
|
||||
syntax(js):
|
||||
`ngAfterViewChecked: function() { ... }`|`ngAfterViewChecked: function()`
|
||||
description:
|
||||
Called after every check of the component's view. Applies to components only.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`ngOnDestroy() { ... }`|`ngOnDestroy()`
|
||||
syntax(js):
|
||||
`ngOnDestroy: function() { ... }`|`ngOnDestroy: function()`
|
||||
description:
|
||||
Called once, before the instance is destroyed.
|
@ -1,58 +0,0 @@
|
||||
@cheatsheetSection
|
||||
NgModules
|
||||
@cheatsheetIndex 1
|
||||
@description
|
||||
{@target ts}`import { NgModule } from '@angular/core';`{@endtarget}
|
||||
{@target js}Available from the `ng.core` namespace{@endtarget}
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`@NgModule({ declarations: ..., imports: ...,
|
||||
exports: ..., providers: ..., bootstrap: ...})
|
||||
class MyModule {}`|`NgModule`
|
||||
description:
|
||||
Defines a module that contains components, directives, pipes, and providers.
|
||||
|
||||
syntax(js):
|
||||
`ng.core.NgModule({declarations: ..., imports: ...,
|
||||
exports: ..., providers: ..., bootstrap: ...}).
|
||||
Class({ constructor: function() {}})`
|
||||
description:
|
||||
Defines a module that contains components, directives, pipes, and providers.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`declarations: [MyRedComponent, MyBlueComponent, MyDatePipe]`|`declarations:`
|
||||
description:
|
||||
List of components, directives, and pipes that belong to this module.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`imports: [BrowserModule, SomeOtherModule]`|`imports:`
|
||||
description:
|
||||
List of modules to import into this module. Everything from the imported modules
|
||||
is available to `declarations` of this module.
|
||||
|
||||
syntax(js):
|
||||
`imports: [ng.platformBrowser.BrowserModule, SomeOtherModule]`|`imports:`
|
||||
description:
|
||||
List of modules to import into this module. Everything from the imported modules
|
||||
is available to `declarations` of this module.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`exports: [MyRedComponent, MyDatePipe]`|`exports:`
|
||||
description:
|
||||
List of components, directives, and pipes visible to modules that import this module.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`providers: [MyService, { provide: ... }]`|`providers:`
|
||||
description:
|
||||
List of dependency injection providers visible both to the contents of this module and to importers of this module.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`bootstrap: [MyAppComponent]`|`bootstrap:`
|
||||
description:
|
||||
List of components to bootstrap when this module is bootstrapped.
|
@ -1,170 +0,0 @@
|
||||
@cheatsheetSection
|
||||
Routing and navigation
|
||||
@cheatsheetIndex 11
|
||||
@description
|
||||
{@target ts}`import { Routes, RouterModule, ... } from '@angular/router';`{@endtarget}
|
||||
{@target js}Available from the `ng.router` namespace{@endtarget}
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`const routes: Routes = [
|
||||
{ path: '', component: HomeComponent },
|
||||
{ path: 'path/:routeParam', component: MyComponent },
|
||||
{ path: 'staticPath', component: ... },
|
||||
{ path: '**', component: ... },
|
||||
{ path: 'oldPath', redirectTo: '/staticPath' },
|
||||
{ path: ..., component: ..., data: { message: 'Custom' } }
|
||||
]);
|
||||
|
||||
const routing = RouterModule.forRoot(routes);`|`Routes`
|
||||
syntax(js):
|
||||
`var routes = [
|
||||
{ path: '', component: HomeComponent },
|
||||
{ path: ':routeParam', component: MyComponent },
|
||||
{ path: 'staticPath', component: ... },
|
||||
{ path: '**', component: ... },
|
||||
{ path: 'oldPath', redirectTo: '/staticPath' },
|
||||
{ path: ..., component: ..., data: { message: 'Custom' } }
|
||||
]);
|
||||
|
||||
var routing = ng.router.RouterModule.forRoot(routes);`|`ng.router.Routes`
|
||||
description:
|
||||
Configures routes for the application. Supports static, parameterized, redirect, and wildcard routes. Also supports custom route data and resolve.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`
|
||||
<router-outlet></router-outlet>
|
||||
<router-outlet name="aux"></router-outlet>
|
||||
`|`router-outlet`
|
||||
description:
|
||||
Marks the location to load the component of the active route.
|
||||
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`
|
||||
<a routerLink="/path">
|
||||
<a [routerLink]="[ '/path', routeParam ]">
|
||||
<a [routerLink]="[ '/path', { matrixParam: 'value' } ]">
|
||||
<a [routerLink]="[ '/path' ]" [queryParams]="{ page: 1 }">
|
||||
<a [routerLink]="[ '/path' ]" fragment="anchor">
|
||||
`|`[routerLink]`
|
||||
description:
|
||||
Creates a link to a different view based on a route instruction consisting of a route path, required and optional parameters, query parameters, and a fragment. To navigate to a root route, use the `/` prefix; for a child route, use the `./`prefix; for a sibling or parent, use the `../` prefix.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<a [routerLink]="[ '/path' ]" routerLinkActive="active">`
|
||||
description:
|
||||
The provided classes are added to the element when the `routerLink` becomes the current active route.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`class CanActivateGuard implements CanActivate {
|
||||
canActivate(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): Observable<boolean>|Promise<boolean>|boolean { ... }
|
||||
}
|
||||
|
||||
{ path: ..., canActivate: [CanActivateGuard] }`|`CanActivate`
|
||||
syntax(js):
|
||||
`var CanActivateGuard = ng.core.Class({
|
||||
canActivate: function(route, state) {
|
||||
// return Observable/Promise boolean or boolean
|
||||
}
|
||||
});
|
||||
|
||||
{ path: ..., canActivate: [CanActivateGuard] }`|`CanActivate`
|
||||
description:
|
||||
An interface for defining a class that the router should call first to determine if it should activate this component. Should return a boolean or an Observable/Promise that resolves to a boolean.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`class CanDeactivateGuard implements CanDeactivate<T> {
|
||||
canDeactivate(
|
||||
component: T,
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): Observable<boolean>|Promise<boolean>|boolean { ... }
|
||||
}
|
||||
|
||||
{ path: ..., canDeactivate: [CanDeactivateGuard] }`|`CanDeactivate`
|
||||
syntax(js):
|
||||
`var CanDeactivateGuard = ng.core.Class({
|
||||
canDeactivate: function(component, route, state) {
|
||||
// return Observable/Promise boolean or boolean
|
||||
}
|
||||
});
|
||||
|
||||
{ path: ..., canDeactivate: [CanDeactivateGuard] }`|`CanDeactivate`
|
||||
description:
|
||||
An interface for defining a class that the router should call first to determine if it should deactivate this component after a navigation. Should return a boolean or an Observable/Promise that resolves to a boolean.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`class CanActivateChildGuard implements CanActivateChild {
|
||||
canActivateChild(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): Observable<boolean>|Promise<boolean>|boolean { ... }
|
||||
}
|
||||
|
||||
{ path: ..., canActivateChild: [CanActivateGuard],
|
||||
children: ... }`|`CanActivateChild`
|
||||
syntax(js):
|
||||
`var CanActivateChildGuard = ng.core.Class({
|
||||
canActivateChild: function(route, state) {
|
||||
// return Observable/Promise boolean or boolean
|
||||
}
|
||||
});
|
||||
|
||||
{ path: ..., canActivateChild: [CanActivateChildGuard],
|
||||
children: ... }`|`CanActivateChild`
|
||||
description:
|
||||
An interface for defining a class that the router should call first to determine if it should activate the child route. Should return a boolean or an Observable/Promise that resolves to a boolean.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`class ResolveGuard implements Resolve<T> {
|
||||
resolve(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): Observable<any>|Promise<any>|any { ... }
|
||||
}
|
||||
|
||||
{ path: ..., resolve: [ResolveGuard] }`|`Resolve`
|
||||
syntax(js):
|
||||
`var ResolveGuard = ng.core.Class({
|
||||
resolve: function(route, state) {
|
||||
// return Observable/Promise value or value
|
||||
}
|
||||
});
|
||||
|
||||
{ path: ..., resolve: [ResolveGuard] }`|`Resolve`
|
||||
description:
|
||||
An interface for defining a class that the router should call first to resolve route data before rendering the route. Should return a value or an Observable/Promise that resolves to a value.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax(ts):
|
||||
`class CanLoadGuard implements CanLoad {
|
||||
canLoad(
|
||||
route: Route
|
||||
): Observable<boolean>|Promise<boolean>|boolean { ... }
|
||||
}
|
||||
|
||||
{ path: ..., canLoad: [CanLoadGuard], loadChildren: ... }`|`CanLoad`
|
||||
syntax(js):
|
||||
`var CanLoadGuard = ng.core.Class({
|
||||
canLoad: function(route) {
|
||||
// return Observable/Promise boolean or boolean
|
||||
}
|
||||
});
|
||||
|
||||
{ path: ..., canLoad: [CanLoadGuard], loadChildren: ... }`|`CanLoad`
|
||||
description:
|
||||
An interface for defining a class that the router should call first to check if the lazy loaded module should be loaded. Should return a boolean or an Observable/Promise that resolves to a boolean.
|
||||
|
@ -1,94 +0,0 @@
|
||||
@cheatsheetSection
|
||||
Template syntax
|
||||
@cheatsheetIndex 2
|
||||
@description
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<input [value]="firstName">`|`[value]`
|
||||
description:
|
||||
Binds property `value` to the result of expression `firstName`.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<div [attr.role]="myAriaRole">`|`[attr.role]`
|
||||
description:
|
||||
Binds attribute `role` to the result of expression `myAriaRole`.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<div [class.extra-sparkle]="isDelightful">`|`[class.extra-sparkle]`
|
||||
description:
|
||||
Binds the presence of the CSS class `extra-sparkle` on the element to the truthiness of the expression `isDelightful`.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<div [style.width.px]="mySize">`|`[style.width.px]`
|
||||
description:
|
||||
Binds style property `width` to the result of expression `mySize` in pixels. Units are optional.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<button (click)="readRainbow($event)">`|`(click)`
|
||||
description:
|
||||
Calls method `readRainbow` when a click event is triggered on this button element (or its children) and passes in the event object.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<div title="Hello {{ponyName}}">`|`{{ponyName}}`
|
||||
description:
|
||||
Binds a property to an interpolated string, for example, "Hello Seabiscuit". Equivalent to:
|
||||
`<div [title]="'Hello ' + ponyName">`
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<p>Hello {{ponyName}}</p>`|`{{ponyName}}`
|
||||
description:
|
||||
Binds text content to an interpolated string, for example, "Hello Seabiscuit".
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<my-cmp [(title)]="name">`|`[(title)]`
|
||||
description:
|
||||
Sets up two-way data binding. Equivalent to: `<my-cmp [title]="name" (titleChange)="name=$event">`
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<video #movieplayer ...>
|
||||
<button (click)="movieplayer.play()">
|
||||
</video>`|`#movieplayer`|`(click)`
|
||||
description:
|
||||
Creates a local variable `movieplayer` that provides access to the `video` element instance in data-binding and event-binding expressions in the current template.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<p *myUnless="myExpression">...</p>`|`*myUnless`
|
||||
description:
|
||||
The `*` symbol turns the current element into an embedded template. Equivalent to:
|
||||
`<ng-template [myUnless]="myExpression"><p>...</p></ng-template>`
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<p>Card No.: {{cardNumber | myCardNumberFormatter}}</p>`|`{{cardNumber | myCardNumberFormatter}}`
|
||||
description:
|
||||
Transforms the current value of expression `cardNumber` via the pipe called `myCardNumberFormatter`.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<p>Employer: {{employer?.companyName}}</p>`|`{{employer?.companyName}}`
|
||||
description:
|
||||
The safe navigation operator (`?`) means that the `employer` field is optional and if `undefined`, the rest of the expression should be ignored.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<svg:rect x="0" y="0" width="100" height="100"/>`|`svg:`
|
||||
description:
|
||||
An SVG snippet template needs an `svg:` prefix on its root element to disambiguate the SVG element from an HTML component.
|
||||
|
||||
@cheatsheetItem
|
||||
syntax:
|
||||
`<svg>
|
||||
<rect x="0" y="0" width="100" height="100"/>
|
||||
</svg>`|`svg`
|
||||
description:
|
||||
An `<svg>` root element is detected as an SVG element automatically, without the prefix.
|
@ -416,14 +416,14 @@ can be found in the LICENSE file at http://angular.io/license
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Copyright 2016 Google Inc. All Rights Reserved.
|
||||
Use of this source code is governed by an MIT-style license that
|
||||
can be found in the LICENSE file at http://angular.io/license
|
||||
-->"><input type="hidden" name="files[app/banner.component.html]" value="<h1>{{title}}</h1>
|
||||
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Copyright 2016 Google Inc. All Rights Reserved.
|
||||
Use of this source code is governed by an MIT-style license that
|
||||
can be found in the LICENSE file at http://angular.io/license
|
||||
@ -432,7 +432,7 @@ can be found in the LICENSE file at http://angular.io/license
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Copyright 2016 Google Inc. All Rights Reserved.
|
||||
Use of this source code is governed by an MIT-style license that
|
||||
can be found in the LICENSE file at http://angular.io/license
|
||||
@ -445,7 +445,7 @@ can be found in the LICENSE file at http://angular.io/license
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Copyright 2016 Google Inc. All Rights Reserved.
|
||||
Use of this source code is governed by an MIT-style license that
|
||||
can be found in the LICENSE file at http://angular.io/license
|
||||
@ -462,7 +462,7 @@ can be found in the LICENSE file at http://angular.io/license
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Copyright 2016 Google Inc. All Rights Reserved.
|
||||
Use of this source code is governed by an MIT-style license that
|
||||
can be found in the LICENSE file at http://angular.io/license
|
||||
@ -476,7 +476,7 @@ can be found in the LICENSE file at http://angular.io/license
|
||||
</ul>
|
||||
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Copyright 2016 Google Inc. All Rights Reserved.
|
||||
Use of this source code is governed by an MIT-style license that
|
||||
can be found in the LICENSE file at http://angular.io/license
|
||||
@ -548,7 +548,7 @@ Copyright 2016 Google Inc. All Rights Reserved.
|
||||
Use of this source code is governed by an MIT-style license that
|
||||
can be found in the LICENSE file at http://angular.io/license
|
||||
*/"><input type="hidden" name="files[app/app.component.router.spec.ts]" value="// For more examples:
|
||||
// https://github.com/angular/angular/blob/master/modules/@angular/router/test/integration.spec.ts
|
||||
// https://github.com/angular/angular/blob/master/packages/router/test/integration.spec.ts
|
||||
|
||||
import { async, ComponentFixture, fakeAsync, TestBed, tick,
|
||||
} from '@angular/core/testing';
|
||||
@ -3296,7 +3296,7 @@ can be found in the LICENSE file at http://angular.io/license
|
||||
</html>
|
||||
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Copyright 2016 Google Inc. All Rights Reserved.
|
||||
Use of this source code is governed by an MIT-style license that
|
||||
can be found in the LICENSE file at http://angular.io/license
|
||||
@ -3375,4 +3375,4 @@ Copyright 2016 Google Inc. All Rights Reserved.
|
||||
Use of this source code is governed by an MIT-style license that
|
||||
can be found in the LICENSE file at http://angular.io/license
|
||||
*/
|
||||
"></form><script>document.getElementById("mainForm").submit();</script></body></html>
|
||||
"></form><script>document.getElementById("mainForm").submit();</script></body></html>
|
||||
|
@ -1,5 +1,5 @@
|
||||
// For more examples:
|
||||
// https://github.com/angular/angular/blob/master/modules/@angular/router/test/integration.spec.ts
|
||||
// https://github.com/angular/angular/blob/master/packages/router/test/integration.spec.ts
|
||||
|
||||
import { async, ComponentFixture, fakeAsync, TestBed, tick,
|
||||
} from '@angular/core/testing';
|
||||
|
3
aio/content/file-not-found.md
Normal file
3
aio/content/file-not-found.md
Normal file
@ -0,0 +1,3 @@
|
||||
@title Document not found
|
||||
@description
|
||||
<h3>Document not found</h3>
|
@ -2,7 +2,7 @@
|
||||
AngularJS to Angular Quick Reference
|
||||
|
||||
@intro
|
||||
Learn how AngularJS concepts and techniques map to Angular
|
||||
Learn how AngularJS concepts and techniques map to Angular.
|
||||
|
||||
@description
|
||||
|
||||
@ -16,16 +16,16 @@ by mapping AngularJS syntax to the equivalent Angular syntax.
|
||||
**See the Angular syntax in this <live-example name="cb-ajs-quick-reference"></live-example>**.
|
||||
|
||||
## Contents
|
||||
This page covers:
|
||||
* [Template basics](#template-basics) - binding and local variables.
|
||||
|
||||
* [Template directives](#template-directives) - built-in directives `ngIf` and `ngClass`.
|
||||
* [Template basics](guide/ajs-quick-reference#template-basics)—binding and local variables.
|
||||
|
||||
* [Filters/pipes](#filters-pipes) - built-in *filters*, known as *pipes* in Angular.
|
||||
* [Template directives](guide/ajs-quick-reference#template-directives)—built-in directives `ngIf` and `ngClass`.
|
||||
|
||||
* [Modules/controllers/components](#controllers-components) - *modules* in Angular are slightly different from *modules* in AngularJS, and *controllers* are *components* in Angular.
|
||||
* [Filters/pipes](guide/ajs-quick-reference#filters-pipes)—built-in *filters*, known as *pipes* in Angular.
|
||||
|
||||
* [Style sheets](#style-sheets) - more options for CSS than in AngularJS.
|
||||
* [Modules/controllers/components](guide/ajs-quick-reference#controllers-components)—*modules* in Angular are slightly different from *modules* in AngularJS, and *controllers* are *components* in Angular.
|
||||
|
||||
* [Style sheets](guide/ajs-quick-reference#style-sheets)—more options for CSS than in AngularJS.
|
||||
|
||||
## Template basics
|
||||
Templates are the user-facing part of an Angular application and are written in HTML.
|
||||
@ -86,7 +86,8 @@ The following table lists some of the key AngularJS template features with their
|
||||
The context of the binding is implied and is always the
|
||||
associated component, so it needs no reference variable.
|
||||
|
||||
For more information, see the [Interpolation](../guide/template-syntax.html#interpolation) section of the Template Syntax page.
|
||||
For more information, see the [Interpolation](guide/template-syntax)
|
||||
section of the [Template Syntax](guide/template-syntax) page.
|
||||
</td>
|
||||
|
||||
|
||||
@ -116,7 +117,7 @@ The following table lists some of the key AngularJS template features with their
|
||||
Many (but not all) of the built-in filters from AngularJS are
|
||||
built-in pipes in Angular.
|
||||
|
||||
For more information, see the heading [Filters/pipes](#filters-pipes) below.
|
||||
For more information, see [Filters/pipes](guide/ajs-quick-reference#filters-pipes) below.
|
||||
</td>
|
||||
|
||||
|
||||
@ -144,7 +145,8 @@ The following table lists some of the key AngularJS template features with their
|
||||
|
||||
Angular has true template input variables that are explicitly defined using the `let` keyword.
|
||||
|
||||
For more information, see the [ngFor micro-syntax](../guide/template-syntax.html#ngForMicrosyntax) section of the Template Syntax page.
|
||||
For more information, see the [ngFor micro-syntax](guide/template-syntax)
|
||||
section of the [Template Syntax](guide/template-syntax) page.
|
||||
</td>
|
||||
|
||||
|
||||
@ -153,7 +155,7 @@ The following table lists some of the key AngularJS template features with their
|
||||
|
||||
</table>
|
||||
|
||||
[Back to top](#top)
|
||||
[Back to top](guide/ajs-quick-reference#top)
|
||||
|
||||
## Template directives
|
||||
AngularJS provides more than seventy built-in directives for templates.
|
||||
@ -217,7 +219,7 @@ The following are some of the key AngularJS built-in directives and their equiva
|
||||
in `main.ts`
|
||||
and the application's root component (`AppComponent`) in `app.module.ts`.
|
||||
|
||||
For more information see the [Setup](../guide/setup.html) page.
|
||||
For more information see the [Setup](guide/setup) page.
|
||||
</td>
|
||||
|
||||
|
||||
@ -260,7 +262,8 @@ The following are some of the key AngularJS built-in directives and their equiva
|
||||
Angular also has **class binding**, which is a good way to add or remove a single class,
|
||||
as shown in the third example.
|
||||
|
||||
For more information see the [Attribute, Class, and Style Bindings](../guide/template-syntax.html#other-bindings) section of the Template Syntax page.
|
||||
For more information see the [Attribute, class, and style bindings](guide/template-syntax)
|
||||
section of the [Template Syntax](guide/template-syntax) page.
|
||||
|
||||
</td>
|
||||
|
||||
@ -287,7 +290,7 @@ The following are some of the key AngularJS built-in directives and their equiva
|
||||
|
||||
|
||||
<td>
|
||||
### bind to the `click` event
|
||||
### Bind to the `click` event
|
||||
|
||||
{@example 'cb-ajs-quick-reference/ts/src/app/app.component.html' region='event-binding'}
|
||||
|
||||
@ -306,7 +309,8 @@ The following are some of the key AngularJS built-in directives and their equiva
|
||||
|
||||
For a list of DOM events, see: https://developer.mozilla.org/en-US/docs/Web/Events.
|
||||
|
||||
For more information, see the [Event Binding](../guide/template-syntax.html#event-binding) section of the Template Syntax page.
|
||||
For more information, see the [Event binding](guide/template-syntax)
|
||||
section of the [Template Syntax](guide/template-syntax) page.
|
||||
|
||||
</td>
|
||||
|
||||
@ -336,7 +340,7 @@ The following are some of the key AngularJS built-in directives and their equiva
|
||||
In Angular, the template no longer specifies its associated controller.
|
||||
Rather, the component specifies its associated template as part of the component class decorator.
|
||||
|
||||
For more information, see [Architecture Overview](../guide/architecture.html#component).
|
||||
For more information, see [Architecture Overview](guide/architecture).
|
||||
|
||||
</td>
|
||||
|
||||
@ -349,14 +353,14 @@ The following are some of the key AngularJS built-in directives and their equiva
|
||||
<td>
|
||||
### ng-hide
|
||||
In AngularJS, the `ng-hide` directive shows or hides the associated HTML element based on
|
||||
an expression. For more information, see [ng-show](#ng-show).
|
||||
an expression. For more information, see [ng-show](guide/ajs-quick-reference#ng-show).
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
### bind to the `hidden` property
|
||||
### Bind to the `hidden` property
|
||||
In Angular, you use property binding; there is no built-in *hide* directive.
|
||||
For more information, see [ng-show](#ng-show).
|
||||
For more information, see [ng-show](guide/ajs-quick-reference#ng-show).
|
||||
</td>
|
||||
|
||||
|
||||
@ -385,20 +389,22 @@ The following are some of the key AngularJS built-in directives and their equiva
|
||||
|
||||
|
||||
<td>
|
||||
### bind to the `href` property
|
||||
### Bind to the `href` property
|
||||
|
||||
{@example 'cb-ajs-quick-reference/ts/src/app/app.component.html' region='href'}
|
||||
|
||||
Angular, uses property binding; there is no built-in *href* directive.
|
||||
Angular uses property binding; there is no built-in *href* directive.
|
||||
Place the element's `href` property in square brackets and set it to a quoted template expression.
|
||||
|
||||
For more information on property binding, see [Template Syntax](../guide/template-syntax.html#property-binding).
|
||||
For more information see the [Property binding](guide/template-syntax)
|
||||
section of the [Template Syntax](guide/template-syntax) page.
|
||||
|
||||
In Angular, `href` is no longer used for routing. Routing uses `routerLink`, as shown in the third example.
|
||||
In Angular, `href` is no longer used for routing. Routing uses `routerLink`, as shown in the following example.
|
||||
|
||||
{@example 'cb-ajs-quick-reference/ts/src/app/app.component.html' region='router-link'}
|
||||
|
||||
For more information on routing, see [Routing & Navigation](../guide/router.html#router-link).
|
||||
For more information on routing, see the [RouterLink binding](guide/router)
|
||||
section of the [Routing & Navigation](guide/router) page.
|
||||
|
||||
</td>
|
||||
|
||||
@ -417,7 +423,7 @@ The following are some of the key AngularJS built-in directives and their equiva
|
||||
In AngularJS, the `ng-if` directive removes or recreates a portion of the DOM,
|
||||
based on an expression. If the expression is false, the element is removed from the DOM.
|
||||
|
||||
In this example, the `table` element is removed from the DOM unless the `movies` array has a length greater than zero.
|
||||
In this example, the `<table>` element is removed from the DOM unless the `movies` array has a length greater than zero.
|
||||
</td>
|
||||
|
||||
|
||||
@ -426,12 +432,13 @@ The following are some of the key AngularJS built-in directives and their equiva
|
||||
|
||||
{@example 'cb-ajs-quick-reference/ts/src/app/movie-list.component.html' region='ngIf'}
|
||||
|
||||
The `*ngIf` directive in Angular works the same as the `ng-if` directive in AngularJS. It removes or recreates a portion of the DOM based on an expression.
|
||||
The `*ngIf` directive in Angular works the same as the `ng-if` directive in AngularJS. It removes
|
||||
or recreates a portion of the DOM based on an expression.
|
||||
|
||||
In this example, the `table` element is removed from the DOM unless the `movies` array has a length.
|
||||
In this example, the `<table>` element is removed from the DOM unless the `movies` array has a length.
|
||||
|
||||
The (*) before `ngIf` is required in this example.
|
||||
For more information, see [Structural Directives](../guide/structural-directives.html).
|
||||
For more information, see [Structural Directives](guide/structural-directives).
|
||||
</td>
|
||||
|
||||
|
||||
@ -459,7 +466,9 @@ The following are some of the key AngularJS built-in directives and their equiva
|
||||
In Angular, **two-way binding** is denoted by `[()]`, descriptively referred to as a "banana in a box". This syntax is a shortcut for defining both property binding (from the component to the view)
|
||||
and event binding (from the view to the component), thereby providing two-way binding.
|
||||
|
||||
For more information on two-way binding with ngModel, see [Template Syntax](../guide/template-syntax.html#ngModel).
|
||||
For more information on two-way binding with `ngModel`, see the [NgModel—Two-way binding to
|
||||
form elements with `[(ngModel)]`](../guide/template-syntax.html#ngModel)
|
||||
section of the [Template Syntax](guide/template-syntax) page.
|
||||
</td>
|
||||
|
||||
|
||||
@ -477,7 +486,7 @@ The following are some of the key AngularJS built-in directives and their equiva
|
||||
In AngularJS, the `ng-repeat` directive repeats the associated DOM element
|
||||
for each item in the specified collection.
|
||||
|
||||
In this example, the table row (`tr`) element repeats for each movie object in the collection of movies.
|
||||
In this example, the table row (`<tr>`) element repeats for each movie object in the collection of movies.
|
||||
</td>
|
||||
|
||||
|
||||
@ -486,8 +495,9 @@ The following are some of the key AngularJS built-in directives and their equiva
|
||||
|
||||
{@example 'cb-ajs-quick-reference/ts/src/app/movie-list.component.html' region='ngFor'}
|
||||
|
||||
The `*ngFor` directive in Angular is similar to the `ng-repeat` directive in AngularJS. It repeats the associated DOM element for each item in the specified collection.
|
||||
More accurately, it turns the defined element (`tr` in this example) and its contents into a template and
|
||||
The `*ngFor` directive in Angular is similar to the `ng-repeat` directive in AngularJS. It repeats
|
||||
the associated DOM element for each item in the specified collection.
|
||||
More accurately, it turns the defined element (`<tr>` in this example) and its contents into a template and
|
||||
uses that template to instantiate a view for each item in the list.
|
||||
|
||||
Notice the other syntax differences:
|
||||
@ -495,7 +505,7 @@ The following are some of the key AngularJS built-in directives and their equiva
|
||||
the `let` keyword identifies `movie` as an input variable;
|
||||
the list preposition is `of`, not `in`.
|
||||
|
||||
For more information, see [Structural Directives](../guide/structural-directives.html).
|
||||
For more information, see [Structural Directives](guide/structural-directives).
|
||||
</td>
|
||||
|
||||
|
||||
@ -515,24 +525,25 @@ The following are some of the key AngularJS built-in directives and their equiva
|
||||
In AngularJS, the `ng-show` directive shows or hides the associated DOM element, based on
|
||||
an expression.
|
||||
|
||||
In this example, the `div` element is shown if the `favoriteHero` variable is truthy.
|
||||
In this example, the `<div>` element is shown if the `favoriteHero` variable is truthy.
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
### bind to the `hidden` property
|
||||
### Bind to the `hidden` property
|
||||
|
||||
{@example 'cb-ajs-quick-reference/ts/src/app/movie-list.component.html' region='hidden'}
|
||||
|
||||
Angular, uses property binding; there is no built-in *show* directive.
|
||||
Angular uses property binding; there is no built-in *show* directive.
|
||||
For hiding and showing elements, bind to the HTML `hidden` property.
|
||||
|
||||
To conditionally display an element, place the element's `hidden` property in square brackets and
|
||||
set it to a quoted template expression that evaluates to the *opposite* of *show*.
|
||||
|
||||
In this example, the `div` element is hidden if the `favoriteHero` variable is not truthy.
|
||||
In this example, the `<div>` element is hidden if the `favoriteHero` variable is not truthy.
|
||||
|
||||
For more information on property binding, see [Template Syntax](../guide/template-syntax.html#property-binding).
|
||||
For more information on property binding, see the [Property binding](guide/template-syntax)
|
||||
section of the [Template Syntax](guide/template-syntax) page.
|
||||
</td>
|
||||
|
||||
|
||||
@ -554,14 +565,15 @@ The following are some of the key AngularJS built-in directives and their equiva
|
||||
|
||||
|
||||
<td>
|
||||
### bind to the `src` property
|
||||
### Bind to the `src` property
|
||||
|
||||
{@example 'cb-ajs-quick-reference/ts/src/app/app.component.html' region='src'}
|
||||
|
||||
Angular, uses property binding; there is no built-in *src* directive.
|
||||
Angular uses property binding; there is no built-in *src* directive.
|
||||
Place the `src` property in square brackets and set it to a quoted template expression.
|
||||
|
||||
For more information on property binding, see [Template Syntax](../guide/template-syntax.html#property-binding).
|
||||
For more information on property binding, see the [Property binding](guide/template-syntax)
|
||||
section of the [Template Syntax](guide/template-syntax) page.
|
||||
</td>
|
||||
|
||||
|
||||
@ -578,7 +590,7 @@ The following are some of the key AngularJS built-in directives and their equiva
|
||||
|
||||
In AngularJS, the `ng-style` directive sets a CSS style on an HTML element
|
||||
based on an expression. That expression is often a key-value control object with each
|
||||
key of the object defined as a CSS style name, and each value defined as an expression
|
||||
key of the object defined as a CSS property, and each value defined as an expression
|
||||
that evaluates to a value appropriate for the style.
|
||||
|
||||
In the example, the `color` style is set to the current value of the `colorPreference` variable.
|
||||
@ -596,9 +608,11 @@ The following are some of the key AngularJS built-in directives and their equiva
|
||||
|
||||
Angular also has **style binding**, which is good way to set a single style. This is shown in the second example.
|
||||
|
||||
For more information on style binding, see [Template Syntax](../guide/template-syntax.html#style-binding).
|
||||
For more information on style binding, see the [Style binding](guide/template-syntax) section of the
|
||||
[Template Syntax](guide/template-syntax) page.
|
||||
|
||||
For more information on the ngStyle directive, see [Template Syntax](../guide/template-syntax.html#ngStyle).
|
||||
For more information on the `ngStyle` directive, see [NgStyle](guide/template-syntax)
|
||||
section of the [Template Syntax](guide/template-syntax) page.
|
||||
</td>
|
||||
|
||||
|
||||
@ -650,7 +664,8 @@ The following are some of the key AngularJS built-in directives and their equiva
|
||||
|
||||
The (*) before `ngSwitchCase` and `ngSwitchDefault` is required in this example.
|
||||
|
||||
For more information on the ngSwitch directive, see [Template Syntax](../guide/template-syntax.html#ngSwitch).
|
||||
For more information, see [The NgSwitch directives](guide/template-syntax)
|
||||
section of the [Template Syntax](guide/template-syntax) page.
|
||||
</td>
|
||||
|
||||
|
||||
@ -659,15 +674,15 @@ The following are some of the key AngularJS built-in directives and their equiva
|
||||
|
||||
</table>
|
||||
|
||||
[Back to top](#top)
|
||||
[Back to top](guide/ajs-quick-reference#top)
|
||||
|
||||
|
||||
{@a filters-pipes}
|
||||
|
||||
## Filters/pipes
|
||||
Angular **pipes** provide formatting and transformation for data in our template, similar to AngularJS **filters**.
|
||||
Angular **pipes** provide formatting and transformation for data in the template, similar to AngularJS **filters**.
|
||||
Many of the built-in filters in AngularJS have corresponding pipes in Angular.
|
||||
For more information on pipes, see [Pipes](../guide/pipes.html).
|
||||
For more information on pipes, see [Pipes](guide/pipes).
|
||||
|
||||
<table width="100%">
|
||||
|
||||
@ -704,7 +719,7 @@ For more information on pipes, see [Pipes](../guide/pipes.html).
|
||||
<td>{{movie.price | currency}}</td>
|
||||
</code-example>
|
||||
|
||||
Formats a number as a currency.
|
||||
Formats a number as currency.
|
||||
</td>
|
||||
|
||||
|
||||
@ -881,7 +896,7 @@ For more information on pipes, see [Pipes](../guide/pipes.html).
|
||||
</code-example>
|
||||
|
||||
Displays the collection in the order specified by the expression.
|
||||
In this example, the movie title orders the movieList.
|
||||
In this example, the movie title orders the `movieList`.
|
||||
</td>
|
||||
|
||||
|
||||
@ -898,7 +913,7 @@ For more information on pipes, see [Pipes](../guide/pipes.html).
|
||||
|
||||
</table>
|
||||
|
||||
[Back to top](#top)
|
||||
[Back to top](guide/ajs-quick-reference#top)
|
||||
|
||||
|
||||
{@a controllers-components}
|
||||
@ -949,17 +964,19 @@ The Angular code is shown using TypeScript.
|
||||
}());
|
||||
</code-example>
|
||||
|
||||
In AngularJS, you often defined an immediately invoked function expression (or IIFE) around your controller code.
|
||||
This kept your controller code out of the global namespace.
|
||||
In AngularJS, an immediately invoked function expression (or IIFE) around controller code
|
||||
keeps it out of the global namespace.
|
||||
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
### none
|
||||
You don't need to worry about this in Angular because you use ES 2015 modules
|
||||
and modules handle the namespacing for you.
|
||||
This is a nonissue in Angular because ES 2015 modules
|
||||
handle the namespacing for you.
|
||||
|
||||
For more information on modules, see [Architecture Overview](../guide/architecture.html#module).
|
||||
For more information on modules, see the [Modules](guide/architecture) section of the
|
||||
[Architecture Overview](guide/architecture).
|
||||
</td>
|
||||
|
||||
|
||||
@ -974,7 +991,8 @@ The Angular code is shown using TypeScript.
|
||||
angular.module("movieHunter", ["ngRoute"]);
|
||||
</code-example>
|
||||
|
||||
In AngularJS, an Angular module keeps track of controllers, services, and other code. The second argument defines the list of other modules that this module depends upon.
|
||||
In AngularJS, an Angular module keeps track of controllers, services, and other code.
|
||||
The second argument defines the list of other modules that this module depends upon.
|
||||
</td>
|
||||
|
||||
|
||||
@ -987,7 +1005,7 @@ The Angular code is shown using TypeScript.
|
||||
- `imports`: specifies the list of other modules that this module depends upon
|
||||
- `declaration`: keeps track of your components, pipes, and directives.
|
||||
|
||||
For more information on modules, see [Angular Modules](../guide/ngmodule.html).
|
||||
For more information on modules, see [Angular Modules (NgModule)](guide/ngmodule).
|
||||
</td>
|
||||
|
||||
|
||||
@ -1006,7 +1024,7 @@ The Angular code is shown using TypeScript.
|
||||
MovieListCtrl]);
|
||||
</code-example>
|
||||
|
||||
AngularJS, has code in each controller that looks up an appropriate Angular module
|
||||
AngularJS has code in each controller that looks up an appropriate Angular module
|
||||
and registers the controller with that module.
|
||||
|
||||
The first argument is the controller name. The second argument defines the string names of
|
||||
@ -1015,17 +1033,18 @@ The Angular code is shown using TypeScript.
|
||||
|
||||
|
||||
<td>
|
||||
### Component Decorator
|
||||
### Component decorator
|
||||
|
||||
{@example 'cb-ajs-quick-reference/ts/src/app/movie-list.component.ts' region='component'}
|
||||
|
||||
Angular, adds a decorator to the component class to provide any required metadata.
|
||||
The Component decorator declares that the class is a component and provides metadata about
|
||||
Angular adds a decorator to the component class to provide any required metadata.
|
||||
The `@Component` decorator declares that the class is a component and provides metadata about
|
||||
that component such as its selector (or tag) and its template.
|
||||
|
||||
This is how you associate a template with code, which is defined in the component class.
|
||||
This is how you associate a template with logic, which is defined in the component class.
|
||||
|
||||
For more information, see the [Components](../guide/architecture.html#components) section of the Architecture Overview page.
|
||||
For more information, see the [Components](guide/architecture)
|
||||
section of the [Architecture Overview](guide/architecture) page.
|
||||
</td>
|
||||
|
||||
|
||||
@ -1054,7 +1073,8 @@ The Angular code is shown using TypeScript.
|
||||
|
||||
NOTE: If you are using TypeScript with AngularJS, you must use the `export` keyword to export the component class.
|
||||
|
||||
For more information, see the [Components](../guide/architecture.html#components) section of the Architecture Overview page.
|
||||
For more information, see the [Components](guide/architecture)
|
||||
section of the [Architecture Overview](guide/architecture) page.
|
||||
</td>
|
||||
|
||||
|
||||
@ -1088,7 +1108,8 @@ The Angular code is shown using TypeScript.
|
||||
This example injects a `MovieService`.
|
||||
The first parameter's TypeScript type tells Angular what to inject, even after minification.
|
||||
|
||||
For more information, see the [Dependency Injection](../guide/architecture.html#dependency-injection) section of the Architecture Overview.
|
||||
For more information, see the [Dependency injection](guide/architecture)
|
||||
section of the [Architecture Overview](guide/architecture).
|
||||
</td>
|
||||
|
||||
|
||||
@ -1097,7 +1118,7 @@ The Angular code is shown using TypeScript.
|
||||
|
||||
</table>
|
||||
|
||||
[Back to top](#top)
|
||||
[Back to top](guide/ajs-quick-reference#top)
|
||||
|
||||
|
||||
{@a style-sheets}
|
||||
@ -1171,4 +1192,4 @@ also encapsulate a style sheet within a specific component.
|
||||
|
||||
</table>
|
||||
|
||||
[Back to top](#top)
|
||||
[Back to top](guide/ajs-quick-reference#top)
|
@ -30,16 +30,16 @@ add it to your page.
|
||||
|
||||
# Contents
|
||||
|
||||
* [Example: Transitioning between two states](#example-transitioning-between-states).
|
||||
* [States and transitions](#states-and-transitions).
|
||||
* [Example: Entering and leaving](#example-entering-and-leaving).
|
||||
* [Example: Entering and leaving from different states](#example-entering-and-leaving-from-different-states).
|
||||
* [Animatable properties and units](#animatable-properties-and-units).
|
||||
* [Automatic property calculation](#automatic-property-calculation).
|
||||
* [Animation timing](#animation-timing).
|
||||
* [Multi-step animations with keyframes](#multi-step-animations-with-keyframes).
|
||||
* [Parallel animation groups](#parallel-animation-groups).
|
||||
* [Animation callbacks](#animation-callbacks).
|
||||
* [Example: Transitioning between two states](guide/animations#example-transitioning-between-states).
|
||||
* [States and transitions](guide/animations#states-and-transitions).
|
||||
* [Example: Entering and leaving](guide/animations#example-entering-and-leaving).
|
||||
* [Example: Entering and leaving from different states](guide/animations#example-entering-and-leaving-from-different-states).
|
||||
* [Animatable properties and units](guide/animations#animatable-properties-and-units).
|
||||
* [Automatic property calculation](guide/animations#automatic-property-calculation).
|
||||
* [Animation timing](guide/animations#animation-timing).
|
||||
* [Multi-step animations with keyframes](guide/animations#multi-step-animations-with-keyframes).
|
||||
* [Parallel animation groups](guide/animations#parallel-animation-groups).
|
||||
* [Animation callbacks](guide/animations#animation-callbacks).
|
||||
|
||||
The examples in this page are available as a <live-example></live-example>.
|
||||
|
||||
@ -48,7 +48,7 @@ The examples in this page are available as a <live-example></live-example>.
|
||||
|
||||
## Quickstart example: Transitioning between two states
|
||||
<figure>
|
||||
<img src="/resources/images/devguide/animations/animation_basic_click.gif" alt="A simple transition animation" align="right" style="width:220px;margin-left:20px"> </img>
|
||||
<img src="assets/images/devguide/animations/animation_basic_click.gif" alt="A simple transition animation" align="right" style="width:220px;margin-left:20px"> </img>
|
||||
</figure>
|
||||
|
||||
You can build a simple animation that transitions an element between two states
|
||||
@ -121,7 +121,7 @@ controls the timing of switching between one set of styles and the next:
|
||||
|
||||
|
||||
<figure class='image-display'>
|
||||
<img src="/resources/images/devguide/animations/ng_animate_transitions_inactive_active.png" alt="In Angular animations you define states and transitions between states" width="400"> </img>
|
||||
<img src="assets/images/devguide/animations/ng_animate_transitions_inactive_active.png" alt="In Angular animations you define states and transitions between states" width="400"> </img>
|
||||
</figure>
|
||||
|
||||
If several transitions have the same timing configuration, you can combine
|
||||
@ -154,7 +154,7 @@ transitions that apply regardless of which state the animation is in. For exampl
|
||||
* The `* => *` transition applies when *any* change between two states takes place.
|
||||
|
||||
<figure class='image-display'>
|
||||
<img src="/resources/images/devguide/animations/ng_animate_transitions_inactive_active_wildcards.png" alt="The wildcard state can be used to match many different transitions at once" width="400"> </img>
|
||||
<img src="assets/images/devguide/animations/ng_animate_transitions_inactive_active_wildcards.png" alt="The wildcard state can be used to match many different transitions at once" width="400"> </img>
|
||||
</figure>
|
||||
|
||||
### The `void` state
|
||||
@ -168,14 +168,14 @@ For example the `* => void` transition applies when the element leaves the view,
|
||||
regardless of what state it was in before it left.
|
||||
|
||||
<figure class='image-display'>
|
||||
<img src="/resources/images/devguide/animations/ng_animate_transitions_void_in.png" alt="The void state can be used for enter and leave transitions" width="400"> </img>
|
||||
<img src="assets/images/devguide/animations/ng_animate_transitions_void_in.png" alt="The void state can be used for enter and leave transitions" width="400"> </img>
|
||||
</figure>
|
||||
|
||||
The wildcard state `*` also matches `void`.
|
||||
|
||||
## Example: Entering and leaving
|
||||
<figure>
|
||||
<img src="/resources/images/devguide/animations/animation_enter_leave.gif" alt="Enter and leave animations" align="right" style="width:250px;"> </img>
|
||||
<img src="assets/images/devguide/animations/animation_enter_leave.gif" alt="Enter and leave animations" align="right" style="width:250px;"> </img>
|
||||
</figure>
|
||||
|
||||
Using the `void` and `*` states you can define transitions that animate the
|
||||
@ -184,6 +184,8 @@ entering and leaving of elements:
|
||||
* Enter: `void => *`
|
||||
* Leave: `* => void`
|
||||
|
||||
For example, in the `animations` !{_array} below there are two transitions that use
|
||||
the `void => *` and `* => void` syntax to animate the element in and out of the view.
|
||||
|
||||
{@example 'animations/ts/src/app/hero-list-enter-leave.component.ts' region='animationdef'}
|
||||
|
||||
@ -201,7 +203,7 @@ These two common animations have their own aliases:
|
||||
|
||||
## Example: Entering and leaving from different states
|
||||
<figure>
|
||||
<img src="/resources/images/devguide/animations/animation_enter_leave_states.gif" alt="Enter and leave animations combined with state animations" align="right" style="width:200px"> </img>
|
||||
<img src="assets/images/devguide/animations/animation_enter_leave_states.gif" alt="Enter and leave animations combined with state animations" align="right" style="width:200px"> </img>
|
||||
</figure>
|
||||
|
||||
You can also combine this animation with the earlier state transition animation by
|
||||
@ -217,7 +219,7 @@ is:
|
||||
This gives you fine-grained control over each transition:
|
||||
|
||||
<figure class='image-display'>
|
||||
<img src="/resources/images/devguide/animations/ng_animate_transitions_inactive_active_void.png" alt="This example transitions between active, inactive, and void states" width="400"> </img>
|
||||
<img src="assets/images/devguide/animations/ng_animate_transitions_inactive_active_void.png" alt="This example transitions between active, inactive, and void states" width="400"> </img>
|
||||
</figure>
|
||||
|
||||
|
||||
@ -245,7 +247,7 @@ If you don't provide a unit when specifying dimension, Angular assumes the defau
|
||||
|
||||
## Automatic property calculation
|
||||
<figure>
|
||||
<img src="/resources/images/devguide/animations/animation_auto.gif" alt="Animation with automated height calculation" align="right" style="width:220px;margin-left:20px"> </img>
|
||||
<img src="assets/images/devguide/animations/animation_auto.gif" alt="Animation with automated height calculation" align="right" style="width:220px;margin-left:20px"> </img>
|
||||
</figure>
|
||||
|
||||
Sometimes you don't know the value of a dimensional style property until runtime.
|
||||
@ -297,21 +299,22 @@ and the delay (or as the *second* value when there is no delay):
|
||||
* Run for 200ms, with easing: `'0.2s ease-in-out'`
|
||||
|
||||
<figure>
|
||||
<img src="/resources/images/devguide/animations/animation_timings.gif" alt="Animations with specific timings" align="right" style="width:220px;margin-left:20px"> </img>
|
||||
<img src="assets/images/devguide/animations/animation_timings.gif" alt="Animations with specific timings" align="right" style="width:220px;margin-left:20px"> </img>
|
||||
</figure>
|
||||
|
||||
### Example
|
||||
|
||||
Here are a couple of custom timings in action. Both enter and leave last for
|
||||
200 milliseconds but they have different easings. The leave begins after a
|
||||
slight delay:
|
||||
200 milliseconds, that is `0.2s`, but they have different easings. The leave begins after a
|
||||
slight delay of 10 milliseconds as specified in `'0.2s 10 ease-out'`:
|
||||
|
||||
|
||||
|
||||
{@example 'animations/ts/src/app/hero-list-timings.component.ts' region='animationdef'}
|
||||
|
||||
## Multi-step animations with keyframes
|
||||
<figure>
|
||||
<img src="/resources/images/devguide/animations/animation_multistep.gif" alt="Animations with some bounce implemented with keyframes" align="right" style="width:220px;margin-left:20px"> </img>
|
||||
<img src="assets/images/devguide/animations/animation_multistep.gif" alt="Animations with some bounce implemented with keyframes" align="right" style="width:220px;margin-left:20px"> </img>
|
||||
</figure>
|
||||
|
||||
Animation *keyframes* go beyond a simple transition to a more intricate animation
|
||||
@ -336,7 +339,7 @@ spacing are automatically assigned. For example, three keyframes without predefi
|
||||
offsets receive offsets `0`, `0.5`, and `1`.
|
||||
## Parallel animation groups
|
||||
<figure>
|
||||
<img src="/resources/images/devguide/animations/animation_groups.gif" alt="Parallel animations with different timings, implemented with groups" align="right" style="width:220px;margin-left:20px"> </img>
|
||||
<img src="assets/images/devguide/animations/animation_groups.gif" alt="Parallel animations with different timings, implemented with groups" align="right" style="width:220px;margin-left:20px"> </img>
|
||||
</figure>
|
||||
|
||||
You've seen how to animate multiple style properties at the same time:
|
||||
@ -358,7 +361,7 @@ One group animates the element transform and width; the other group animates the
|
||||
|
||||
A callback is fired when an animation is started and also when it is done.
|
||||
|
||||
In the keyframes example, you have a `trigger` called `@flyInOut`. There you can hook
|
||||
In the keyframes example, you have a `trigger` called `@flyInOut`. You can hook
|
||||
those callbacks like this:
|
||||
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
Ahead-of-Time Compilation
|
||||
|
||||
@intro
|
||||
Learn how to use Ahead-of-time compilation
|
||||
Learn how to use Ahead-of-time compilation.
|
||||
|
||||
@description
|
||||
This cookbook describes how to radically improve performance by compiling _Ahead of Time_ (AOT)
|
||||
@ -11,16 +11,16 @@ during a build process.
|
||||
|
||||
{@a toc}
|
||||
## Table of Contents
|
||||
* [Overview](#overview)
|
||||
* [_Ahead-of-Time_ vs _Just-in-Time_](#aot-jit)
|
||||
* [Compile with AOT](#compile)
|
||||
* [Bootstrap](#bootstrap)
|
||||
* [Tree Shaking](#tree-shaking)
|
||||
* [Load the bundle](#load)
|
||||
* [Serve the app](#serve)
|
||||
* [Workflow and convenience script](#workflow)
|
||||
* [Source Code](#source-code)
|
||||
* [Tour of Heroes](#toh)
|
||||
* [Overview](guide/aot-compiler#overview)
|
||||
* [_Ahead-of-Time_ vs _Just-in-Time_](guide/aot-compiler#aot-jit)
|
||||
* [Compile with AOT](guide/aot-compiler#compile)
|
||||
* [Bootstrap](guide/aot-compiler#bootstrap)
|
||||
* [Tree Shaking](guide/aot-compiler#tree-shaking)
|
||||
* [Load the bundle](guide/aot-compiler#load)
|
||||
* [Serve the app](guide/aot-compiler#serve)
|
||||
* [Workflow and convenience script](guide/aot-compiler#workflow)
|
||||
* [Source Code](guide/aot-compiler#source-code)
|
||||
* [Tour of Heroes](guide/aot-compiler#toh)
|
||||
|
||||
|
||||
{@a overview}
|
||||
@ -128,7 +128,7 @@ then modify it to look as follows.
|
||||
|
||||
The `compilerOptions` section is unchanged except for one property.
|
||||
**Set the `module` to `es2015`**.
|
||||
This is important as explained later in the [Tree Shaking](#tree-shaking) section.
|
||||
This is important as explained later in the [Tree Shaking](guide/aot-compiler#tree-shaking) section.
|
||||
|
||||
What's really new is the `ngc` section at the bottom called `angularCompilerOptions`.
|
||||
Its `"genDir"` property tells the compiler
|
||||
@ -198,7 +198,7 @@ The AOT path changes application bootstrapping.
|
||||
Instead of bootstrapping `AppModule`, you bootstrap the application with the generated module factory, `AppModuleNgFactory`.
|
||||
|
||||
Make a copy of `main.ts` and name it `main-jit.ts`.
|
||||
This is the JIT version; set it aside as you may need it [later](#run-jit "Running with JIT").
|
||||
This is the JIT version; set it aside as you may need it [later](guide/aot-compiler#run-jit "Running with JIT").
|
||||
|
||||
Open `main.ts` and convert it to AOT compilation.
|
||||
Switch from the `platformBrowserDynamic.bootstrap` used in JIT compilation to
|
||||
@ -408,7 +408,7 @@ The same source code can be built both ways. Here's one way to do that.
|
||||
{@example 'cb-aot-compiler/ts/src/index-jit.html' region='jit'}
|
||||
|
||||
Notice the slight change to the `system.import` which now specifies `src/app/main-jit`.
|
||||
That's the JIT version of the bootstrap file that we preserved [above](#bootstrap)
|
||||
That's the JIT version of the bootstrap file that we preserved [above](guide/aot-compiler#bootstrap)
|
||||
Open a _different_ terminal window and enter.
|
||||
<code-example language="none" class="code-shell">
|
||||
npm start
|
||||
@ -434,7 +434,7 @@ Now you can develop JIT and AOT, side-by-side.
|
||||
|
||||
The sample above is a trivial variation of the QuickStart app.
|
||||
In this section you apply what you've learned about AOT compilation and Tree Shaking
|
||||
to an app with more substance, the tutorial [_Tour of Heroes_](../tutorial/toh-pt6.html).
|
||||
to an app with more substance, the tutorial [_Tour of Heroes_](tutorial/toh-pt6).
|
||||
|
||||
### JIT in development, AOT in production
|
||||
|
||||
@ -472,7 +472,7 @@ It does not need `SystemJS`, so that script is absent from its `index.html`
|
||||
***main.ts***
|
||||
|
||||
JIT and AOT applications boot in much the same way but require different Angular libraries to do so.
|
||||
The key differences, covered in the [Bootstrap](#bootstrap) section above,
|
||||
The key differences, covered in the [Bootstrap](guide/aot-compiler#bootstrap) section above,
|
||||
are evident in these `main` files which can and should reside in the same folder:
|
||||
|
||||
<md-tab-group>
|
||||
@ -615,9 +615,9 @@ The `source-map-explorer` analyzes the source map generated with the bundle and
|
||||
showing exactly which application and Angular modules and classes are included in the bundle.
|
||||
|
||||
Here's the map for _Tour of Heroes_.
|
||||
<a href="/resources/images/cookbooks/aot-compiler/toh6-bundle.png" target="_blank" title="View larger image">
|
||||
<a href="assets/images/cookbooks/aot-compiler/toh6-bundle.png" target="_blank" title="View larger image">
|
||||
<figure class='image-display'>
|
||||
<img src="/resources/images/cookbooks/aot-compiler/toh6-bundle.png" alt="TOH-6-bundle"> </img>
|
||||
<img src="assets/images/cookbooks/aot-compiler/toh6-bundle.png" alt="TOH-6-bundle"> </img>
|
||||
</figure>
|
||||
|
||||
</a>
|
@ -7,17 +7,17 @@ Tell Angular how to construct and bootstrap the app in the root "AppModule".
|
||||
@description
|
||||
An Angular module class describes how the application parts fit together.
|
||||
Every application has at least one Angular module, the _root_ module
|
||||
that you [bootstrap](#main) to launch the application.
|
||||
that you [bootstrap](guide/appmodule#main) to launch the application.
|
||||
You can call it anything you want. The conventional name is `AppModule`.
|
||||
|
||||
The [setup](setup.html) instructions produce a new project with the following minimal `AppModule`.
|
||||
The [setup](guide/setup) instructions produce a new project with the following minimal `AppModule`.
|
||||
You'll evolve this module as your application grows.
|
||||
|
||||
|
||||
{@example 'setup/ts/src/app/app.module.ts'}
|
||||
|
||||
After the `import` statements, you come to a class adorned with the
|
||||
**`@NgModule`** [_decorator_](glossary.html#decorator '"Decorator" explained').
|
||||
**`@NgModule`** [_decorator_](guide/glossary).
|
||||
|
||||
The `@NgModule` decorator identifies `AppModule` as an Angular module class (also called an `NgModule` class).
|
||||
`@NgModule` takes a _metadata_ object that tells Angular how to compile and launch the application.
|
||||
@ -26,7 +26,7 @@ The `@NgModule` decorator identifies `AppModule` as an Angular module class (als
|
||||
* **_declarations_** — the application's lone component, which is also ...
|
||||
* **_bootstrap_** — the _root_ component that Angular creates and inserts into the `index.html` host web page.
|
||||
|
||||
The [Angular Modules (NgModule)](ngmodule.html) guide dives deeply into the details of Angular modules.
|
||||
The [Angular Modules (NgModule)](guide/ngmodule) guide dives deeply into the details of Angular modules.
|
||||
All you need to know at the moment is a few basics about these three properties.
|
||||
|
||||
|
||||
@ -76,7 +76,7 @@ You must declare _every_ component in an `NgModule` class.
|
||||
If you use a component without declaring it, you'll see a clear error message in the browser console.
|
||||
|
||||
You'll learn to create two other kinds of classes —
|
||||
[directives](attribute-directives.html) and [pipes](pipes.html) —
|
||||
[directives](guide/attribute-directives) and [pipes](guide/pipes) —
|
||||
that you must also add to the `declarations` array.
|
||||
|
||||
|
||||
@ -93,7 +93,7 @@ Do not put any other kind of class in `declarations`; _not_ `NgModule` classes,
|
||||
{@a bootstrap-array}
|
||||
### The _bootstrap_ array
|
||||
|
||||
You launch the application by [_bootstrapping_](#main) the root `AppModule`.
|
||||
You launch the application by [_bootstrapping_](guide/appmodule#main) the root `AppModule`.
|
||||
Among other things, the _bootstrapping_ process creates the component(s) listed in the `bootstrap` array
|
||||
and inserts each one into the browser DOM.
|
||||
|
||||
@ -151,7 +151,7 @@ This file is very stable. Once you've set it up, you may never change it again.
|
||||
|
||||
Your initial app has only a single module, the _root_ module.
|
||||
As your app grows, you'll consider subdividing it into multiple "feature" modules,
|
||||
so of which can be loaded later ("lazy loaded") if and when the user chooses
|
||||
some of which can be loaded later ("lazy loaded") if and when the user chooses
|
||||
to visit those features.
|
||||
|
||||
When you're ready to explore these possibilities, visit the [Angular Modules (NgModule)](ngmodule.html) guide.
|
||||
When you're ready to explore these possibilities, visit the [Angular Modules (NgModule)](guide/ngmodule) guide.
|
@ -2,7 +2,7 @@
|
||||
Architecture Overview
|
||||
|
||||
@intro
|
||||
The basic building blocks of Angular applications
|
||||
The basic building blocks of Angular applications.
|
||||
|
||||
@description
|
||||
You write Angular applications by composing HTML *templates* with Angularized markup,
|
||||
@ -17,19 +17,19 @@ Of course, there is more to it than this.
|
||||
You'll learn the details in the pages that follow. For now, focus on the big picture.
|
||||
|
||||
<figure>
|
||||
<img src="/resources/images/devguide/architecture/overview2.png" alt="overview" style="margin-left:-40px;" width="700"> </img>
|
||||
<img src="assets/images/devguide/architecture/overview2.png" alt="overview" style="margin-left:-40px;" width="700"> </img>
|
||||
</figure>
|
||||
|
||||
The architecture diagram identifies the eight main building blocks of an Angular application:
|
||||
|
||||
* [Modules](#modules)
|
||||
* [Components](#components)
|
||||
* [Templates](#templates)
|
||||
* [Metadata](#metadata)
|
||||
* [Data binding](#data-binding)
|
||||
* [Directives](#directives)
|
||||
* [Services](#services)
|
||||
* [Dependency injection](#dependency-injection)
|
||||
* [Modules](guide/architecture#modules)
|
||||
* [Components](guide/architecture#components)
|
||||
* [Templates](guide/architecture#templates)
|
||||
* [Metadata](guide/architecture#metadata)
|
||||
* [Data binding](guide/architecture#data-binding)
|
||||
* [Directives](guide/architecture#directives)
|
||||
* [Services](guide/architecture#services)
|
||||
* [Dependency injection](guide/architecture#dependency-injection)
|
||||
|
||||
Learn these building blocks, and you're on your way.
|
||||
|
||||
@ -41,13 +41,13 @@ Learn these building blocks, and you're on your way.
|
||||
|
||||
## Modules
|
||||
<figure>
|
||||
<img src="/resources/images/devguide/architecture/module.png" alt="Component" align="left" style="width:240px; margin-left:-40px;margin-right:10px"> </img>
|
||||
<img src="assets/images/devguide/architecture/module.png" alt="Component" align="left" style="width:240px; margin-left:-40px;margin-right:10px"> </img>
|
||||
</figure>
|
||||
|
||||
### Angular libraries
|
||||
|
||||
<figure>
|
||||
<img src="/resources/images/devguide/architecture/library-module.png" alt="Component" align="left" style="width:240px; margin-left:-40px;margin-right:10px"> </img>
|
||||
<img src="assets/images/devguide/architecture/library-module.png" alt="Component" align="left" style="width:240px; margin-left:-40px;margin-right:10px"> </img>
|
||||
</figure>
|
||||
|
||||
|
||||
@ -59,7 +59,7 @@ Learn these building blocks, and you're on your way.
|
||||
## Components
|
||||
|
||||
<figure>
|
||||
<img src="/resources/images/devguide/architecture/hero-component.png" alt="Component" align="left" style="width:200px; margin-left:-40px;margin-right:10px"> </img>
|
||||
<img src="assets/images/devguide/architecture/hero-component.png" alt="Component" align="left" style="width:200px; margin-left:-40px;margin-right:10px"> </img>
|
||||
</figure>
|
||||
|
||||
A _component_ controls a patch of screen called a *view*.
|
||||
@ -78,7 +78,7 @@ For example, this `HeroListComponent` has a `heroes` property that returns !{_an
|
||||
that it acquires from a service.
|
||||
`HeroListComponent` also has a `selectHero()` method that sets a `selectedHero` property when the user clicks to choose a hero from that list.
|
||||
Angular creates, updates, and destroys components as the user moves through the application.
|
||||
Your app can take action at each moment in this lifecycle through optional [lifecycle hooks](lifecycle-hooks.html), like `ngOnInit()` declared above.
|
||||
Your app can take action at each moment in this lifecycle through optional [lifecycle hooks](guide/lifecycle-hooks), like `ngOnInit()` declared above.
|
||||
|
||||
<div class='l-hr'>
|
||||
|
||||
@ -87,7 +87,7 @@ Your app can take action at each moment in this lifecycle through optional [life
|
||||
|
||||
## Templates
|
||||
<figure>
|
||||
<img src="/resources/images/devguide/architecture/template.png" alt="Template" align="left" style="width:200px; margin-left:-40px;margin-right:10px"> </img>
|
||||
<img src="assets/images/devguide/architecture/template.png" alt="Template" align="left" style="width:200px; margin-left:-40px;margin-right:10px"> </img>
|
||||
</figure>
|
||||
|
||||
You define a component's view with its companion **template**. A template is a form of HTML
|
||||
@ -99,7 +99,7 @@ template for our `HeroListComponent`:
|
||||
|
||||
{@example 'architecture/ts/src/app/hero-list.component.html'}
|
||||
|
||||
Although this template uses typical HTML elements like `<h2>` and `<p>`, it also has some differences. Code like `*ngFor`, `{{hero.name}}`, `(click)`, `[hero]`, and `<hero-detail>` uses Angular's [template syntax](template-syntax.html).
|
||||
Although this template uses typical HTML elements like `<h2>` and `<p>`, it also has some differences. Code like `*ngFor`, `{{hero.name}}`, `(click)`, `[hero]`, and `<hero-detail>` uses Angular's [template syntax](guide/template-syntax).
|
||||
|
||||
|
||||
In the last line of the template, the `<hero-detail>` tag is a custom element that represents a new component, `HeroDetailComponent`.
|
||||
@ -110,7 +110,7 @@ hero that the user selects from the list presented by the `HeroListComponent`.
|
||||
The `HeroDetailComponent` is a **child** of the `HeroListComponent`.
|
||||
|
||||
<figure>
|
||||
<img src="/resources/images/devguide/architecture/component-tree.png" alt="Metadata" align="left" style="width:300px; margin-left:-40px;margin-right:10px"> </img>
|
||||
<img src="assets/images/devguide/architecture/component-tree.png" alt="Metadata" align="left" style="width:300px; margin-left:-40px;margin-right:10px"> </img>
|
||||
</figure>
|
||||
|
||||
Notice how `<hero-detail>` rests comfortably among native HTML elements. Custom components mix seamlessly with native HTML in the same layouts.
|
||||
@ -122,11 +122,11 @@ Notice how `<hero-detail>` rests comfortably among native HTML elements. Custom
|
||||
|
||||
## Metadata
|
||||
<figure>
|
||||
<img src="/resources/images/devguide/architecture/metadata.png" alt="Metadata" align="left" style="width:150px; margin-left:-40px;margin-right:10px"> </img>
|
||||
<img src="assets/images/devguide/architecture/metadata.png" alt="Metadata" align="left" style="width:150px; margin-left:-40px;margin-right:10px"> </img>
|
||||
</figure>
|
||||
|
||||
<p style="padding-top:10px">Metadata tells Angular how to process a class.</p>
|
||||
<br class="l-clear-both">[Looking back at the code](#component-code) for `HeroListComponent`, you can see that it's just a class.
|
||||
<br class="l-clear-both">[Looking back at the code](guide/architecture#component-code) for `HeroListComponent`, you can see that it's just a class.
|
||||
There is no evidence of a framework, no "Angular" in it at all.
|
||||
|
||||
In fact, `HeroListComponent` really is *just a class*. It's not a component until you *tell Angular about it*.
|
||||
@ -144,13 +144,13 @@ where it finds a `<hero-list>` tag in *parent* HTML.
|
||||
For example, if an app's HTML contains `<hero-list></hero-list>`, then
|
||||
Angular inserts an instance of the `HeroListComponent` view between those tags.
|
||||
|
||||
- `templateUrl`: module-relative address of this component's HTML template, shown [above](#templates).
|
||||
- `templateUrl`: module-relative address of this component's HTML template, shown [above](guide/architecture#templates).
|
||||
- `providers`: !{_array} of **dependency injection providers** for services that the component requires.
|
||||
This is one way to tell Angular that the component's constructor requires a `HeroService`
|
||||
so it can get the list of heroes to display.
|
||||
|
||||
<figure>
|
||||
<img src="/resources/images/devguide/architecture/template-metadata-component.png" alt="Metadata" align="left" style="height:200px; margin-left:-40px;margin-right:10px"> </img>
|
||||
<img src="assets/images/devguide/architecture/template-metadata-component.png" alt="Metadata" align="left" style="height:200px; margin-left:-40px;margin-right:10px"> </img>
|
||||
</figure>
|
||||
|
||||
The metadata in the `@Component` tells Angular where to get the major building blocks you specify for the component.
|
||||
@ -171,21 +171,21 @@ Without a framework, you would be responsible for pushing data values into the H
|
||||
into actions and value updates. Writing such push/pull logic by hand is tedious, error-prone, and a nightmare to
|
||||
read as any experienced jQuery programmer can attest.
|
||||
<figure>
|
||||
<img src="/resources/images/devguide/architecture/databinding.png" alt="Data Binding" style="width:220px; float:left; margin-left:-40px;margin-right:20px"> </img>
|
||||
<img src="assets/images/devguide/architecture/databinding.png" alt="Data Binding" style="width:220px; float:left; margin-left:-40px;margin-right:20px"> </img>
|
||||
</figure>
|
||||
|
||||
Angular supports **data binding**,
|
||||
a mechanism for coordinating parts of a template with parts of a component.
|
||||
Add binding markup to the template HTML to tell Angular how to connect both sides.
|
||||
|
||||
As the diagram shows, there are four forms of data binding syntax. Each form has a direction — to the DOM, from the DOM, or in both directions.<br class="l-clear-both">The `HeroListComponent` [example](#templates) template has three forms:
|
||||
* The `{{hero.name}}` [*interpolation*](displaying-data.html#interpolation)
|
||||
As the diagram shows, there are four forms of data binding syntax. Each form has a direction — to the DOM, from the DOM, or in both directions.<br class="l-clear-both">The `HeroListComponent` [example](guide/architecture#templates) template has three forms:
|
||||
* The `{{hero.name}}` [*interpolation*](guide/displaying-data)
|
||||
displays the component's `hero.name` property value within the `<li>` element.
|
||||
|
||||
* The `[hero]` [*property binding*](template-syntax.html#property-binding) passes the value of `selectedHero` from
|
||||
* The `[hero]` [*property binding*](guide/template-syntax) passes the value of `selectedHero` from
|
||||
the parent `HeroListComponent` to the `hero` property of the child `HeroDetailComponent`.
|
||||
|
||||
* The `(click)` [*event binding*](user-input.html#click) calls the component's `selectHero` method when the user clicks a hero's name.
|
||||
* The `(click)` [*event binding*](guide/user-input) calls the component's `selectHero` method when the user clicks a hero's name.
|
||||
|
||||
**Two-way data binding** is an important fourth form
|
||||
that combines property and event binding in a single notation, using the `ngModel` directive.
|
||||
@ -198,13 +198,13 @@ Angular processes *all* data bindings once per JavaScript event cycle,
|
||||
from the root of the application component tree through all child components.
|
||||
|
||||
<figure>
|
||||
<img src="/resources/images/devguide/architecture/component-databinding.png" alt="Data Binding" style="float:left; width:300px; margin-left:-40px;margin-right:10px"> </img>
|
||||
<img src="assets/images/devguide/architecture/component-databinding.png" alt="Data Binding" style="float:left; width:300px; margin-left:-40px;margin-right:10px"> </img>
|
||||
</figure>
|
||||
|
||||
Data binding plays an important role in communication
|
||||
between a template and its component.<br class="l-clear-both">
|
||||
<figure>
|
||||
<img src="/resources/images/devguide/architecture/parent-child-binding.png" alt="Parent/Child binding" style="float:left; width:300px; margin-left:-40px;margin-right:10px"> </img>
|
||||
<img src="assets/images/devguide/architecture/parent-child-binding.png" alt="Parent/Child binding" style="float:left; width:300px; margin-left:-40px;margin-right:10px"> </img>
|
||||
</figure>
|
||||
|
||||
Data binding is also important for communication between parent and child components.<br class="l-clear-both">
|
||||
@ -215,7 +215,7 @@ Data binding is also important for communication between parent and child compon
|
||||
|
||||
## Directives
|
||||
<figure>
|
||||
<img src="/resources/images/devguide/architecture/directive.png" alt="Parent child" style="float:left; width:150px; margin-left:-40px;margin-right:10px"> </img>
|
||||
<img src="assets/images/devguide/architecture/directive.png" alt="Parent child" style="float:left; width:150px; margin-left:-40px;margin-right:10px"> </img>
|
||||
</figure>
|
||||
|
||||
Angular templates are *dynamic*. When Angular renders them, it transforms the DOM
|
||||
@ -234,9 +234,9 @@ sometimes by name but more often as the target of an assignment or a binding.
|
||||
|
||||
**Structural** directives alter layout by adding, removing, and replacing elements in DOM.
|
||||
|
||||
The [example template](#templates) uses two built-in structural directives:
|
||||
* [`*ngFor`](displaying-data.html#ngFor) tells Angular to stamp out one `<li>` per hero in the `heroes` list.
|
||||
* [`*ngIf`](displaying-data.html#ngIf) includes the `HeroDetail` component only if a selected hero exists.
|
||||
The [example template](guide/architecture#templates) uses two built-in structural directives:
|
||||
* [`*ngFor`](guide/displaying-data) tells Angular to stamp out one `<li>` per hero in the `heroes` list.
|
||||
* [`*ngIf`](guide/displaying-data) includes the `HeroDetail` component only if a selected hero exists.
|
||||
**Attribute** directives alter the appearance or behavior of an existing element.
|
||||
In templates they look like regular HTML attributes, hence the name.
|
||||
|
||||
@ -245,9 +245,9 @@ an example of an attribute directive. `ngModel` modifies the behavior of
|
||||
an existing element (typically an `<input>`)
|
||||
by setting its display value property and responding to change events.
|
||||
Angular has a few more directives that either alter the layout structure
|
||||
(for example, [ngSwitch](template-syntax.html#ngSwitch))
|
||||
(for example, [ngSwitch](guide/template-syntax))
|
||||
or modify aspects of DOM elements and components
|
||||
(for example, [ngStyle](template-syntax.html#ngStyle) and [ngClass](template-syntax.html#ngClass)).
|
||||
(for example, [ngStyle](guide/template-syntax) and [ngClass](guide/template-syntax)).
|
||||
|
||||
Of course, you can also write your own directives. Components such as
|
||||
`HeroListComponent` are one kind of custom directive.
|
||||
@ -260,7 +260,7 @@ Of course, you can also write your own directives. Components such as
|
||||
|
||||
## Services
|
||||
<figure>
|
||||
<img src="/resources/images/devguide/architecture/service.png" alt="Service" style="float:left; margin-left:-40px;margin-right:10px"> </img>
|
||||
<img src="assets/images/devguide/architecture/service.png" alt="Service" style="float:left; margin-left:-40px;margin-right:10px"> </img>
|
||||
</figure>
|
||||
|
||||
_Service_ is a broad category encompassing any value, function, or feature that your application needs.
|
||||
@ -305,7 +305,7 @@ application logic into services and make those services available to components
|
||||
|
||||
## Dependency injection
|
||||
<figure>
|
||||
<img src="/resources/images/devguide/architecture/dependency-injection.png" alt="Service" style="float:left; width:200px; margin-left:-40px;margin-right:10px"> </img>
|
||||
<img src="assets/images/devguide/architecture/dependency-injection.png" alt="Service" style="float:left; width:200px; margin-left:-40px;margin-right:10px"> </img>
|
||||
</figure>
|
||||
|
||||
_Dependency injection_ is a way to supply a new instance of a class
|
||||
@ -324,7 +324,7 @@ This is *dependency injection*.
|
||||
|
||||
The process of `HeroService` injection looks a bit like this:
|
||||
<figure>
|
||||
<img src="/resources/images/devguide/architecture/injector-injects.png" alt="Service"> </img>
|
||||
<img src="assets/images/devguide/architecture/injector-injects.png" alt="Service"> </img>
|
||||
</figure>
|
||||
|
||||
If the injector doesn't have a `HeroService`, how does it know how to make one?
|
||||
@ -336,7 +336,7 @@ Registering at a component level means you get a new instance of the
|
||||
service with each new instance of that component.
|
||||
|
||||
<!-- We've vastly oversimplified dependency injection for this overview.
|
||||
The full story is in the [dependency injection](dependency-injection.html) page. -->
|
||||
The full story is in the [dependency injection](guide/dependency-injection) page. -->
|
||||
|
||||
Points to remember about dependency injection:
|
||||
|
||||
@ -359,14 +359,14 @@ Points to remember about dependency injection:
|
||||
|
||||
You've learned the basics about the eight main building blocks of an Angular application:
|
||||
|
||||
* [Modules](#modules)
|
||||
* [Components](#components)
|
||||
* [Templates](#templates)
|
||||
* [Metadata](#metadata)
|
||||
* [Data binding](#data-binding)
|
||||
* [Directives](#directives)
|
||||
* [Services](#services)
|
||||
* [Dependency injection](#dependency-injection)
|
||||
* [Modules](guide/architecture#modules)
|
||||
* [Components](guide/architecture#components)
|
||||
* [Templates](guide/architecture#templates)
|
||||
* [Metadata](guide/architecture#metadata)
|
||||
* [Data binding](guide/architecture#data-binding)
|
||||
* [Directives](guide/architecture#directives)
|
||||
* [Services](guide/architecture#services)
|
||||
* [Dependency injection](guide/architecture#dependency-injection)
|
||||
|
||||
That's a foundation for everything else in an Angular application,
|
||||
and it's more than enough to get going.
|
||||
@ -375,7 +375,7 @@ But it doesn't include everything you need to know.
|
||||
Here is a brief, alphabetical list of other important Angular features and services.
|
||||
Most of them are covered in this documentation (or soon will be).
|
||||
|
||||
> [**Animations**](animations.html): Animate component behavior
|
||||
> [**Animations**](guide/animations): Animate component behavior
|
||||
without deep knowledge of animation techniques or CSS with Angular's animation library.
|
||||
|
||||
> **Change detection**: The change detection documentation will cover how Angular decides that a component property value has changed,
|
||||
@ -384,18 +384,18 @@ when to update the screen, and how it uses **zones** to intercept asynchronous a
|
||||
> **Events**: The events documentation will cover how to use components and services to raise events with mechanisms for
|
||||
publishing and subscribing to events.
|
||||
|
||||
> [**Forms**](forms.html): Support complex data entry scenarios with HTML-based validation and dirty checking.
|
||||
> [**Forms**](guide/forms): Support complex data entry scenarios with HTML-based validation and dirty checking.
|
||||
|
||||
> [**HTTP**](server-communication.html): Communicate with a server to get data, save data, and invoke server-side actions with an HTTP client.
|
||||
> [**HTTP**](guide/server-communication): Communicate with a server to get data, save data, and invoke server-side actions with an HTTP client.
|
||||
|
||||
> [**Lifecycle hooks**](lifecycle-hooks.html): Tap into key moments in the lifetime of a component, from its creation to its destruction,
|
||||
> [**Lifecycle hooks**](guide/lifecycle-hooks): Tap into key moments in the lifetime of a component, from its creation to its destruction,
|
||||
by implementing the lifecycle hook interfaces.
|
||||
|
||||
> [**Pipes**](pipes.html): Use pipes in your templates to improve the user experience by transforming values for display. Consider this `currency` pipe expression:
|
||||
> [**Pipes**](guide/pipes): Use pipes in your templates to improve the user experience by transforming values for display. Consider this `currency` pipe expression:
|
||||
>
|
||||
> > `price | currency:'USD':true`
|
||||
>
|
||||
> It displays a price of 42.33 as `$42.33`.
|
||||
|
||||
> [**Router**](router.html): Navigate from page to page within the client
|
||||
> [**Router**](guide/router): Navigate from page to page within the client
|
||||
application and never leave the browser.
|
||||
|
@ -9,19 +9,15 @@ An **Attribute** directive changes the appearance or behavior of a DOM element.
|
||||
|
||||
# Contents
|
||||
|
||||
|
||||
* [Directives overview](#directive-overview)
|
||||
* [Build a simple attribute directive](#write-directive)
|
||||
* [Apply the attribute directive to an element in a template](#apply-directive)
|
||||
* [Respond to user-initiated events](#respond-to-user)
|
||||
* [Pass values into the directive with an _@Input_ data binding](#bindings)
|
||||
* [Bind to a second property](#second-property)
|
||||
* [Directives overview](guide/attribute-directives#directive-overview)
|
||||
* [Build a simple attribute directive](guide/attribute-directives#write-directive)
|
||||
* [Apply the attribute directive to an element in a template](guide/attribute-directives#apply-directive)
|
||||
* [Respond to user-initiated events](guide/attribute-directives#respond-to-user)
|
||||
* [Pass values into the directive with an _@Input_ data binding](guide/attribute-directives#bindings)
|
||||
* [Bind to a second property](guide/attribute-directives#second-property)
|
||||
|
||||
Try the <live-example title="Attribute Directive example"></live-example>.
|
||||
|
||||
|
||||
|
||||
{@a directive-overview}
|
||||
## Directives overview
|
||||
|
||||
There are three kinds of directives in Angular:
|
||||
@ -31,20 +27,19 @@ There are three kinds of directives in Angular:
|
||||
1. Attribute directives—change the appearance or behavior of an element, component, or another directive.
|
||||
|
||||
*Components* are the most common of the three directives.
|
||||
You saw a component for the first time in the [QuickStart](../quickstart.html) guide.
|
||||
You saw a component for the first time in the [QuickStart](quickstart) guide.
|
||||
|
||||
*Structural Directives* change the structure of the view.
|
||||
Two examples are [NgFor](template-syntax.html#ngFor) and [NgIf](template-syntax.html#ngIf).
|
||||
Learn about them in the [Structural Directives](structural-directives.html) guide.
|
||||
Two examples are [NgFor](guide/template-syntax) and [NgIf](guide/template-syntax).
|
||||
Learn about them in the [Structural Directives](guide/structural-directives) guide.
|
||||
|
||||
*Attribute directives* are used as attributes of elements.
|
||||
The built-in [NgStyle](template-syntax.html#ngStyle) directive in the [Template Syntax](template-syntax.html) guide, for example,
|
||||
The built-in [NgStyle](guide/template-syntax) directive in the
|
||||
[Template Syntax](guide/template-syntax) guide, for example,
|
||||
can change several element styles at the same time.
|
||||
|
||||
|
||||
|
||||
{@a write-directive}
|
||||
## Build a simple attribute directive
|
||||
|
||||
An attribute directive minimally requires building a controller class annotated with
|
||||
`@Directive`, which specifies the selector that identifies
|
||||
the attribute.
|
||||
@ -53,35 +48,45 @@ The controller class implements the desired directive behavior.
|
||||
This page demonstrates building a simple _myHighlight_ attribute
|
||||
directive to set an element's background color
|
||||
when the user hovers over that element. You can apply it like this:
|
||||
|
||||
|
||||
{@example 'attribute-directives/ts/src/app/app.component.1.html' region='applied'}
|
||||
|
||||
### Write the directive code
|
||||
Follow the [setup](setup.html) instructions for creating a new local project
|
||||
|
||||
Follow the [setup](guide/setup) instructions for creating a new local project
|
||||
named <span ngio-ex>attribute-directives</span>.
|
||||
Create the following source file in `src/app` with the following code:
|
||||
|
||||
Create the following source file in the indicated folder:
|
||||
|
||||
|
||||
{@example 'attribute-directives/ts/src/app/highlight.directive.1.ts'}
|
||||
|
||||
The `import` statement specifies symbols from the Angular `core`:
|
||||
|
||||
1. `Directive` provides the functionality of the `@Directive` decorator.
|
||||
1. `ElementRef` [injects](guide/dependency-injection) into the directive's constructor
|
||||
so the code can access the DOM element.
|
||||
1. `Input` allows data to flow from the binding expression into the directive.
|
||||
|
||||
Next, the `@Directive` decorator function contains the directive metadata in a configuration object
|
||||
as an argument.
|
||||
`@Directive` requires a CSS selector to identify
|
||||
the HTML in the template that is associated with the directive.
|
||||
The [CSS selector for an attribute](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors)
|
||||
is the attribute name in square brackets.
|
||||
Here, the directive's selector is `[myHighlight]`.
|
||||
Angular locates all elements in the template that have an attribute named `myHighlight`.
|
||||
|
||||
### Why not call it "highlight"?
|
||||
|
||||
Though *highlight* is a more concise name than *myHighlight* and would work,
|
||||
a best practice is to prefix selector names to ensure
|
||||
they don't conflict with standard HTML attributes.
|
||||
they don't conflict with standard HTML attributes.
|
||||
This also reduces the risk of colliding with third-party directive names.
|
||||
|
||||
Make sure you do **not** prefix the `highlight` directive name with **`ng`** because
|
||||
that prefix is reserved for Angular and using it could cause bugs that are difficult to diagnose. For a simple demo, the short prefix, `my`, helps distinguish your custom directive.
|
||||
|
||||
<p>
|
||||
After the <code> @Directive </code> metadata comes the directive's controller class, called <code> HighlightDirective </code> , which contains the logic for the directive.
|
||||
</p>
|
||||
that prefix is reserved for Angular and using it could cause bugs that are difficult to diagnose.
|
||||
For a simple demo, the short prefix, `my`, helps distinguish your custom directive.
|
||||
After the `@Directive` metadata comes the directive's controller class,
|
||||
called `HighlightDirective`, which contains the logic for the directive.
|
||||
<span if-docs="ts">Exporting `HighlightDirective` makes it accessible to other components.</span>
|
||||
|
||||
Angular creates a new instance of the directive's controller class for
|
||||
each matching element, injecting an Angular `ElementRef`
|
||||
@ -89,42 +94,43 @@ into the constructor.
|
||||
`ElementRef` is a service that grants direct access to the DOM element
|
||||
through its `nativeElement` property.
|
||||
|
||||
|
||||
|
||||
{@a apply-directive}
|
||||
## Apply the attribute directive
|
||||
|
||||
To use the new `HighlightDirective`, create a template that
|
||||
applies the directive as an attribute to a paragraph (`<p>`) element.
|
||||
In Angular terms, the `<p>` element is the attribute **host**.
|
||||
<p>
|
||||
Put the template in its own <code> </code> file that looks like this:
|
||||
</p>
|
||||
|
||||
Put the template in its own <span ngio-ex>app.component.html</span>
|
||||
file that looks like this:
|
||||
|
||||
|
||||
{@example 'attribute-directives/ts/src/app/app.component.1.html'}
|
||||
|
||||
Now reference this template in the `AppComponent`:
|
||||
|
||||
|
||||
{@example 'attribute-directives/ts/src/app/app.component.ts'}
|
||||
|
||||
Next, add an `import` statement to fetch the `Highlight` directive and
|
||||
add that class to the `declarations` NgModule metadata. This way Angular
|
||||
recognizes the directive when it encounters `myHighlight` in the template.
|
||||
|
||||
|
||||
{@example 'attribute-directives/ts/src/app/app.module.ts'}
|
||||
|
||||
Now when the app runs, the `myHighlight` directive highlights the paragraph text.
|
||||
|
||||
<figure class='image-display'>
|
||||
<img src="/resources/images/devguide/attribute-directives/first-highlight.png" alt="First Highlight"> </img>
|
||||
<img src="assets/images/devguide/attribute-directives/first-highlight.png" alt="First Highlight"> </img>
|
||||
</figure>
|
||||
|
||||
|
||||
### Your directive isn't working?
|
||||
|
||||
Did you remember to add the directive to the `declarations` attribute of `@NgModule`? It is easy to forget!
|
||||
Did you remember to add the directive to the `declarations` attribute of `@NgModule`?
|
||||
It is easy to forget!
|
||||
Open the console in the browser tools and look for an error like this:
|
||||
|
||||
<code-example format="nocode">
|
||||
EXCEPTION: Template parse errors:
|
||||
Can't bind to 'myHighlight' since it isn't a known property of 'p'.
|
||||
@ -140,9 +146,6 @@ It created an instance of the `HighlightDirective` class and
|
||||
injected a reference to the `<p>` element into the directive's constructor
|
||||
which sets the `<p>` element's background style to yellow.
|
||||
|
||||
|
||||
|
||||
{@a respond-to-user}
|
||||
## Respond to user-initiated events
|
||||
|
||||
Currently, `myHighlight` simply sets an element color.
|
||||
@ -152,14 +155,10 @@ and respond by setting or clearing the highlight color.
|
||||
|
||||
Begin by adding `HostListener` to the list of imported symbols;
|
||||
add the `Input` symbol as well because you'll need it soon.
|
||||
|
||||
{@example 'attribute-directives/ts/src/app/highlight.directive.ts' region='imports'}
|
||||
|
||||
Then add two eventhandlers that respond when the mouse enters or leaves, each adorned by the `HostListener` !{_decorator}.
|
||||
|
||||
{@example 'attribute-directives/ts/src/app/highlight.directive.2.ts' region='mouse-methods'}
|
||||
|
||||
The `@HostListener` !{_decorator} lets you subscribe to events of the DOM element that hosts an attribute directive, the `<p>` in this case.
|
||||
Then add two eventhandlers that respond when the mouse enters or leaves,
|
||||
each adorned by the `HostListener` !{_decorator}.
|
||||
The `@HostListener` !{_decorator} lets you subscribe to events of the DOM
|
||||
element that hosts an attribute directive, the `<p>` in this case.
|
||||
|
||||
Of course you could reach into the DOM with standard JavaScript and and attach event listeners manually.
|
||||
There are at least three problems with _that_ approach:
|
||||
@ -169,24 +168,19 @@ There are at least three problems with _that_ approach:
|
||||
1. Talking to DOM API directly isn't a best practice.
|
||||
The handlers delegate to a helper method that sets the color on the DOM element, `#{_priv}el`,
|
||||
which you declare and initialize in the constructor.
|
||||
|
||||
|
||||
{@example 'attribute-directives/ts/src/app/highlight.directive.2.ts' region='ctor'}
|
||||
|
||||
Here's the updated directive in full:
|
||||
|
||||
|
||||
{@example 'attribute-directives/ts/src/app/highlight.directive.2.ts'}
|
||||
|
||||
Run the app and confirm that the background color appears when the mouse hovers over the `p` and
|
||||
disappears as it moves out.
|
||||
Run the app and confirm that the background color appears when
|
||||
the mouse hovers over the `p` and disappears as it moves out.
|
||||
|
||||
<figure class='image-display'>
|
||||
<img src="/resources/images/devguide/attribute-directives/highlight-directive-anim.gif" alt="Second Highlight"> </img>
|
||||
<img src="assets/images/devguide/attribute-directives/highlight-directive-anim.gif" alt="Second Highlight"> </img>
|
||||
</figure>
|
||||
|
||||
|
||||
|
||||
|
||||
{@a bindings}
|
||||
## Pass values into the directive with an _@Input_ data binding
|
||||
|
||||
Currently the highlight color is hard-coded _within_ the directive. That's inflexible.
|
||||
@ -194,9 +188,6 @@ In this section, you give the developer the power to set the highlight color whi
|
||||
|
||||
Start by adding a `highlightColor` property to the directive class like this:
|
||||
|
||||
{@example 'attribute-directives/ts/src/app/highlight.directive.2.ts' region='color'}
|
||||
|
||||
|
||||
|
||||
{@a input}
|
||||
### Binding to an _@Input_ property
|
||||
@ -204,33 +195,18 @@ Start by adding a `highlightColor` property to the directive class like this:
|
||||
Notice the `@Input` !{_decorator}. It adds metadata to the class that makes the directive's `highlightColor` property available for binding.
|
||||
|
||||
It's called an *input* property because data flows from the binding expression _into_ the directive.
|
||||
Without that input metadata, Angular rejects the binding; see [below](#why-input "Why add @Input?") for more about that.
|
||||
Without that input metadata, Angular rejects the binding; see [below](guide/attribute-directives#why-input "Why add @Input?") for more about that.
|
||||
|
||||
Try it by adding the following directive binding variations to the `AppComponent` template:
|
||||
|
||||
{@example 'attribute-directives/ts/src/app/app.component.1.html' region='color-1'}
|
||||
|
||||
Add a `color` property to the `AppComponent`.
|
||||
|
||||
{@example 'attribute-directives/ts/src/app/app.component.1.ts' region='class'}
|
||||
|
||||
Let it control the highlight color with a property binding.
|
||||
|
||||
{@example 'attribute-directives/ts/src/app/app.component.1.html' region='color-2'}
|
||||
|
||||
That's good, but it would be nice to _simultaneously_ apply the directive and set the color _in the same attribute_ like this.
|
||||
|
||||
{@example 'attribute-directives/ts/src/app/app.component.html' region='color'}
|
||||
|
||||
The `[myHighlight]` attribute binding both applies the highlighting directive to the `<p>` element
|
||||
and sets the directive's highlight color with a property binding.
|
||||
You're re-using the directive's attribute selector (`[myHighlight]`) to do both jobs.
|
||||
That's a crisp, compact syntax.
|
||||
|
||||
You'll have to rename the directive's `highlightColor` property to `myHighlight` because that's now the color property binding name.
|
||||
|
||||
{@example 'attribute-directives/ts/src/app/highlight.directive.2.ts' region='color-2'}
|
||||
|
||||
This is disagreeable. The word, `myHighlight`, is a terrible property name and it doesn't convey the property's intent.
|
||||
|
||||
|
||||
@ -244,64 +220,58 @@ _Inside_ the directive the property is known as `highlightColor`.
|
||||
_Outside_ the directive, where you bind to it, it's known as `myHighlight`.
|
||||
|
||||
You get the best of both worlds: the property name you want and the binding syntax you want:
|
||||
|
||||
{@example 'attribute-directives/ts/src/app/app.component.html' region='color'}
|
||||
|
||||
Now that you're binding to `highlightColor`, modify the `onMouseEnter()` method to use it.
|
||||
If someone neglects to bind to `highlightColor`, highlight in "red" by default.
|
||||
If someone neglects to bind to `highlightColor`, highlight in red:
|
||||
Here's the latest version of the directive class.
|
||||
## Write a harness to try it
|
||||
|
||||
|
||||
{@example 'attribute-directives/ts/src/app/highlight.directive.3.ts' region='mouse-enter'}
|
||||
|
||||
Here's the latest version of the directive class.## Write a harness to try itIt may be difficult to imagine how this directive actually works.
|
||||
It may be difficult to imagine how this directive actually works.
|
||||
In this section, you'll turn `AppComponent` into a harness that
|
||||
lets you pick the highlight color with a radio button and bind your color choice to the directive.
|
||||
|
||||
Update `app.component.html` as follows:
|
||||
Revise the `AppComponent.color` so that it has no initial value.Here is the harness and directive in action.
|
||||
Update <span ngio-ex>app.component.html</span> as follows:
|
||||
Revise the `AppComponent.color` so that it has no initial value.
|
||||
Here are the harness and directive in action.
|
||||
|
||||
<figure class='image-display'>
|
||||
<img src="/resources/images/devguide/attribute-directives/highlight-directive-v2-anim.gif" alt="Highlight v.2"> </img>
|
||||
<img src="assets/images/devguide/attribute-directives/highlight-directive-v2-anim.gif" alt="Highlight v.2"> </img>
|
||||
</figure>
|
||||
|
||||
|
||||
|
||||
|
||||
{@a second-property}
|
||||
## Bind to a second property
|
||||
|
||||
This highlight directive has a single customizable property. In a real app, it may need more.
|
||||
|
||||
At the moment, the default color—the color that prevails until
|
||||
the user picks a highlight color—is hard-coded as "red".
|
||||
At the moment, the default color—the color that prevails until
|
||||
the user picks a highlight color—is hard-coded as "red".
|
||||
Let the template developer set the default color.
|
||||
|
||||
Add a second **input** property to `HighlightDirective` called `defaultColor`:Revise the directive's `onMouseEnter` so that it first tries to highlight with the `highlightColor`,
|
||||
Add a second **input** property to `HighlightDirective` called `defaultColor`:
|
||||
Revise the directive's `onMouseEnter` so that it first tries to highlight with the `highlightColor`,
|
||||
then with the `defaultColor`, and falls back to "red" if both properties are undefined.
|
||||
|
||||
{@example 'attribute-directives/ts/src/app/highlight.directive.ts' region='mouse-enter'}
|
||||
|
||||
How do you bind to a second property when you're already binding to the `myHighlight` attribute name?
|
||||
|
||||
As with components, you can add as many directive property bindings as you need by stringing them along in the template.
|
||||
The developer should be able to write the following template HTML to both bind to the `AppComponent.color`
|
||||
and fall back to "violet" as the default color.
|
||||
|
||||
{@example 'attribute-directives/ts/src/app/app.component.html' region='defaultColor'}
|
||||
|
||||
Angular knows that the `defaultColor` binding belongs to the `HighlightDirective`
|
||||
because you made it _public_ with the `@Input` !{_decorator}.
|
||||
|
||||
Here's how the harness should work when you're done coding.
|
||||
|
||||
<figure class='image-display'>
|
||||
<img src="/resources/images/devguide/attribute-directives/highlight-directive-final-anim.gif" alt="Final Highlight"> </img>
|
||||
<img src="assets/images/devguide/attribute-directives/highlight-directive-final-anim.gif" alt="Final Highlight"> </img>
|
||||
</figure>
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
This page covered how to:
|
||||
- [Build an **attribute directive**](#write-directive) that modifies the behavior of an element.
|
||||
- [Apply the directive](#apply-directive) to an element in a template.
|
||||
- [Respond to **events**](#respond-to-user) that change the directive's behavior.
|
||||
- [**Bind** values to the directive](#bindings).
|
||||
|
||||
- [Build an **attribute directive**](guide/attribute-directives#write-directive) that modifies the behavior of an element.
|
||||
- [Apply the directive](guide/attribute-directives#apply-directive) to an element in a template.
|
||||
- [Respond to **events**](guide/attribute-directives#respond-to-user) that change the directive's behavior.
|
||||
- [**Bind** values to the directive](guide/attribute-directives#bindings).
|
||||
|
||||
The final source code follows:
|
||||
|
||||
@ -341,20 +311,11 @@ The final source code follows:
|
||||
|
||||
You can also experience and download the <live-example title="Attribute Directive example"></live-example>.
|
||||
|
||||
|
||||
{@a why-input}
|
||||
|
||||
### Appendix: Why add _@Input_?
|
||||
|
||||
In this demo, the `hightlightColor` property is an ***input*** property of
|
||||
the `HighlightDirective`. You've seen it applied without an alias:
|
||||
|
||||
{@example 'attribute-directives/ts/src/app/highlight.directive.2.ts' region='color'}
|
||||
|
||||
You've seen it with an alias:
|
||||
|
||||
{@example 'attribute-directives/ts/src/app/highlight.directive.ts' region='color'}
|
||||
|
||||
Either way, the `@Input` !{_decorator} tells Angular that this property is
|
||||
_public_ and available for binding by a parent component.
|
||||
Without `@Input`, Angular refuses to bind to the property.
|
||||
@ -377,20 +338,17 @@ Only then can it be bound by some other component or directive.
|
||||
You can tell if `@Input` is needed by the position of the property name in a binding.
|
||||
|
||||
* When it appears in the template expression to the ***right*** of the equals (=),
|
||||
it belongs to the template's component and does not require the `@Input` !{_decorator}.
|
||||
it belongs to the template's component and does not require the `@Input` !{_decorator}.
|
||||
|
||||
* When it appears in **square brackets** ([ ]) to the **left** of the equals (=),
|
||||
the property belongs to some _other_ component or directive;
|
||||
that property must be adorned with the `@Input` !{_decorator}.
|
||||
the property belongs to some _other_ component or directive;
|
||||
that property must be adorned with the `@Input` !{_decorator}.
|
||||
|
||||
Now apply that reasoning to the following example:
|
||||
|
||||
{@example 'attribute-directives/ts/src/app/app.component.html' region='color'}
|
||||
|
||||
* The `color` property in the expression on the right belongs to the template's component.
|
||||
The template and its component trust each other.
|
||||
The `color` property doesn't require the `@Input` !{_decorator}.
|
||||
The template and its component trust each other.
|
||||
The `color` property doesn't require the `@Input` !{_decorator}.
|
||||
|
||||
* The `myHighlight` property on the left refers to an _aliased_ property of the `MyHighlightDirective`,
|
||||
not a property of the template's component. There are trust issues.
|
||||
Therefore, the directive property must carry the `@Input` !{_decorator}.
|
||||
* The `myHighlight` property on the left refers to an _aliased_ property of the `HighlightDirective`,
|
||||
not a property of the template's component. There are trust issues.
|
||||
Therefore, the directive property must carry the `@Input` !{_decorator}.
|
@ -244,7 +244,7 @@ using <a href="https://saucelabs.com/" target="_blank">SauceLabs</a> and
|
||||
Angular is built on the latest standards of the web platform.
|
||||
Targeting such a wide range of browsers is challenging because they do not support all features of modern browsers.
|
||||
|
||||
You compensate by loading polyfill scripts ("polyfills") on the host web page (`index.html`)
|
||||
You can compensate by loading polyfill scripts ("polyfills") on the host web page (`index.html`)
|
||||
that implement missing features in JavaScript.
|
||||
|
||||
{@example 'quickstart/ts/src/index.html' region='polyfills'}
|
||||
@ -252,12 +252,12 @@ that implement missing features in JavaScript.
|
||||
A particular browser may require at least one polyfill to run _any_ Angular application.
|
||||
You may need additional polyfills for specific features.
|
||||
|
||||
The tables below will help you determine which polyfills to load, depending on the browsers you target and the features you use.
|
||||
The tables below can help you determine which polyfills to load, depending on the browsers you target and the features you use.
|
||||
|
||||
|
||||
~~~ {.alert.is-important}
|
||||
|
||||
The suggested polyfills are the ones we know will run full Angular applications.
|
||||
The suggested polyfills are the ones that run full Angular applications.
|
||||
You may need additional polyfills to support features not covered by this list.
|
||||
Note that polyfills cannot magically transform an old, slow browser into a modern, fast one.
|
||||
|
||||
@ -307,7 +307,7 @@ These are the polyfills required to run an Angular application on each supported
|
||||
|
||||
|
||||
<td>
|
||||
[ES6](#core-es6)
|
||||
[ES6](guide/browser-support#core-es6)
|
||||
</td>
|
||||
|
||||
|
||||
@ -322,7 +322,7 @@ These are the polyfills required to run an Angular application on each supported
|
||||
|
||||
|
||||
<td>
|
||||
[ES6<br>classList](#classlist)
|
||||
[ES6<br>classList](guide/browser-support#classlist)
|
||||
|
||||
</td>
|
||||
|
||||
@ -370,7 +370,7 @@ Here are the features which may require additional polyfills:
|
||||
|
||||
|
||||
<td>
|
||||
[Web Animations](#web-animations)
|
||||
[Web Animations](guide/browser-support#web-animations)
|
||||
</td>
|
||||
|
||||
|
||||
@ -390,7 +390,7 @@ Here are the features which may require additional polyfills:
|
||||
|
||||
|
||||
<td>
|
||||
[Intl API](#intl)
|
||||
[Intl API](guide/browser-support#intl)
|
||||
</td>
|
||||
|
||||
|
||||
@ -410,7 +410,7 @@ Here are the features which may require additional polyfills:
|
||||
|
||||
|
||||
<td>
|
||||
[classList](#classlist)
|
||||
[classList](guide/browser-support#classlist)
|
||||
</td>
|
||||
|
||||
|
||||
@ -430,7 +430,7 @@ Here are the features which may require additional polyfills:
|
||||
|
||||
|
||||
<td>
|
||||
[Typed Array](#typedarray) <br>[Blob](#blob)<br>[FormData](#formdata)
|
||||
[Typed Array](guide/browser-support#typedarray) <br>[Blob](guide/browser-support#blob)<br>[FormData](guide/browser-support#formdata)
|
||||
</td>
|
||||
|
||||
|
||||
@ -457,7 +457,7 @@ Below are the polyfills which are used to test the framework itself. They are a
|
||||
|
||||
|
||||
<th>
|
||||
Licence
|
||||
License
|
||||
</th>
|
||||
|
||||
|
||||
@ -517,7 +517,7 @@ Below are the polyfills which are used to test the framework itself. They are a
|
||||
|
||||
|
||||
<td>
|
||||
MIT / Unicode licence
|
||||
MIT / Unicode license
|
||||
</td>
|
||||
|
||||
|
||||
@ -611,4 +611,5 @@ Below are the polyfills which are used to test the framework itself. They are a
|
||||
|
||||
</table>
|
||||
|
||||
\* Figures are for minified and gzipped code, computed with the <a href="http://closure-compiler.appspot.com/home" target="_blank">closure compiler</a>
|
||||
\* Figures are for minified and gzipped code,
|
||||
computed with the <a href="http://closure-compiler.appspot.com/home" target="_blank">closure compiler</a>.
|
@ -2,48 +2,48 @@
|
||||
Dependency Injection
|
||||
|
||||
@intro
|
||||
Techniques for Dependency Injection
|
||||
Techniques for Dependency Injection.
|
||||
|
||||
@description
|
||||
Dependency Injection is a powerful pattern for managing code dependencies.
|
||||
In this cookbook we will explore many of the features of Dependency Injection (DI) in Angular.
|
||||
<a id="toc"></a>## Table of contents
|
||||
|
||||
[Application-wide dependencies](#app-wide-dependencies)
|
||||
[Application-wide dependencies](guide/cb-dependency-injection#app-wide-dependencies)
|
||||
|
||||
[External module configuration](#external-module-configuration)
|
||||
[External module configuration](guide/cb-dependency-injection#external-module-configuration)
|
||||
|
||||
[*@Injectable* and nested service dependencies](#nested-dependencies)
|
||||
[*@Injectable* and nested service dependencies](guide/cb-dependency-injection#nested-dependencies)
|
||||
|
||||
[Limit service scope to a component subtree](#service-scope)
|
||||
[Limit service scope to a component subtree](guide/cb-dependency-injection#service-scope)
|
||||
|
||||
[Multiple service instances (sandboxing)](#multiple-service-instances)
|
||||
[Multiple service instances (sandboxing)](guide/cb-dependency-injection#multiple-service-instances)
|
||||
|
||||
[Qualify dependency lookup with *@Optional* and *@Host*](#qualify-dependency-lookup)
|
||||
[Qualify dependency lookup with *@Optional* and *@Host*](guide/cb-dependency-injection#qualify-dependency-lookup)
|
||||
|
||||
[Inject the component's DOM element](#component-element)
|
||||
[Inject the component's DOM element](guide/cb-dependency-injection#component-element)
|
||||
|
||||
[Define dependencies with providers](#providers)
|
||||
* [The *provide* object literal](#provide)
|
||||
* [useValue - the *value provider*](#usevalue)
|
||||
* [useClass - the *class provider*](#useclass)
|
||||
* [useExisting - the *alias provider*](#useexisting)
|
||||
* [useFactory - the *factory provider*](#usefactory)
|
||||
[Define dependencies with providers](guide/cb-dependency-injection#providers)
|
||||
* [The *provide* object literal](guide/cb-dependency-injection#provide)
|
||||
* [useValue - the *value provider*](guide/cb-dependency-injection#usevalue)
|
||||
* [useClass - the *class provider*](guide/cb-dependency-injection#useclass)
|
||||
* [useExisting - the *alias provider*](guide/cb-dependency-injection#useexisting)
|
||||
* [useFactory - the *factory provider*](guide/cb-dependency-injection#usefactory)
|
||||
|
||||
[Provider token alternatives](#tokens)
|
||||
* [class-interface](#class-interface)
|
||||
* [OpaqueToken](#opaque-token)
|
||||
[Provider token alternatives](guide/cb-dependency-injection#tokens)
|
||||
* [class-interface](guide/cb-dependency-injection#class-interface)
|
||||
* [OpaqueToken](guide/cb-dependency-injection#opaque-token)
|
||||
|
||||
[Inject into a derived class](#di-inheritance)
|
||||
[Inject into a derived class](guide/cb-dependency-injection#di-inheritance)
|
||||
|
||||
[Find a parent component by injection](#find-parent)
|
||||
* [Find parent with a known component type](#known-parent)
|
||||
* [Cannot find a parent by its base class](#base-parent)
|
||||
* [Find a parent by its class-interface](#class-interface-parent)
|
||||
* [Find a parent in a tree of parents (*@SkipSelf*)](#parent-tree)
|
||||
* [A *provideParent* helper function](#provideparent)
|
||||
[Find a parent component by injection](guide/cb-dependency-injection#find-parent)
|
||||
* [Find parent with a known component type](guide/cb-dependency-injection#known-parent)
|
||||
* [Cannot find a parent by its base class](guide/cb-dependency-injection#base-parent)
|
||||
* [Find a parent by its class-interface](guide/cb-dependency-injection#class-interface-parent)
|
||||
* [Find a parent in a tree of parents (*@SkipSelf*)](guide/cb-dependency-injection#parent-tree)
|
||||
* [A *provideParent* helper function](guide/cb-dependency-injection#provideparent)
|
||||
|
||||
[Break circularities with a forward class reference (*forwardRef*)](#forwardref)
|
||||
[Break circularities with a forward class reference (*forwardRef*)](guide/cb-dependency-injection#forwardref)
|
||||
**See the <live-example name="cb-dependency-injection"></live-example>**
|
||||
of the code supporting this cookbook.
|
||||
|
||||
@ -62,7 +62,7 @@ Service classes can act as their own providers which is why listing them in the
|
||||
is all the registration we need.
|
||||
A *provider* is something that can create or deliver a service.
|
||||
Angular creates a service instance from a class provider by "new-ing" it.
|
||||
Learn more about providers [below](#providers).Now that we've registered these services,
|
||||
Learn more about providers [below](guide/cb-dependency-injection#providers).Now that we've registered these services,
|
||||
Angular can inject them into the constructor of *any* component or service, *anywhere* in the application.
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-bios.component.ts' region='ctor'}
|
||||
@ -79,7 +79,7 @@ We do this when (a) we expect the service to be injectable everywhere
|
||||
or (b) we must configure another application global service _before it starts_.
|
||||
|
||||
We see an example of the second case here, where we configure the Component Router with a non-default
|
||||
[location strategy](../guide/router.html#location-strategy) by listing its provider
|
||||
[location strategy](guide/router) by listing its provider
|
||||
in the `providers` list of the `AppModule`.
|
||||
|
||||
|
||||
@ -122,7 +122,7 @@ The author simply declared what was needed in the constructor (`LoggerService` a
|
||||
Once all the dependencies are in place, the `AppComponent` displays the user information:
|
||||
|
||||
<figure class='image-display'>
|
||||
<img src="/resources/images/cookbooks/dependency-injection/logged-in-user.png" alt="Logged In User"> </img>
|
||||
<img src="assets/images/cookbooks/dependency-injection/logged-in-user.png" alt="Logged In User"> </img>
|
||||
</figure>
|
||||
|
||||
### *@Injectable()*
|
||||
@ -136,7 +136,7 @@ Technically, the `@Injectable()`decorator is only _required_ for a service class
|
||||
The `LoggerService` doesn't depend on anything. The logger would work if we omitted `@Injectable()`
|
||||
and the generated code would be slightly smaller.
|
||||
|
||||
But the service would break the moment we gave it a dependency and we'd have to go back and
|
||||
But the service would break the moment we gave it a dependency and we'd have to go back
|
||||
and add `@Injectable()` to fix it. We add `@Injectable()` from the start for the sake of consistency and to avoid future pain.
|
||||
|
||||
|
||||
@ -233,7 +233,7 @@ And the template displays this data-bound property.
|
||||
Find this example in <live-example name="cb-dependency-injection">live code</live-example>
|
||||
and confirm that the three `HeroBioComponent` instances have their own cached hero data.
|
||||
<figure class='image-display'>
|
||||
<img src="/resources/images/cookbooks/dependency-injection/hero-bios.png" alt="Bios"> </img>
|
||||
<img src="assets/images/cookbooks/dependency-injection/hero-bios.png" alt="Bios"> </img>
|
||||
</figure>
|
||||
|
||||
|
||||
@ -264,7 +264,7 @@ But when this component is projected into a *parent* component, that parent comp
|
||||
We look at this second, more interesting case in our next example.
|
||||
|
||||
### Demonstration
|
||||
The `HeroBiosAndContactsComponent` is a revision of the `HeroBiosComponent` that we looked at [above](#hero-bios-component).
|
||||
The `HeroBiosAndContactsComponent` is a revision of the `HeroBiosComponent` that we looked at [above](guide/cb-dependency-injection#hero-bios-component).
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-bios.component.ts' region='hero-bios-and-contacts'}
|
||||
|
||||
@ -280,7 +280,7 @@ placing it in the `<ng-content>` slot of the `HeroBioComponent` template:
|
||||
|
||||
It looks like this, with the hero's telephone number from `HeroContactComponent` projected above the hero description:
|
||||
<figure class='image-display'>
|
||||
<img src="/resources/images/cookbooks/dependency-injection/hero-bio-and-content.png" alt="bio and contact"> </img>
|
||||
<img src="assets/images/cookbooks/dependency-injection/hero-bio-and-content.png" alt="bio and contact"> </img>
|
||||
</figure>
|
||||
|
||||
Here's the `HeroContactComponent` which demonstrates the qualifying decorators that we're talking about in this section:
|
||||
@ -304,14 +304,14 @@ Thanks to `@Optional()`, Angular sets the `loggerService` to null and the rest o
|
||||
|
||||
We'll come back to the `elementRef` property shortly.Here's the `HeroBiosAndContactsComponent` in action.
|
||||
<figure class='image-display'>
|
||||
<img src="/resources/images/cookbooks/dependency-injection/hero-bios-and-contacts.png" alt="Bios with contact into"> </img>
|
||||
<img src="assets/images/cookbooks/dependency-injection/hero-bios-and-contacts.png" alt="Bios with contact into"> </img>
|
||||
</figure>
|
||||
|
||||
If we comment out the `@Host()` decorator, Angular now walks up the injector ancestor tree
|
||||
until it finds the logger at the `AppComponent` level. The logger logic kicks in and the hero display updates
|
||||
with the gratuitous "!!!", indicating that the logger was found.
|
||||
<figure class='image-display'>
|
||||
<img src="/resources/images/cookbooks/dependency-injection/hero-bio-contact-no-host.png" alt="Without @Host"> </img>
|
||||
<img src="assets/images/cookbooks/dependency-injection/hero-bio-contact-no-host.png" alt="Without @Host"> </img>
|
||||
</figure>
|
||||
|
||||
On the other hand, if we restore the `@Host()` decorator and comment out `@Optional`,
|
||||
@ -325,7 +325,7 @@ Although we strive to avoid it, many visual effects and 3rd party tools (such as
|
||||
require DOM access.
|
||||
|
||||
To illustrate, we've written a simplified version of the `HighlightDirective` from
|
||||
the [Attribute Directives](../guide/attribute-directives.html) chapter.
|
||||
the [Attribute Directives](guide/attribute-directives) chapter.
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/highlight.directive.ts'}
|
||||
|
||||
@ -343,7 +343,7 @@ first without a value (yielding the default color) and then with an assigned col
|
||||
|
||||
The following image shows the effect of mousing over the `<hero-bios-and-contacts>` tag.
|
||||
<figure class='image-display'>
|
||||
<img src="/resources/images/cookbooks/dependency-injection/highlight.png" alt="Highlighted bios"> </img>
|
||||
<img src="assets/images/cookbooks/dependency-injection/highlight.png" alt="Highlighted bios"> </img>
|
||||
</figure>
|
||||
|
||||
<a id="providers"></a>
|
||||
@ -362,7 +362,7 @@ Here's a typical example:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/hero-bios.component.ts' region='ctor'}
|
||||
|
||||
Angular asks the injector for the service associated with the `LoggerService` and
|
||||
Angular asks the injector for the service associated with the `LoggerService`
|
||||
and assigns the returned value to the `logger` parameter.
|
||||
|
||||
Where did the injector get that value?
|
||||
@ -371,7 +371,7 @@ If it doesn't, it may be able to make one with the help of a ***provider***.
|
||||
A *provider* is a recipe for delivering a service associated with a *token*.
|
||||
If the injector doesn't have a provider for the requested *token*, it delegates the request
|
||||
to its parent injector, where the process repeats until there are no more injectors.
|
||||
If the search is futile, the injector throws an error ... unless the request was [optional](#optional).
|
||||
If the search is futile, the injector throws an error ... unless the request was [optional](guide/cb-dependency-injection#optional).
|
||||
|
||||
Let's return our attention to providers themselves.A new injector has no providers.
|
||||
Angular initializes the injectors it creates with some providers it cares about.
|
||||
@ -394,7 +394,7 @@ We need other ways to deliver dependency values and that means we need other way
|
||||
The `HeroOfTheMonthComponent` example demonstrates many of the alternatives and why we need them.
|
||||
|
||||
<figure class='image-display'>
|
||||
<img src="/resources/images/cookbooks/dependency-injection/hero-of-month.png" alt="Hero of the month" width="300px"> </img>
|
||||
<img src="assets/images/cookbooks/dependency-injection/hero-of-month.png" alt="Hero of the month" width="300px"> </img>
|
||||
</figure>
|
||||
|
||||
It's visually simple: a few properties and the output of a logger. The code behind it gives us plenty to talk about.
|
||||
@ -408,7 +408,7 @@ It's visually simple: a few properties and the output of a logger. The code behi
|
||||
#### The *provide* object literal
|
||||
|
||||
The `provide` object literal takes a *token* and a *definition object*.
|
||||
The *token* is usually a class but [it doesn't have to be](#tokens).
|
||||
The *token* is usually a class but [it doesn't have to be](guide/cb-dependency-injection#tokens).
|
||||
|
||||
The *definition* object has one main property, (e.g. `useValue`) that indicates how the provider
|
||||
should create or return the provided value.
|
||||
@ -433,7 +433,7 @@ The `Hero` provider token is a class which makes sense because the value is a `H
|
||||
and the consumer of the injected hero would want the type information.
|
||||
|
||||
The `TITLE` provider token is *not a class*.
|
||||
It's a special kind of provider lookup key called an [OpaqueToken](#opaquetoken).
|
||||
It's a special kind of provider lookup key called an [OpaqueToken](guide/cb-dependency-injection#opaquetoken).
|
||||
We often use an `OpaqueToken` when the dependency is a simple value like a string, a number, or a function.
|
||||
|
||||
The value of a *value provider* must be defined *now*. We can't create the value later.
|
||||
@ -486,21 +486,21 @@ creating ***two ways to access the same service object***.
|
||||
Narrowing an API through an aliasing interface is _one_ important use case for this technique.
|
||||
We're aliasing for that very purpose here.
|
||||
Imagine that the `LoggerService` had a large API (it's actually only three methods and a property).
|
||||
We want to shrink that API surface to just the two members exposed by the `MinimalLogger` [*class-interface*](#class-interface):
|
||||
We want to shrink that API surface to just the two members exposed by the `MinimalLogger` [*class-interface*](guide/cb-dependency-injection#class-interface):
|
||||
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/date-logger.service.ts' region='minimal-logger'}
|
||||
|
||||
The constructor's `logger` parameter is typed as `MinimalLogger` so only its two members are visible in TypeScript:
|
||||
<figure class='image-display'>
|
||||
<img src="/resources/images/cookbooks/dependency-injection/minimal-logger-intellisense.png" alt="MinimalLogger restricted API"> </img>
|
||||
<img src="assets/images/cookbooks/dependency-injection/minimal-logger-intellisense.png" alt="MinimalLogger restricted API"> </img>
|
||||
</figure>
|
||||
|
||||
Angular actually sets the `logger` parameter to the injector's full version of the `LoggerService`
|
||||
which happens to be the `DateLoggerService` thanks to the override provider registered previously via `useClass`.
|
||||
The following image, which displays the logging date, confirms the point:
|
||||
<figure class='image-display'>
|
||||
<img src="/resources/images/cookbooks/dependency-injection/date-logger-entry.png" alt="DateLoggerService entry" width="300px"> </img>
|
||||
<img src="assets/images/cookbooks/dependency-injection/date-logger-entry.png" alt="DateLoggerService entry" width="300px"> </img>
|
||||
</figure>
|
||||
|
||||
|
||||
@ -641,7 +641,7 @@ In this contrived example, `SortedHeroesComponent` inherits from `HeroesBaseComp
|
||||
to display a *sorted* list of heroes.
|
||||
|
||||
<figure class='image-display'>
|
||||
<img src="/resources/images/cookbooks/dependency-injection/sorted-heroes.png" alt="Sorted Heroes"> </img>
|
||||
<img src="assets/images/cookbooks/dependency-injection/sorted-heroes.png" alt="Sorted Heroes"> </img>
|
||||
</figure>
|
||||
|
||||
The `HeroesBaseComponent` could stand on its own.
|
||||
@ -718,7 +718,7 @@ after injecting an `AlexComponent` into her constructor:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='cathy'}
|
||||
|
||||
We added the [@Optional](#optional) qualifier for safety but
|
||||
We added the [@Optional](guide/cb-dependency-injection#optional) qualifier for safety but
|
||||
the <live-example name="cb-dependency-injection"></live-example>
|
||||
confirms that the `alex` parameter is set.
|
||||
|
||||
@ -741,7 +741,7 @@ That's not possible because TypeScript interfaces disappear from the transpiled
|
||||
which doesn't support interfaces. There's no artifact we could look for.We're not claiming this is good design.
|
||||
We are asking *can a component inject its parent via the parent's base class*?
|
||||
|
||||
The sample's `CraigComponent` explores this question. [Looking back](#alex)
|
||||
The sample's `CraigComponent` explores this question. [Looking back](guide/cb-dependency-injection#alex)
|
||||
we see that the `Alex` component *extends* (*inherits*) from a class named `Base`.
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='alex-class-signature'}
|
||||
@ -758,14 +758,14 @@ confirms that the `alex` parameter is null.
|
||||
<a id="class-interface-parent"></a>
|
||||
### Find a parent by its class-interface
|
||||
|
||||
We can find a parent component with a [class-interface](#class-interface).
|
||||
We can find a parent component with a [class-interface](guide/cb-dependency-injection#class-interface).
|
||||
|
||||
The parent must cooperate by providing an *alias* to itself in the name of a *class-interface* token.
|
||||
|
||||
Recall that Angular always adds a component instance to its own injector;
|
||||
that's why we could inject *Alex* into *Cathy* [earlier](#known-parent).
|
||||
that's why we could inject *Alex* into *Cathy* [earlier](guide/cb-dependency-injection#known-parent).
|
||||
|
||||
We write an [*alias provider*](#useexisting) — a `provide` object literal with a `useExisting` definition —
|
||||
We write an [*alias provider*](guide/cb-dependency-injection#useexisting) — a `provide` object literal with a `useExisting` definition —
|
||||
that creates an *alternative* way to inject the same component instance
|
||||
and add that provider to the `providers` array of the `@Component` metadata for the `AlexComponent`:
|
||||
|
||||
@ -774,8 +774,8 @@ and add that provider to the `providers` array of the `@Component` metadata for
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='alex-providers'}
|
||||
|
||||
[Parent](#parent-token) is the provider's *class-interface* token.
|
||||
The [*forwardRef*](#forwardref) breaks the circular reference we just created by having the `AlexComponent` refer to itself.
|
||||
[Parent](guide/cb-dependency-injection#parent-token) is the provider's *class-interface* token.
|
||||
The [*forwardRef*](guide/cb-dependency-injection#forwardref) breaks the circular reference we just created by having the `AlexComponent` refer to itself.
|
||||
|
||||
*Carol*, the third of *Alex*'s child components, injects the parent into its `parent` parameter, the same way we've done it before:
|
||||
|
||||
@ -783,7 +783,7 @@ The [*forwardRef*](#forwardref) breaks the circular reference we just created by
|
||||
|
||||
Here's *Alex* and family in action:
|
||||
<figure class='image-display'>
|
||||
<img src="/resources/images/cookbooks/dependency-injection/alex.png" alt="Alex in action"> </img>
|
||||
<img src="assets/images/cookbooks/dependency-injection/alex.png" alt="Alex in action"> </img>
|
||||
</figure>
|
||||
|
||||
|
||||
@ -802,8 +802,8 @@ Here's *Barry*:
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='barry'}
|
||||
|
||||
*Barry*'s `providers` array looks just like [*Alex*'s](#alex-providers).
|
||||
If we're going to keep writing [*alias providers*](#useexisting) like this we should create a [helper function](#provideparent).
|
||||
*Barry*'s `providers` array looks just like [*Alex*'s](guide/cb-dependency-injection#alex-providers).
|
||||
If we're going to keep writing [*alias providers*](guide/cb-dependency-injection#useexisting) like this we should create a [helper function](guide/cb-dependency-injection#provideparent).
|
||||
|
||||
For now, focus on *Barry*'s constructor:
|
||||
<md-tab-group>
|
||||
@ -834,14 +834,14 @@ which *is* what parent means.
|
||||
Here's *Alice*, *Barry* and family in action:
|
||||
|
||||
<figure class='image-display'>
|
||||
<img src="/resources/images/cookbooks/dependency-injection/alice.png" alt="Alice in action"> </img>
|
||||
<img src="assets/images/cookbooks/dependency-injection/alice.png" alt="Alice in action"> </img>
|
||||
</figure>
|
||||
|
||||
|
||||
|
||||
{@a parent-token}
|
||||
### The *Parent* class-interface
|
||||
We [learned earlier](#class-interface) that a *class-interface* is an abstract class used as an interface rather than as a base class.
|
||||
We [learned earlier](guide/cb-dependency-injection#class-interface) that a *class-interface* is an abstract class used as an interface rather than as a base class.
|
||||
|
||||
Our example defines a `Parent` *class-interface* .
|
||||
|
||||
@ -870,7 +870,7 @@ It doesn't in this example *only* to demonstrate that the code will compile and
|
||||
### A *provideParent* helper function
|
||||
|
||||
Writing variations of the same parent *alias provider* gets old quickly,
|
||||
especially this awful mouthful with a [*forwardRef*](#forwardref):
|
||||
especially this awful mouthful with a [*forwardRef*](guide/cb-dependency-injection#forwardref):
|
||||
|
||||
{@example 'cb-dependency-injection/ts/src/app/parent-finder.component.ts' region='alex-providers'}
|
||||
|
@ -2,7 +2,7 @@
|
||||
Cookbook
|
||||
|
||||
@intro
|
||||
A collection of recipes for common Angular application scenarios
|
||||
A collection of recipes for common Angular application scenarios.
|
||||
|
||||
@description
|
||||
The *Cookbook* offers answers to common implementation questions.
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user