Compare commits
1081 Commits
4.0.0-rc.1
...
4.2.0
Author | SHA1 | Date | |
---|---|---|---|
1c04b83ea3 | |||
429bc9d3cd | |||
41a765d715 | |||
f370fd36e0 | |||
a222c3e609 | |||
5a71df0cb3 | |||
6a46cabb10 | |||
74673545c0 | |||
75a311f250 | |||
391ed6334d | |||
0940e6d6ed | |||
4d2ee51bb0 | |||
680128bc09 | |||
5364b51979 | |||
67ef0f0c8f | |||
e9886d701d | |||
022835bab2 | |||
8be2e4c325 | |||
33ba3e31ed | |||
cba2b3c72d | |||
a4a2901294 | |||
bb46f54ad7 | |||
c9b930dd82 | |||
a39f7d63bb | |||
e317f7d51c | |||
3ddd28d37d | |||
fe6b39d585 | |||
d837bfc2d7 | |||
4759975be6 | |||
078a4b00a7 | |||
b00b80a45b | |||
269bbe0e7d | |||
bb2fc6b8da | |||
d4e196035c | |||
b8979c8701 | |||
bfdd3398f6 | |||
3a121a621f | |||
c06f4fc702 | |||
95f1ea2f12 | |||
784347f61f | |||
42176a7ac4 | |||
76b4b80a23 | |||
7c78282ce8 | |||
47c2a2e411 | |||
5faf520067 | |||
02d74cafba | |||
734f30d14c | |||
f2d810febc | |||
819514aeba | |||
b55adee982 | |||
4c32cb952f | |||
35f714e438 | |||
d5ce086089 | |||
82ec02daf8 | |||
8e7a3f031b | |||
7822187b17 | |||
068133ec85 | |||
598fdad089 | |||
ad6a57e0a3 | |||
230255f887 | |||
1338995ee9 | |||
47e4fca7fd | |||
665e7071fa | |||
c20f60b144 | |||
1408357198 | |||
1f9a3dd1e6 | |||
11505fa0d8 | |||
9326e062d8 | |||
cb2cb7c3bd | |||
92d99ba279 | |||
dfbbbb5e3e | |||
89f317915d | |||
4d5fa5c855 | |||
2f35392cd8 | |||
b056adc032 | |||
0d894a18fc | |||
e5138081ec | |||
8ffa483bb6 | |||
ea8a43def0 | |||
535d9da6b6 | |||
18bf77204e | |||
160221c815 | |||
6220b49463 | |||
aa683a765d | |||
b8b91d3418 | |||
0018acfede | |||
51d7a65a2b | |||
a1724f7816 | |||
d108479d84 | |||
cd5bc64658 | |||
5f723cb92c | |||
7ffb75f476 | |||
c0981b8e13 | |||
80e506563c | |||
694951096c | |||
9e7e178585 | |||
db3113ba16 | |||
3117f565c1 | |||
8443d199b2 | |||
8e2f72c644 | |||
0c1d1e2396 | |||
740450287d | |||
0c691af1d2 | |||
5aa53d70aa | |||
3ab86bd661 | |||
2538094e13 | |||
b37a0484d4 | |||
4c5e28e53a | |||
e10d763446 | |||
573b8611bc | |||
966eb2fbd0 | |||
b0c5018c70 | |||
19a509a92c | |||
9e17a147ec | |||
eba59aaf87 | |||
fa809ec8cf | |||
1651a8f189 | |||
27761b4500 | |||
a80ac0a8d3 | |||
5af143e8e4 | |||
255d7226d1 | |||
50abca4583 | |||
de8d7c65f2 | |||
6123b9c0c6 | |||
3b28c75d1f | |||
368169dc15 | |||
f5b2ce0206 | |||
78e3be12a4 | |||
e7d9fd8056 | |||
08dfe91b95 | |||
85d4c4b82e | |||
6e41add867 | |||
11c10b2ab8 | |||
98849de127 | |||
afd703d08c | |||
ce18fdb399 | |||
670771da33 | |||
c8b08f3a59 | |||
3d382dc750 | |||
9081f84543 | |||
f1a9e3c1bb | |||
b10029c18b | |||
5d4f5434fd | |||
81ca51a8f0 | |||
6cb93c1fac | |||
eed67ddafb | |||
be9e8b99ff | |||
8a0e5659c0 | |||
2842b0cc3d | |||
a42322da0c | |||
4ccb2269a5 | |||
b836aca999 | |||
7d9f96abf0 | |||
86b7bd9c8e | |||
a0a6029915 | |||
6531806996 | |||
3a604ba0bf | |||
39f2977fa8 | |||
faacbe4dac | |||
47c72e4627 | |||
59136fdbe4 | |||
35d1922006 | |||
b40aae54b7 | |||
baa3fbab46 | |||
3361a7b834 | |||
0e13a5956c | |||
9466908c22 | |||
93d27d283a | |||
6f59a4a5b2 | |||
71c3103312 | |||
198edb3109 | |||
44d48d9d7a | |||
712630ca65 | |||
6416e79933 | |||
238c5238a5 | |||
593fe5ed25 | |||
54a6e4ff9e | |||
8a6eb1ac78 | |||
16c8167886 | |||
d761059e4d | |||
c805082648 | |||
9a7f5d580f | |||
af99cf2a41 | |||
221f85af3f | |||
a68ad6d58d | |||
7ae0440cca | |||
2ee7662737 | |||
44195858e6 | |||
39b92f7e54 | |||
1eba623d12 | |||
ea2b24fb4e | |||
a9d9aa18a0 | |||
ecc356ecea | |||
98e31290a7 | |||
7a759df49e | |||
38c524d655 | |||
06264645fd | |||
21d213dfc7 | |||
3065fc6cca | |||
bcefc61da4 | |||
569b1e0eb7 | |||
b83fe6f2a4 | |||
b016984c04 | |||
90bc5b221b | |||
37f1fcbfc8 | |||
4a599eec45 | |||
fdfeaaf1f3 | |||
decb4cc4a3 | |||
44c7ac0fe9 | |||
abb36e3cba | |||
84c30be164 | |||
5afaa39e68 | |||
ce1d7c4a6e | |||
b9521b568f | |||
2eca6e67e1 | |||
c757e5794f | |||
67dc970ce4 | |||
56ccd5e6a1 | |||
bcbee13e26 | |||
d28a3f7878 | |||
162dffb7e8 | |||
0f0b9896b7 | |||
e242e20ca3 | |||
f148ebe99e | |||
f795f649cf | |||
063db641f8 | |||
b44f5c69e1 | |||
56833a6171 | |||
efa2928547 | |||
f5335d17ec | |||
dc7d24267d | |||
412ab3f20c | |||
954c08e97c | |||
a2dcb7b476 | |||
bb0902c592 | |||
9e661e58d1 | |||
d0e72a8f8f | |||
b44eb328e0 | |||
7b4a8d53a7 | |||
73505e2ff0 | |||
e11f5294ab | |||
0190df9cf3 | |||
1af42aedfa | |||
215611aa9e | |||
98d83b2e2d | |||
cf7689ea7b | |||
2848f0499f | |||
5d4b36f80f | |||
415a0f8047 | |||
e0a8376237 | |||
f0f65443c6 | |||
fcc91d862f | |||
3887d8a429 | |||
041f57cb7d | |||
c5ce0408a1 | |||
98dd609f89 | |||
6dc67772b0 | |||
5cf64266f8 | |||
03513e9d70 | |||
b9ed97c0d9 | |||
799be9c98a | |||
566dab1140 | |||
42dc2c19ee | |||
518eb540aa | |||
309ada5df5 | |||
9da63408b0 | |||
7ae7a8440c | |||
aef524506b | |||
547c363473 | |||
2714644528 | |||
d27588b5fb | |||
a8379a46cf | |||
b7caa3e024 | |||
3e33482386 | |||
5057e16874 | |||
9449eff6fd | |||
03610526e5 | |||
cd28df627c | |||
59d62bc5aa | |||
8f46db3701 | |||
109229246c | |||
5856298798 | |||
7f9c589ba3 | |||
8931e71c5c | |||
5bc435eba3 | |||
4dabec6a48 | |||
978376a46e | |||
895f47a9c2 | |||
aec65dee71 | |||
2f66932bd1 | |||
919ff12377 | |||
6748aeabb6 | |||
b3e63c09ab | |||
77b8a76f2e | |||
55b8de9fdd | |||
eb56ab38dc | |||
f29c6bbc6f | |||
464701a899 | |||
5b96fb9320 | |||
1cfb263ee3 | |||
9ca2b4c967 | |||
673d8ae583 | |||
8760bf7be4 | |||
ea02073c84 | |||
b051d7ff48 | |||
cade722e48 | |||
58817f55e1 | |||
3f1d7f7a76 | |||
c8dc116951 | |||
9684d78cae | |||
71f5b73296 | |||
5ba8c14893 | |||
ea9d8a6bc7 | |||
9650ff0824 | |||
00dce1698a | |||
c946a929b7 | |||
21c96a5af1 | |||
a0b9c23100 | |||
cb5bc76766 | |||
1c8772a879 | |||
79ed0e7121 | |||
0c69903123 | |||
04dc24820d | |||
e263e19a2a | |||
ac220fc2bb | |||
3b80472bad | |||
ca17d4f639 | |||
64335d3521 | |||
9945ce2259 | |||
6d9da73090 | |||
c889fb1ef5 | |||
d32bc5df3e | |||
efaf502e95 | |||
ff32c53099 | |||
235eb17cca | |||
aaa562898f | |||
c8fd904c32 | |||
65a8f7f6c9 | |||
f32bfcdbe5 | |||
24b9067047 | |||
de8de84cea | |||
8d300ffbfc | |||
f74dafcb07 | |||
065b76d14c | |||
253345c0c0 | |||
270d694ed2 | |||
f4b771a0c6 | |||
a4de214e2b | |||
aa8bba4865 | |||
392d584572 | |||
900a88b15d | |||
cb384e8ed3 | |||
a2b2afb21c | |||
b70b9606eb | |||
938406122e | |||
951a575db3 | |||
8c50457385 | |||
8c09d10ba9 | |||
c04e51cb15 | |||
7b94f493b9 | |||
f1f04fa782 | |||
4be1966a21 | |||
de25cfc0cb | |||
de36a9b718 | |||
883a3250e4 | |||
1ceb2f9c79 | |||
fbde2a8010 | |||
81925fa66d | |||
6e2abcd5fc | |||
3f46645f5f | |||
2a7f63650c | |||
cd5b1f306a | |||
749bcf3d9c | |||
a9027a2570 | |||
72e32b1f17 | |||
96648ba98a | |||
6561d46349 | |||
2e5e37ac58 | |||
6bac4e65ce | |||
a28f5eeed3 | |||
c0c50133e3 | |||
b9723f9765 | |||
cb35c26d70 | |||
bb52e22ecf | |||
35a2dfc177 | |||
d8e2829e5b | |||
ac5e6baced | |||
710b4a3503 | |||
5331fc1aab | |||
9b7f2ce6c3 | |||
11b2f62ed2 | |||
e7c37d77a8 | |||
1762047bc0 | |||
9a4c8d543d | |||
f5aaa55f21 | |||
23e6502ef3 | |||
ff0f53c915 | |||
64ef69fb34 | |||
249cd8c2ec | |||
d1fb066d07 | |||
2f977312be | |||
feda017c50 | |||
2991221551 | |||
d7719aa0f5 | |||
54e587a46d | |||
4ac5096232 | |||
3d06b18fee | |||
446153675f | |||
bb1850a912 | |||
93516ea8a1 | |||
c3fa8803d3 | |||
3cad5da5a4 | |||
7a8bd99ab1 | |||
7520ddcf40 | |||
9c1318d731 | |||
062fc4afee | |||
87ec8d13ce | |||
cf034f759a | |||
b0c5d21f31 | |||
cbe5f78c1a | |||
4598af23c0 | |||
d855e90524 | |||
897fc87620 | |||
b37f5fc59b | |||
9730351048 | |||
ce687550ae | |||
563b909279 | |||
c390b06da2 | |||
db4e9ea04a | |||
5e6a3ff6a7 | |||
d0dcabd700 | |||
6ed812ff75 | |||
5461182e25 | |||
cbe95f3d25 | |||
ce57b6fb45 | |||
134f542b36 | |||
c3727f330b | |||
d313aad671 | |||
a54fe634ee | |||
ccb6c45466 | |||
800b1b060e | |||
ab03852234 | |||
f90258162a | |||
745731e1a3 | |||
11b0213d20 | |||
cd29d68f3c | |||
da668848c9 | |||
bfd5f27525 | |||
9f66c9c1d1 | |||
1259fe75c9 | |||
e9db74f937 | |||
f99cb96533 | |||
1e848d696b | |||
1bfa7c6f14 | |||
05d1008a08 | |||
9a98fc0f05 | |||
2ca6258a0f | |||
24c34385ee | |||
e07b7ea114 | |||
73c7503ae0 | |||
79db6c3d19 | |||
b6ddacdccf | |||
c9983e6440 | |||
51098c4f86 | |||
4624406ce8 | |||
76269f4a1f | |||
50174d9fb5 | |||
648de7dd86 | |||
9d40ab9e20 | |||
691e86c9bf | |||
8fa4453aec | |||
93d8f8cd63 | |||
c2fca3d9e0 | |||
8b31178647 | |||
e951612af2 | |||
b668c2c781 | |||
ecd0348d96 | |||
69b86925b3 | |||
5293794316 | |||
728c9d0632 | |||
01d93f3af8 | |||
a0d124bd91 | |||
57bc245cb4 | |||
bc431888f3 | |||
ec028b8109 | |||
014594fe8f | |||
3c8a61e40c | |||
bccfaa46ec | |||
2f442062d2 | |||
840cb3d69e | |||
7dfa0b9da9 | |||
f5d0fac800 | |||
baecb553a6 | |||
6e89f0bf8d | |||
f21ff904c2 | |||
38a7e0d1c7 | |||
90814e4449 | |||
e927aeae86 | |||
a77b126d72 | |||
82417b3ca5 | |||
5b141fbf27 | |||
268884296a | |||
ea8ffc9841 | |||
6d930d2fc5 | |||
2ddf3bcdd1 | |||
0a3a9afe58 | |||
5a88d2f68b | |||
7165eb15bd | |||
e20a72280b | |||
abecf3eb6d | |||
7c418a7e06 | |||
58f080a325 | |||
be719e4817 | |||
11b54f69e5 | |||
4a3c66fe22 | |||
9a5084412d | |||
24670667f1 | |||
3f307ff061 | |||
7433da546f | |||
eda2a7b2dc | |||
196203f6d7 | |||
46b0c7a18c | |||
b6a0098aa3 | |||
7d986ae5dc | |||
a2bdcc9ba8 | |||
624b5a5f83 | |||
b57c9605ce | |||
49dfc9fe2d | |||
8f9ba62dc3 | |||
16673fa38b | |||
49d97e1216 | |||
a73050de48 | |||
2535769a65 | |||
b1a3c47766 | |||
8479e9e6d7 | |||
c0b1bbea3e | |||
092f0df7a6 | |||
dfc81c3dab | |||
6649743a2d | |||
56c46d70f7 | |||
c36ec9bf60 | |||
86396a43e9 | |||
590e68c251 | |||
bd704c90dd | |||
ea4afebeb9 | |||
886cca028f | |||
4054055d0d | |||
09c4cb2540 | |||
8ad464d90e | |||
65af9641c2 | |||
cb5a7efa91 | |||
ce47d33cd9 | |||
2e47a0d19f | |||
6a2e08d0a8 | |||
fdb3f26448 | |||
14b7dfa007 | |||
9394835db4 | |||
70b1d6dd9d | |||
d263595c63 | |||
9cb5964b4d | |||
3ad0cc5736 | |||
f09fd6ec16 | |||
8cfa58715c | |||
909264feb5 | |||
cc1ed77dd8 | |||
7d69a91bfe | |||
a6545ddd4d | |||
498bd64d9c | |||
ad9a3a2d3b | |||
2da0f1639f | |||
3ec58ecea0 | |||
061475402c | |||
8f1359d25f | |||
8659bd2c4b | |||
4a052cd343 | |||
28c2a71cfc | |||
69b37fff26 | |||
df619adc76 | |||
374bf1ed98 | |||
540581da3e | |||
6f5fccfeb7 | |||
14669f20bf | |||
075f3f8c9f | |||
2798c1bbec | |||
38d75d410e | |||
04fb29b589 | |||
9037593ab8 | |||
3ced940b5a | |||
f5a98d98c8 | |||
0799f184dc | |||
c530c5d317 | |||
91b2e394d5 | |||
a487563768 | |||
0ab04bd62c | |||
46ce3317c3 | |||
83527fd4fb | |||
6d12aa978d | |||
96aa2365ae | |||
258d5392d5 | |||
09d9f5fe54 | |||
d8b73e4223 | |||
4c566dbfbb | |||
bde9771991 | |||
fe0d02fc47 | |||
7b005aadc1 | |||
7764c5c697 | |||
c6917d9d4f | |||
25132bff86 | |||
0e38bf9de0 | |||
309bae0a0b | |||
ae70293df3 | |||
1e623a3710 | |||
928e2543cd | |||
92e14a36f9 | |||
34a1990c57 | |||
e4c3882a81 | |||
62f9738a9a | |||
a562c64ed6 | |||
8c726ea87e | |||
c8be3960a0 | |||
759af8b56b | |||
97deb01b1f | |||
ad639d783f | |||
e1c2e50d92 | |||
dd4e3f8704 | |||
08941aa0c7 | |||
03fb766428 | |||
2454a3b641 | |||
3e793f079d | |||
5fbb0d050c | |||
71a8627c5d | |||
5c38012980 | |||
7c2ce296d3 | |||
9d37d86223 | |||
21ef5a1961 | |||
46f8a6dd85 | |||
0aa90c6be4 | |||
c05a8cf7bb | |||
4c21114087 | |||
65bb2373ae | |||
8c925bca71 | |||
330a8c90e4 | |||
d8c35c893b | |||
0fe308c0ef | |||
2d14c3b17a | |||
8ef621ad2a | |||
28bf222a6a | |||
9e883f5873 | |||
43a9619e57 | |||
f518e3a75f | |||
146b30796e | |||
5942b2b27c | |||
330b1090f4 | |||
3ccbe28d9f | |||
67719f2185 | |||
6388768d73 | |||
1dda05a965 | |||
14fd78fd85 | |||
a9321b1387 | |||
19cb503531 | |||
9f2acf54bc | |||
9c77a7cdaf | |||
d58a242fe7 | |||
910c0d9ee7 | |||
331b9f6425 | |||
49162784a8 | |||
0c36f2353d | |||
a580f8c61f | |||
f368381d12 | |||
7c2f795ea6 | |||
93d48f1d89 | |||
861953c95c | |||
aa16ccda79 | |||
6269d28bb0 | |||
d438b88f19 | |||
8e03f65645 | |||
606b8fafb0 | |||
b7fa5dec21 | |||
a5c972aa8b | |||
d05aa70c6b | |||
eac99c1b16 | |||
b7a89cec59 | |||
9e0d7be014 | |||
99951911d5 | |||
fd72fad8fd | |||
ff82756415 | |||
d28243d5fc | |||
447e534350 | |||
cda887896a | |||
09b548db75 | |||
920bf373fe | |||
a9ae4daab2 | |||
15662efec4 | |||
5846c46b76 | |||
816b389759 | |||
6605dd1c7c | |||
837ed788f4 | |||
2e4fe7fd2e | |||
52ea193638 | |||
5e3ef775d5 | |||
228238e602 | |||
a6fd22c399 | |||
1616cae5cf | |||
d5cf684d99 | |||
c9e51d9911 | |||
b2830384f5 | |||
487a0e1b5d | |||
8fe42e58b3 | |||
afb17a5b6e | |||
b8f3533d53 | |||
4918cd241f | |||
45983301d5 | |||
acf57def13 | |||
800591db00 | |||
159c98c202 | |||
c837b3dfb9 | |||
53b89ec312 | |||
c17b912eb9 | |||
8785b2bf6d | |||
fb1be83a1b | |||
ea848f74af | |||
b8d5f87f96 | |||
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 |
6
.bazelrc
Normal file
6
.bazelrc
Normal file
@ -0,0 +1,6 @@
|
||||
# Disable sandboxing because it's too slow.
|
||||
# https://github.com/bazelbuild/bazel/issues/2424
|
||||
build --spawn_strategy=standalone
|
||||
|
||||
# Performance: avoid stat'ing input files
|
||||
build --watchfs
|
20
.circleci/config.yml
Normal file
20
.circleci/config.yml
Normal file
@ -0,0 +1,20 @@
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
working_directory: ~/ng
|
||||
docker:
|
||||
- image: alexeagle/ngcontainer
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
|
||||
- run: npm install
|
||||
- run: npm run postinstall
|
||||
- run: ./node_modules/.bin/gulp lint
|
||||
# Build twice, workaround for
|
||||
# https://github.com/bazelbuild/bazel/issues/3114
|
||||
- run: bazel build ... || bazel build ...
|
||||
- save_cache:
|
||||
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
|
||||
paths:
|
||||
- "node_modules"
|
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
.DS_STORE
|
||||
|
||||
/dist/
|
||||
bazel-*
|
||||
node_modules
|
||||
bower_components
|
||||
|
||||
@ -17,16 +18,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,27 +90,27 @@ groups:
|
||||
core:
|
||||
conditions:
|
||||
files:
|
||||
- "modules/@angular/core/*"
|
||||
- "packages/core/*"
|
||||
users:
|
||||
- tbosch #primary
|
||||
- mhevery
|
||||
- vicb
|
||||
- IgorMinar #fallback
|
||||
|
||||
compiler/animations:
|
||||
animations:
|
||||
conditions:
|
||||
files:
|
||||
- "modules/@angular/compiler/src/animation/*"
|
||||
- "packages/animation/*"
|
||||
- "packages/platform-browser/animations/*"
|
||||
users:
|
||||
- matsko #primary
|
||||
- tbosch
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
- IgorMinar #fallback
|
||||
|
||||
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,10 +132,11 @@ groups:
|
||||
conditions:
|
||||
files:
|
||||
- "tools/@angular/tsc-wrapped/*"
|
||||
- "modules/@angular/compiler-cli/*"
|
||||
- "packages/compiler-cli/*"
|
||||
users:
|
||||
- alexeagle
|
||||
- chuckjaz
|
||||
- vicb
|
||||
- tbosch
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
@ -137,7 +144,7 @@ groups:
|
||||
common:
|
||||
conditions:
|
||||
files:
|
||||
- "modules/@angular/common/*"
|
||||
- "packages/common/*"
|
||||
users:
|
||||
- pkozlowski-opensource #primary
|
||||
- vicb
|
||||
@ -147,17 +154,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 +174,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 +205,7 @@ groups:
|
||||
platform-browser:
|
||||
conditions:
|
||||
files:
|
||||
- "modules/@angular/platform-browser/*"
|
||||
- "packages/platform-browser/*"
|
||||
users:
|
||||
- tbosch #primary
|
||||
- vicb #secondary
|
||||
@ -207,7 +215,7 @@ groups:
|
||||
platform-server:
|
||||
conditions:
|
||||
files:
|
||||
- "modules/@angular/platform-server/*"
|
||||
- "packages/platform-server/*"
|
||||
users:
|
||||
- vikerman #primary
|
||||
- alxhub
|
||||
@ -219,7 +227,7 @@ groups:
|
||||
platform-webworker:
|
||||
conditions:
|
||||
files:
|
||||
- "modules/@angular/platform-webworker/*"
|
||||
- "packages/platform-webworker/*"
|
||||
users:
|
||||
- vicb #primary
|
||||
- tbosch #secondary
|
||||
@ -231,7 +239,7 @@ groups:
|
||||
benchpress:
|
||||
conditions:
|
||||
files:
|
||||
- "modules/@angular/benchpress/*"
|
||||
- "packages/benchpress/*"
|
||||
users:
|
||||
- tbosch #primary
|
||||
# needs secondary
|
||||
@ -243,7 +251,8 @@ groups:
|
||||
files:
|
||||
- "aio/*"
|
||||
users:
|
||||
- IgorMinar
|
||||
- robwormald
|
||||
- petebacondarwin
|
||||
- IgorMinar #primary
|
||||
- petebacondarwin #secondary
|
||||
- gkalpak
|
||||
- wardbell
|
||||
- mhevery #fallback
|
||||
|
43
.travis.yml
43
.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,16 +30,17 @@ 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: "aCdHveZuY8AT4Jr1JoJB4LxZsnGWRe/KseZh1YXYe5UtufFCtTVHvUcLn0j2aLBF0KpdyS+hWf0i4np9jthKu2xPKriefoPgCMpisYeC0MFkwbmv+XlgkUbgkgVZMGiVyX7DCYXVahxIoOUjVMEDCbNiHTIrfEuyq24U3ok2tHc="
|
||||
# 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.
|
||||
- secure: "MPx3UM77o5IlhT75PKHL0FXoB5tSXDc3vnCXCd1sRy4XUTZ9vjcV6nNuyqEf+SOw659bGbC1FI4mACGx1Q+z7MQDR85b1mcA9uSgHDkh+IR82CnCVdaX9d1RXafdJIArahxfmorbiiPPLyPIKggo7ituRm+2c+iraoCkE/pXxYg="
|
||||
# This is needed for publishing builds to the "aio-staging" and "angular-io" firebase projects.
|
||||
# This token was generated using the aio-deploy@angular.io account using `firebase login:ci` and password from valentine
|
||||
- secure: "L5CyQmpwWtoR4Qi4xlWQh/cL1M6ZeJL4W4QAr4HdKFMgYt9h+Whqkymyh2NxwmCbPvWa7yUd+OiLQUDCY7L2VIg16hTwoe2CgYDyQA0BEwLzxtRrJXl93TfwMlrUx5JSIzAccD6D4sjtz8kSFMomK2Nls33xOXOukwyhVMjd0Cg="
|
||||
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
|
||||
@ -42,21 +48,28 @@ env:
|
||||
- CI_MODE=browserstack_optional
|
||||
- CI_MODE=docs_test
|
||||
- CI_MODE=aio
|
||||
- CI_MODE=aio_e2e
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- env: "CI_MODE=saucelabs_optional"
|
||||
- env: "CI_MODE=browserstack_optional"
|
||||
- env: "CI_MODE=aio_e2e"
|
||||
|
||||
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
|
||||
|
18
BUILD
Normal file
18
BUILD
Normal file
@ -0,0 +1,18 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
exports_files(["tsconfig.json"])
|
||||
|
||||
# This rule belongs in node_modules/BUILD
|
||||
# It's here as a workaround for
|
||||
# https://github.com/bazelbuild/bazel/issues/374#issuecomment-296217940
|
||||
filegroup(
|
||||
name = "node_modules",
|
||||
srcs = glob([
|
||||
# Performance workaround: list individual files
|
||||
# This won't scale in the general case.
|
||||
# TODO(alexeagle): figure out what to do
|
||||
"node_modules/typescript/lib/**",
|
||||
"node_modules/zone.js/**/*.d.ts",
|
||||
"node_modules/rxjs/**/*.d.ts",
|
||||
"node_modules/@types/**/*.d.ts",
|
||||
]),
|
||||
)
|
631
CHANGELOG.md
631
CHANGELOG.md
@ -1,7 +1,608 @@
|
||||
<a name="4.0.0-rc.0"></a>
|
||||
<a name="4.2.0"></a>
|
||||
# [4.2.0](https://github.com/angular/angular/compare/4.2.0-rc.2...4.2.0) salubrious-stratagem (2017-06-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** ensure web-animations understands a numeric CSS perspective value ([819514a](https://github.com/angular/angular/commit/819514a)), closes [#14007](https://github.com/angular/angular/issues/14007)
|
||||
* **animations:** evaluate substitutions on option param values ([e9886d7](https://github.com/angular/angular/commit/e9886d7))
|
||||
* **forms:** fix min and max validator behavior on non-numbers ([a222c3e](https://github.com/angular/angular/commit/a222c3e))
|
||||
* **router:** opening links in new window ([4c32cb9](https://github.com/angular/angular/commit/4c32cb9))
|
||||
* **upgrade:** call setInterval outside the Angular zone ([269bbe0](https://github.com/angular/angular/commit/269bbe0))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler-cli:** introduce synchronous codegen API ([b00b80a](https://github.com/angular/angular/commit/b00b80a))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **animations:** do not create a closure each time a node is removed ([fe6b39d](https://github.com/angular/angular/commit/fe6b39d))
|
||||
* **animations:** only apply `:leave` flags if animations are set to run ([b55adee](https://github.com/angular/angular/commit/b55adee))
|
||||
|
||||
|
||||
|
||||
<a name="4.2.0-rc.2"></a>
|
||||
# [4.2.0-rc.2](https://github.com/angular/angular/compare/4.2.0-rc.1...4.2.0-rc.2) (2017-06-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** always change to desired animation state even if no transition fires ([#17025](https://github.com/angular/angular/issues/17025)) ([665e707](https://github.com/angular/angular/commit/665e707)), closes [#16947](https://github.com/angular/angular/issues/16947)
|
||||
* **animations:** do not retain deleted nodes during an non-removal animation ([#17153](https://github.com/angular/angular/issues/17153)) ([068133e](https://github.com/angular/angular/commit/068133e)), closes [#17086](https://github.com/angular/angular/issues/17086)
|
||||
* **common:** always use 'other' case for locales with no plural rules ([#16990](https://github.com/angular/angular/issues/16990)) ([535d9da](https://github.com/angular/angular/commit/535d9da))
|
||||
* **compiler:** enableLegacyTemplate should not be ignored ([#17051](https://github.com/angular/angular/issues/17051)) ([8ffa483](https://github.com/angular/angular/commit/8ffa483)), closes [#15555](https://github.com/angular/angular/issues/15555)
|
||||
* **router:** make remove trailing slash consistent with URL params ([c20f60b](https://github.com/angular/angular/commit/c20f60b)), closes [#16069](https://github.com/angular/angular/issues/16069)
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler:** emit typescript nodes from an output ast ([#16823](https://github.com/angular/angular/issues/16823)) ([18bf772](https://github.com/angular/angular/commit/18bf772))
|
||||
* **compiler-cli:** produce template diagnostics error messages ([#17125](https://github.com/angular/angular/issues/17125)) ([230255f](https://github.com/angular/angular/commit/230255f))
|
||||
* **tsc-wrapped:** always convert shorthand imports ([#16898](https://github.com/angular/angular/issues/16898)) ([ea8a43d](https://github.com/angular/angular/commit/ea8a43d))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **animations:** do not place enterId values on elements for querying purposes ([#17150](https://github.com/angular/angular/issues/17150)) ([ad6a57e](https://github.com/angular/angular/commit/ad6a57e))
|
||||
|
||||
<a name="4.2.0-rc.1"></a>
|
||||
# [4.2.0-rc.1](https://github.com/angular/angular/compare/4.2.0-rc.0...4.2.0-rc.1) (2017-05-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** repair flicker issues with WA polyfill ([#16937](https://github.com/angular/angular/issues/16937)) ([e7d9fd8](https://github.com/angular/angular/commit/e7d9fd8)), closes [#16919](https://github.com/angular/angular/issues/16919) [#16918](https://github.com/angular/angular/issues/16918)
|
||||
* **animations:** use a lightweight renderer for non-animation components ([#17003](https://github.com/angular/angular/issues/17003)) ([3ab86bd](https://github.com/angular/angular/commit/3ab86bd))
|
||||
* **compiler:** compile `.ngfactory.ts` files even if nobody references them. ([#16899](https://github.com/angular/angular/issues/16899)) ([573b861](https://github.com/angular/angular/commit/573b861)), closes [#16741](https://github.com/angular/angular/issues/16741)
|
||||
* **compiler:** do not report type errors for arguments with `@Inject` ([#16222](https://github.com/angular/angular/issues/16222)) ([27761b4](https://github.com/angular/angular/commit/27761b4)), closes [#15424](https://github.com/angular/angular/issues/15424)
|
||||
* **core:** make decorators closure safe ([#16905](https://github.com/angular/angular/issues/16905)) ([a80ac0a](https://github.com/angular/angular/commit/a80ac0a)), closes [#16889](https://github.com/angular/angular/issues/16889)
|
||||
* **tsc-wrapped:** ignore `|null` and `|undefined` when collecting types ([#16222](https://github.com/angular/angular/issues/16222)) ([1651a8f](https://github.com/angular/angular/commit/1651a8f))
|
||||
* **tsc-wrapped:** resolve short-hand literal values to locals ([#16873](https://github.com/angular/angular/issues/16873)) ([11c10b2](https://github.com/angular/angular/commit/11c10b2))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler:** add location note to extracted xliff2 files ([#16791](https://github.com/angular/angular/issues/16791)) ([08dfe91](https://github.com/angular/angular/commit/08dfe91)), closes [#16531](https://github.com/angular/angular/issues/16531)
|
||||
* **core:** update zone.js to 0.8.10 and expose the flush method ([#16860](https://github.com/angular/angular/issues/16860)) ([85d4c4b](https://github.com/angular/angular/commit/85d4c4b))
|
||||
* **tsc-wrapped:** support template literals in metadata collection ([#16880](https://github.com/angular/angular/issues/16880)) ([6e41add](https://github.com/angular/angular/commit/6e41add))
|
||||
|
||||
|
||||
|
||||
<a name="4.2.0-rc.0"></a>
|
||||
# [4.2.0-rc.0](https://github.com/angular/angular/compare/4.2.0-beta.0...4.2.0-rc.0) (2017-05-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **animations:** make sure reuseable animation subtitutions work without default params ([#16875](https://github.com/angular/angular/issues/16875)) ([7d9f96a](https://github.com/angular/angular/commit/7d9f96a))
|
||||
* **animations:** only require one flushMicrotasks call when testing animations ([6cb93c1](https://github.com/angular/angular/commit/6cb93c1))
|
||||
* **compiler:** avoid a `...null` spread in extraction ([#16547](https://github.com/angular/angular/issues/16547)) ([e0a8376](https://github.com/angular/angular/commit/e0a8376))
|
||||
* **compiler-cli:** allow '==' to compare nullable types ([#16731](https://github.com/angular/angular/issues/16731)) ([d761059](https://github.com/angular/angular/commit/d761059))
|
||||
* **core:** detach projected views when a parent view is destroyed ([#16592](https://github.com/angular/angular/issues/16592)) ([f0f6544](https://github.com/angular/angular/commit/f0f6544)), closes [#15578](https://github.com/angular/angular/issues/15578)
|
||||
* **core:** projected views should be dirty checked when the declaring component is dirty checked. ([#16592](https://github.com/angular/angular/issues/16592)) ([fcc91d8](https://github.com/angular/angular/commit/fcc91d8)), closes [#14321](https://github.com/angular/angular/issues/14321)
|
||||
* **http:** flatten metadata for [@angular](https://github.com/angular)/http/testing ([9da6340](https://github.com/angular/angular/commit/9da6340)), closes [#15521](https://github.com/angular/angular/issues/15521)
|
||||
* **http:** honor RequestArgs.search and RequestArgs.params map type ([aef5245](https://github.com/angular/angular/commit/aef5245)), closes [#15761](https://github.com/angular/angular/issues/15761) [#16392](https://github.com/angular/angular/issues/16392)
|
||||
* **http:** introduce encodingHint for text() for better ArrayBuffer support ([7ae7a84](https://github.com/angular/angular/commit/7ae7a84)), closes [#15932](https://github.com/angular/angular/issues/15932) [#16420](https://github.com/angular/angular/issues/16420)
|
||||
* **router:** fix redirect to a URL with a param having multiple values ([#16376](https://github.com/angular/angular/issues/16376)) ([5d4b36f](https://github.com/angular/angular/commit/5d4b36f)), closes [#16310](https://github.com/angular/angular/issues/16310)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **animations:** introduce a wave of new animation features ([16c8167](https://github.com/angular/angular/commit/16c8167))
|
||||
* **animations:** introduce routeable animation support ([f1a9e3c](https://github.com/angular/angular/commit/f1a9e3c))
|
||||
* add .ngsummary.ts files to support AOT unit tests ([547c363](https://github.com/angular/angular/commit/547c363))
|
||||
* introduce `TestBed.overrideProvider` ([#16725](https://github.com/angular/angular/issues/16725)) ([39b92f7](https://github.com/angular/angular/commit/39b92f7))
|
||||
* **compiler:** support a non-null postfix assert ([#16672](https://github.com/angular/angular/issues/16672)) ([b9521b5](https://github.com/angular/angular/commit/b9521b5))
|
||||
* **core:** introduce fixture.whenRenderingDone for testing ([#16732](https://github.com/angular/angular/issues/16732)) ([38c524d](https://github.com/angular/angular/commit/38c524d))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **animations:** reduce size of animations bundle ([712630c](https://github.com/angular/angular/commit/712630c))
|
||||
|
||||
|
||||
|
||||
<a name="4.1.3"></a>
|
||||
## [4.1.3](https://github.com/angular/angular/compare/4.1.2...4.1.3) (2017-05-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add typescript 2.3.2 typings test ([#16738](https://github.com/angular/angular/issues/16738)) ([a5bdbed](https://github.com/angular/angular/commit/a5bdbed)), closes [#16663](https://github.com/angular/angular/issues/16663)
|
||||
* **compiler-cli:** import routing module with forRoot ([#16438](https://github.com/angular/angular/issues/16438)) ([b7f8581](https://github.com/angular/angular/commit/b7f8581))
|
||||
* **platform-server:** wait for async app initializers to complete before removing server side styles ([#16712](https://github.com/angular/angular/issues/16712)) ([0a82f7d](https://github.com/angular/angular/commit/0a82f7d)), closes [#15716](https://github.com/angular/angular/issues/15716)
|
||||
* **router:** Wrap Promise-like instances in native Promises ([#16759](https://github.com/angular/angular/issues/16759)) ([883ca28](https://github.com/angular/angular/commit/883ca28))
|
||||
* **upgrade:** Prevent renaming of $inject property ([#16706](https://github.com/angular/angular/issues/16706)) ([afb7540](https://github.com/angular/angular/commit/afb7540))
|
||||
* **upgrade:** use quote to prevent ClossureCompiler obfuscating $event. ([#16724](https://github.com/angular/angular/issues/16724)) ([47df3d6](https://github.com/angular/angular/commit/47df3d6))
|
||||
|
||||
|
||||
|
||||
<a name="4.2.0-beta.1"></a>
|
||||
# [4.2.0-beta.1](https://github.com/angular/angular/compare/4.2.0-beta.0...4.2.0-beta.1) (2017-05-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add .ngsummary.ts files to support AOT unit tests ([547c363](https://github.com/angular/angular/commit/547c363))
|
||||
|
||||
|
||||
|
||||
<a name="4.1.2"></a>
|
||||
## [4.1.2](https://github.com/angular/angular/compare/4.1.1...4.1.2) (2017-05-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler:** avoid a `...null` spread in extraction ([#16547](https://github.com/angular/angular/issues/16547)) ([d0e1688](https://github.com/angular/angular/commit/d0e1688))
|
||||
* **core:** detach projected views when a parent view is destroyed ([#16592](https://github.com/angular/angular/issues/16592)) ([ee6705a](https://github.com/angular/angular/commit/ee6705a)), closes [#15578](https://github.com/angular/angular/issues/15578)
|
||||
* **core:** projected views should be dirty checked when the declaring component is dirty checked. ([#16592](https://github.com/angular/angular/issues/16592)) ([9218812](https://github.com/angular/angular/commit/9218812)), closes [#14321](https://github.com/angular/angular/issues/14321)
|
||||
* **http:** flatten metadata for [@angular](https://github.com/angular)/http/testing ([9c70a3c](https://github.com/angular/angular/commit/9c70a3c)), closes [#15521](https://github.com/angular/angular/issues/15521)
|
||||
* **http:** honor RequestArgs.search and RequestArgs.params map type ([63066f7](https://github.com/angular/angular/commit/63066f7)), closes [#15761](https://github.com/angular/angular/issues/15761) [#16392](https://github.com/angular/angular/issues/16392)
|
||||
* **http:** introduce encodingHint for text() for better ArrayBuffer support ([ec3b6e9](https://github.com/angular/angular/commit/ec3b6e9)), closes [#15932](https://github.com/angular/angular/issues/15932) [#16420](https://github.com/angular/angular/issues/16420)
|
||||
* **router:** fix redirect to a URL with a param having multiple values ([#16376](https://github.com/angular/angular/issues/16376)) ([915eae5](https://github.com/angular/angular/commit/915eae5)), closes [#16310](https://github.com/angular/angular/issues/16310)
|
||||
|
||||
|
||||
|
||||
<a name="4.2.0-beta.0"></a>
|
||||
# [4.2.0-beta.0](https://github.com/angular/angular/compare/4.1.0...4.2.0-beta.0) (2017-05-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core**: strictNullCheck support. ([#16389](https://github.com/angular/angular/issues/16389)) ([#16389](https://github.com/angular/angular/issues/16389)) ([8c09d10](https://github.com/angular/angular/commit/8c09d10)), closes [#16357](https://github.com/angular/angular/issues/16357)
|
||||
* **core:** allow directives to inject the component’s `ChangeDetectorRef`. ([#16394](https://github.com/angular/angular/issues/16394)) ([392d584](https://github.com/angular/angular/commit/392d584)), closes [#12816](https://github.com/angular/angular/issues/12816)
|
||||
* **core:** allow to detach `OnPush` components ([#16394](https://github.com/angular/angular/issues/16394)) ([aa8bba4](https://github.com/angular/angular/commit/aa8bba4)), closes [#9720](https://github.com/angular/angular/issues/9720)
|
||||
* **core:** don’t set `ng-version` for dynamically created components ([#16394](https://github.com/angular/angular/issues/16394)) ([a4de214](https://github.com/angular/angular/commit/a4de214)), closes [#15880](https://github.com/angular/angular/issues/15880)
|
||||
* **core:** don’t stop change detection because of errors ([e263e19](https://github.com/angular/angular/commit/e263e19)), closes [#9531](https://github.com/angular/angular/issues/9531) [#2413](https://github.com/angular/angular/issues/2413) [#15925](https://github.com/angular/angular/issues/15925)
|
||||
* **language-service:** remove asserts for non-null expressions ([#16422](https://github.com/angular/angular/issues/16422)) ([253345c](https://github.com/angular/angular/commit/253345c))
|
||||
* **upgrade:** initialize all inputs in time for `ngOnChanges()` ([b3e63c0](https://github.com/angular/angular/commit/b3e63c0)), closes [#16212](https://github.com/angular/angular/issues/16212)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler-cli:** add param to set MissingTranslationStrategy on ngc ([#15987](https://github.com/angular/angular/issues/15987)) ([6e2abcd](https://github.com/angular/angular/commit/6e2abcd)), closes [#15808](https://github.com/angular/angular/issues/15808)
|
||||
* **core:** add `begin` and `end` renderer methods to track change detection ([7f9c589](https://github.com/angular/angular/commit/7f9c589))
|
||||
* **core:** allow custom selector when bootstrapping components ([#15668](https://github.com/angular/angular/issues/15668)) ([900a88b](https://github.com/angular/angular/commit/900a88b)), closes [#7136](https://github.com/angular/angular/issues/7136)
|
||||
* **core:** upgrade dep on zone.js to 0.8.9 ([#16401](https://github.com/angular/angular/issues/16401)) ([065b76d](https://github.com/angular/angular/commit/065b76d))
|
||||
* **forms:** introduce min and max validators ([#15813](https://github.com/angular/angular/issues/15813)) ([81925fa](https://github.com/angular/angular/commit/81925fa))
|
||||
* **language-service:** provide external file list to TypeScript ([#16417](https://github.com/angular/angular/issues/16417)) ([f4b771a](https://github.com/angular/angular/commit/f4b771a))
|
||||
|
||||
|
||||
|
||||
<a name="4.1.1"></a>
|
||||
## [4.1.1](https://github.com/angular/angular/compare/4.1.0...4.1.1) (2017-05-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core**: strictNullCheck support. ([#16389](https://github.com/angular/angular/issues/16389)) ([#16389](https://github.com/angular/angular/issues/16389)) ([427d63a](https://github.com/angular/angular/commit/427d63a)), closes [#16357](https://github.com/angular/angular/issues/16357)
|
||||
* **core:** allow directives to inject the component’s `ChangeDetectorRef`. ([#16394](https://github.com/angular/angular/issues/16394)) ([f66e59e](https://github.com/angular/angular/commit/f66e59e)), closes [#12816](https://github.com/angular/angular/issues/12816)
|
||||
* **core:** allow to detach `OnPush` components ([#16394](https://github.com/angular/angular/issues/16394)) ([acf83b9](https://github.com/angular/angular/commit/acf83b9)), closes [#9720](https://github.com/angular/angular/issues/9720)
|
||||
* **core:** don’t set `ng-version` for dynamically created components ([#16394](https://github.com/angular/angular/issues/16394)) ([85a1b54](https://github.com/angular/angular/commit/85a1b54)), closes [#15880](https://github.com/angular/angular/issues/15880)
|
||||
* **core:** don’t stop change detection because of errors ([07cef36](https://github.com/angular/angular/commit/07cef36)), closes [#9531](https://github.com/angular/angular/issues/9531) [#2413](https://github.com/angular/angular/issues/2413) [#15925](https://github.com/angular/angular/issues/15925)
|
||||
* **language-service:** remove asserts for non-null expressions ([#16422](https://github.com/angular/angular/issues/16422)) ([c060110](https://github.com/angular/angular/commit/c060110))
|
||||
* **upgrade:** initialize all inputs in time for `ngOnChanges()` ([dd4e501](https://github.com/angular/angular/commit/dd4e501)), closes [#16212](https://github.com/angular/angular/issues/16212)
|
||||
|
||||
|
||||
|
||||
<a name="4.1.0"></a>
|
||||
# [4.1.0](https://github.com/angular/angular/compare/4.1.0-rc.0...4.1.0) (2017-04-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **router:** forward the query parameters in the ng1 -> ng2 url sync ([#16249](https://github.com/angular/angular/issues/16249)) ([2f97731](https://github.com/angular/angular/commit/2f97731)), closes [#16067](https://github.com/angular/angular/issues/16067)
|
||||
* **upgrade:** use correct attribute name for upgraded component's bindings ([#16128](https://github.com/angular/angular/issues/16128)) ([d1fb066](https://github.com/angular/angular/commit/d1fb066)), closes [#8856](https://github.com/angular/angular/issues/8856)
|
||||
|
||||
|
||||
<a name="4.1.0-rc.0"></a>
|
||||
# [4.1.0-rc.0](https://github.com/angular/angular/compare/4.1.0-beta.0...4.1.0-rc.0) (2017-04-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **benchpress:** chrome - prevent trace buffer overflow ([2f44206](https://github.com/angular/angular/commit/2f44206))
|
||||
* **benchpress:** Update types for TypeScript nullability support ([14669f2](https://github.com/angular/angular/commit/14669f2))
|
||||
* **common:** Update types for TypeScript nullability support ([d8b73e4](https://github.com/angular/angular/commit/d8b73e4))
|
||||
* **compiler:** fix build error in xliff2 ([bd704c9](https://github.com/angular/angular/commit/bd704c9))
|
||||
* **compiler:** fix inheritance for AOT with summaries ([#15583](https://github.com/angular/angular/issues/15583)) ([8ef621a](https://github.com/angular/angular/commit/8ef621a))
|
||||
* **compiler:** ignore calls to unresolved symbols in metadata ([38a7e0d](https://github.com/angular/angular/commit/38a7e0d)), closes [#15969](https://github.com/angular/angular/issues/15969)
|
||||
* **compiler:** ignore calls to unresolved symbols in metadata ([#15970](https://github.com/angular/angular/issues/15970)) ([ce47d33](https://github.com/angular/angular/commit/ce47d33)), closes [#15969](https://github.com/angular/angular/issues/15969)
|
||||
* **compiler:** Inform user where Quoted error was thrown ([a77b126](https://github.com/angular/angular/commit/a77b126))
|
||||
* **compiler:** make I18NHtmlParser provider AoT-compliant ([#15980](https://github.com/angular/angular/issues/15980)) ([745731e](https://github.com/angular/angular/commit/745731e))
|
||||
* **compiler:** support `<ng-container>` whatever the namespace ([5b141fb](https://github.com/angular/angular/commit/5b141fb)), closes [#14257](https://github.com/angular/angular/issues/14257)
|
||||
* **compiler:** suppress another closure warning ([#16137](https://github.com/angular/angular/issues/16137)) ([11b0213](https://github.com/angular/angular/commit/11b0213))
|
||||
* **compiler:** Update types for TypeScript nullability support ([09d9f5f](https://github.com/angular/angular/commit/09d9f5f))
|
||||
* **core:** benchmarks - enable ng1 benchmark again ([bccfaa4](https://github.com/angular/angular/commit/bccfaa4))
|
||||
* **core:** distribute externs for testability API ([#16179](https://github.com/angular/angular/issues/16179)) ([da66884](https://github.com/angular/angular/commit/da66884))
|
||||
* **core:** key-value differ changes iteration ([#15968](https://github.com/angular/angular/issues/15968)) ([cb5a7ef](https://github.com/angular/angular/commit/cb5a7ef)), closes [#14997](https://github.com/angular/angular/issues/14997)
|
||||
* **forms:** Update types for TypeScript nullability support ([6649743](https://github.com/angular/angular/commit/6649743))
|
||||
* **forms:** Update types for TypeScript nullability support ([57bc245](https://github.com/angular/angular/commit/57bc245))
|
||||
* **forms:** Update types for TypeScript nullability support ([#15859](https://github.com/angular/angular/issues/15859)) ([6a2e08d](https://github.com/angular/angular/commit/6a2e08d))
|
||||
* **http:** Update types for TypeScript nullability support ([c36ec9b](https://github.com/angular/angular/commit/c36ec9b))
|
||||
* **http:** Update types for TypeScript nullability support ([ec028b8](https://github.com/angular/angular/commit/ec028b8))
|
||||
* **language-service:** avoid throwing exceptions when reporting metadata errors ([7764c5c](https://github.com/angular/angular/commit/7764c5c))
|
||||
* **language-service:** detect when there isn't a tsconfig.json ([258d539](https://github.com/angular/angular/commit/258d539)), closes [#15874](https://github.com/angular/angular/issues/15874)
|
||||
* **language-service:** improve resilience to incomplete information ([71a8627](https://github.com/angular/angular/commit/71a8627))
|
||||
* **language-service:** infer correct type of `?.` expressions ([0a3a9af](https://github.com/angular/angular/commit/0a3a9af)), closes [#15885](https://github.com/angular/angular/issues/15885)
|
||||
* **language-service:** initialize static reflector correctly ([fe0d02f](https://github.com/angular/angular/commit/fe0d02f)), closes [#15768](https://github.com/angular/angular/issues/15768)
|
||||
* **language-service:** look for type constructors on canonical symbol ([2ddf3bc](https://github.com/angular/angular/commit/2ddf3bc))
|
||||
* **language-service:** only use canonical symbols ([5a88d2f](https://github.com/angular/angular/commit/5a88d2f))
|
||||
* **language-service:** parse extended i18n forms ([bde9771](https://github.com/angular/angular/commit/bde9771))
|
||||
* **language-service:** resolve any parameter types to any result ([5fbb0d0](https://github.com/angular/angular/commit/5fbb0d0))
|
||||
* **language-service:** respect baseUrl compiler option ([f21ff90](https://github.com/angular/angular/commit/f21ff90)), closes [#15974](https://github.com/angular/angular/issues/15974)
|
||||
* **language-service:** Update types for TypeScript nullability support ([540581d](https://github.com/angular/angular/commit/540581d))
|
||||
* **packaging:** increased buffer size ([#15840](https://github.com/angular/angular/issues/15840)) ([65af964](https://github.com/angular/angular/commit/65af964))
|
||||
* **platform-browser:** Update types for TypeScript nullability support ([728c9d0](https://github.com/angular/angular/commit/728c9d0)), closes [#15898](https://github.com/angular/angular/issues/15898)
|
||||
* **platform-server:** handle innerText ([#15818](https://github.com/angular/angular/issues/15818)) ([9394835](https://github.com/angular/angular/commit/9394835))
|
||||
* **router:** fix query param parsing ([a487563](https://github.com/angular/angular/commit/a487563))
|
||||
* **router:** prevent `RouterLinkActive` from causing an infinite CD loop ([82417b3](https://github.com/angular/angular/commit/82417b3)), closes [#15825](https://github.com/angular/angular/issues/15825)
|
||||
* **router:** relax nullability requirements ([a0d124b](https://github.com/angular/angular/commit/a0d124b))
|
||||
* turn on nullability in the code base. ([5293794](https://github.com/angular/angular/commit/5293794))
|
||||
* Update types for TypeScript nullability support in examples ([6f5fccf](https://github.com/angular/angular/commit/6f5fccf))
|
||||
* **router:** the preloader use the module from the loaded config ([6d12aa9](https://github.com/angular/angular/commit/6d12aa9))
|
||||
* **router:** Update types for TypeScript nullability support ([56c46d7](https://github.com/angular/angular/commit/56c46d7))
|
||||
* **router:** Update types for TypeScript nullability support ([bc43188](https://github.com/angular/angular/commit/bc43188))
|
||||
* **tsc-wrapped:** collect new expressions with no arguments ([#15908](https://github.com/angular/angular/issues/15908)) ([70b1d6d](https://github.com/angular/angular/commit/70b1d6d)), closes [#15906](https://github.com/angular/angular/issues/15906)
|
||||
* **tsc-wrapped:** ensure valid path separators in metadata ([96aa236](https://github.com/angular/angular/commit/96aa236))
|
||||
* **upgrade:** Update types for TypeScript nullability support ([01d93f3](https://github.com/angular/angular/commit/01d93f3)), closes [#15897](https://github.com/angular/angular/issues/15897)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **animations:** Update types for TypeScript nullability support ([38d75d4](https://github.com/angular/angular/commit/38d75d4)), closes [#15870](https://github.com/angular/angular/issues/15870)
|
||||
* **compiler:** add source files to xmb/xliff translations ([#14705](https://github.com/angular/angular/issues/14705)) ([4054055](https://github.com/angular/angular/commit/4054055)), closes [#14190](https://github.com/angular/angular/issues/14190)
|
||||
* **compiler:** Implement i18n XLIFF 2.0 serializer ([#14185](https://github.com/angular/angular/issues/14185)) ([09c4cb2](https://github.com/angular/angular/commit/09c4cb2)), closes [#11735](https://github.com/angular/angular/issues/11735)
|
||||
* **upgrade:** allow setting the angularjs lib at runtime ([#15168](https://github.com/angular/angular/issues/15168)) ([e927aea](https://github.com/angular/angular/commit/e927aea))
|
||||
* **upgrade:** allow setting the angularjs lib at runtime ([#15168](https://github.com/angular/angular/issues/15168)) ([8ad464d](https://github.com/angular/angular/commit/8ad464d))
|
||||
* **upgrade:** fixes for allow setting the angularjs lib at runtime ([90814e4](https://github.com/angular/angular/commit/90814e4))
|
||||
* add support for TS 2.2 ([3c8a61e](https://github.com/angular/angular/commit/3c8a61e))
|
||||
* add support for TS 2.3 ([014594f](https://github.com/angular/angular/commit/014594f))
|
||||
|
||||
|
||||
|
||||
<a name="4.0.3"></a>
|
||||
## [4.0.3](https://github.com/angular/angular/compare/4.0.2...4.0.3) (2017-04-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **benchpress:** chrome - prevent trace buffer overflow ([d216f94](https://github.com/angular/angular/commit/d216f94))
|
||||
* **compiler:** fix build error in xliff2 ([1870347](https://github.com/angular/angular/commit/1870347))
|
||||
* **compiler:** ignore calls to unresolved symbols in metadata ([d4038ab](https://github.com/angular/angular/commit/d4038ab)), closes [#15969](https://github.com/angular/angular/issues/15969)
|
||||
* **compiler:** ignore calls to unresolved symbols in metadata ([#15970](https://github.com/angular/angular/issues/15970)) ([db25f08](https://github.com/angular/angular/commit/db25f08)), closes [#15969](https://github.com/angular/angular/issues/15969)
|
||||
* **compiler:** Inform user where Quoted error was thrown ([3184cc5](https://github.com/angular/angular/commit/3184cc5))
|
||||
* **compiler:** suppress another closure warning ([#16137](https://github.com/angular/angular/issues/16137)) ([72e240a](https://github.com/angular/angular/commit/72e240a))
|
||||
* **core:** benchmarks - enable ng1 benchmark again ([ccac4c6](https://github.com/angular/angular/commit/ccac4c6))
|
||||
* **core:** distribute externs for testability API ([#16179](https://github.com/angular/angular/issues/16179)) ([e377d9d](https://github.com/angular/angular/commit/e377d9d))
|
||||
* **core:** key-value differ changes iteration ([#15968](https://github.com/angular/angular/issues/15968)) ([a8600dc](https://github.com/angular/angular/commit/a8600dc)), closes [#14997](https://github.com/angular/angular/issues/14997)
|
||||
* **language-service:** only use canonical symbols ([786093a](https://github.com/angular/angular/commit/786093a))
|
||||
* **packaging:** increased buffer size ([#15840](https://github.com/angular/angular/issues/15840)) ([88ad490](https://github.com/angular/angular/commit/88ad490))
|
||||
* **platform-server:** handle innerText ([#15818](https://github.com/angular/angular/issues/15818)) ([7de340d](https://github.com/angular/angular/commit/7de340d))
|
||||
* **router:** prevent `RouterLinkActive` from causing an infinite CD loop ([4479c42](https://github.com/angular/angular/commit/4479c42)), closes [#15825](https://github.com/angular/angular/issues/15825)
|
||||
* **tsc-wrapped:** collect new expressions with no arguments ([#15908](https://github.com/angular/angular/issues/15908)) ([41cac9e](https://github.com/angular/angular/commit/41cac9e)), closes [#15906](https://github.com/angular/angular/issues/15906)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler:** Implement i18n XLIFF 2.0 serializer ([#14185](https://github.com/angular/angular/issues/14185)) ([a7d8edd](https://github.com/angular/angular/commit/a7d8edd)), closes [#11735](https://github.com/angular/angular/issues/11735)
|
||||
* **upgrade:** allow setting the angularjs lib at runtime ([#15168](https://github.com/angular/angular/issues/15168)) ([a75d056](https://github.com/angular/angular/commit/a75d056))
|
||||
* **upgrade:** allow setting the angularjs lib at runtime ([#15168](https://github.com/angular/angular/issues/15168)) ([4f172b0](https://github.com/angular/angular/commit/4f172b0))
|
||||
* **upgrade:** fixes for allow setting the angularjs lib at runtime ([bb6932d](https://github.com/angular/angular/commit/bb6932d))
|
||||
* add support for TS 2.3 ([5cf101f](https://github.com/angular/angular/commit/5cf101f))
|
||||
|
||||
|
||||
|
||||
<a name="4.1.0-beta.1"></a>
|
||||
# [4.1.0-beta.1](https://github.com/angular/angular/compare/4.1.0-beta.0...4.1.0-beta.1) (2017-04-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler:** fix inheritance for AOT with summaries ([#15583](https://github.com/angular/angular/issues/15583)) ([8ef621a](https://github.com/angular/angular/commit/8ef621a))
|
||||
* **language-service:** avoid throwing exceptions when reporting metadata errors ([7764c5c](https://github.com/angular/angular/commit/7764c5c))
|
||||
* **language-service:** detect when there isn't a tsconfig.json ([258d539](https://github.com/angular/angular/commit/258d539)), closes [#15874](https://github.com/angular/angular/issues/15874)
|
||||
* **language-service:** improve resilience to incomplete information ([71a8627](https://github.com/angular/angular/commit/71a8627))
|
||||
* **language-service:** initialize static reflector correctly ([fe0d02f](https://github.com/angular/angular/commit/fe0d02f)), closes [#15768](https://github.com/angular/angular/issues/15768)
|
||||
* **language-service:** parse extended i18n forms ([bde9771](https://github.com/angular/angular/commit/bde9771))
|
||||
* **language-service:** resolve any parameter types to any result ([5fbb0d0](https://github.com/angular/angular/commit/5fbb0d0))
|
||||
* **router:** fix query param parsing ([a487563](https://github.com/angular/angular/commit/a487563))
|
||||
* **router:** the preloader use the module from the loaded config ([6d12aa9](https://github.com/angular/angular/commit/6d12aa9))
|
||||
* **tsc-wrapped:** ensure valid path separators in metadata ([96aa236](https://github.com/angular/angular/commit/96aa236))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **animations:** Update types for TypeScript nullability support ([38d75d4](https://github.com/angular/angular/commit/38d75d4)), closes [#15870](https://github.com/angular/angular/issues/15870)
|
||||
* **benchpress:** Update types for TypeScript nullability support ([14669f2](https://github.com/angular/angular/commit/14669f2))
|
||||
* **common:** Update types for TypeScript nullability support ([d8b73e4](https://github.com/angular/angular/commit/d8b73e4))
|
||||
* **compiler:** Update types for TypeScript nullability support ([09d9f5f](https://github.com/angular/angular/commit/09d9f5f))
|
||||
* **language-service:** Update types for TypeScript nullability support ([540581d](https://github.com/angular/angular/commit/540581d))
|
||||
|
||||
|
||||
|
||||
<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.1.0-beta.0"></a>
|
||||
# [4.1.0-beta.0](https://github.com/angular/angular/compare/4.0.0...4.1.0-beta.0) (2017-03-29)
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler:** support ICU messages in XLIFF ([b8d5f87](https://github.com/angular/angular/commit/b8d5f87)), closes [#12636](https://github.com/angular/angular/issues/12636) [#15068](https://github.com/angular/angular/issues/15068)
|
||||
|
||||
Note: 4.1.0-beta.0 release also contains all the changes present in the 4.0.1 release.
|
||||
|
||||
|
||||
<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))
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
From 4.0.0 @angular/core uses a [`WeakMap`](https://github.com/angular/angular/commit/52b21275f4c2c26c46627f5648b41a33bb5c8283), a polyfill needs to be included for [browsers that do not support it natively](http://kangax.github.io/compat-table/es6/#test-WeakMap).
|
||||
|
||||
<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 +619,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 +642,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 +661,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 +706,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 +1011,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 +2580,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
|
||||
|
12
WORKSPACE
Normal file
12
WORKSPACE
Normal file
@ -0,0 +1,12 @@
|
||||
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
|
||||
|
||||
git_repository(
|
||||
name = "io_bazel_rules_typescript",
|
||||
remote = "https://github.com/bazelbuild/rules_typescript.git",
|
||||
tag = "0.0.3",
|
||||
)
|
||||
|
||||
load("@io_bazel_rules_typescript//:defs.bzl", "node_repositories", "yarn_install")
|
||||
|
||||
node_repositories()
|
||||
yarn_install(package_json = "//:package.json")
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"project": {
|
||||
"version": "1.0.0-beta.32.3",
|
||||
"name": "site"
|
||||
},
|
||||
"apps": [
|
||||
@ -9,20 +9,24 @@
|
||||
"outDir": "dist",
|
||||
"assets": [
|
||||
"assets",
|
||||
"content",
|
||||
"favicon.ico"
|
||||
"generated",
|
||||
"app/search/search-worker.js",
|
||||
"favicon.ico",
|
||||
"pwa-manifest.json",
|
||||
"google385281288605d160.html"
|
||||
],
|
||||
"index": "index.html",
|
||||
"main": "main.ts",
|
||||
"polyfills": "polyfills.ts",
|
||||
"test": "test.ts",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"tsconfig": "tsconfig.app.json",
|
||||
"testTsconfig": "tsconfig.spec.json",
|
||||
"prefix": "aio",
|
||||
"serviceWorker": false,
|
||||
"styles": [
|
||||
"styles.scss"
|
||||
],
|
||||
"scripts": [
|
||||
|
||||
],
|
||||
"environmentSource": "environments/environment.ts",
|
||||
"environments": {
|
||||
@ -38,12 +42,13 @@
|
||||
},
|
||||
"lint": [
|
||||
{
|
||||
"files": "src/**/*.ts",
|
||||
"project": "src/tsconfig.json"
|
||||
"project": "src/tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"files": "e2e/**/*.ts",
|
||||
"project": "e2e/tsconfig.json"
|
||||
"project": "src/tsconfig.spec.json"
|
||||
},
|
||||
{
|
||||
"project": "e2e/tsconfig.e2e.json"
|
||||
}
|
||||
],
|
||||
"test": {
|
||||
@ -53,19 +58,8 @@
|
||||
},
|
||||
"defaults": {
|
||||
"styleExt": "scss",
|
||||
"component": {},
|
||||
"prefixInterfaces": false,
|
||||
"inline": {
|
||||
"style": false,
|
||||
"template": false
|
||||
},
|
||||
"spec": {
|
||||
"class": false,
|
||||
"component": true,
|
||||
"directive": true,
|
||||
"module": false,
|
||||
"pipe": true,
|
||||
"service": true
|
||||
"component": {
|
||||
"inlineStyle": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
45
aio/.gitignore
vendored
Normal file
45
aio/.gitignore
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/out-tsc
|
||||
/src/generated
|
||||
/tmp
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
testem.log
|
||||
/typings
|
||||
yarn-error.log
|
||||
|
||||
# e2e
|
||||
/e2e/*.js
|
||||
/e2e/*.map
|
||||
protractor-results*.txt
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
108
aio/README.md
108
aio/README.md
@ -1,31 +1,105 @@
|
||||
# Site
|
||||
# Angular documentation project (https://angular.io)
|
||||
|
||||
This project was generated with [angular-cli](https://github.com/angular/angular-cli) version 1.0.0-beta.26.
|
||||
Everything in this folder is part of the documentation project. This includes
|
||||
|
||||
## Development server
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||
* the web site for displaying the documentation
|
||||
* the dgeni configuration for converting source files to rendered files that can be viewed in the web site.
|
||||
* the tooling for setting up examples for development; and generating plunkers and zip files from the examples.
|
||||
|
||||
## Code scaffolding
|
||||
## Developer tasks
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive/pipe/service/class/module`.
|
||||
We use `yarn` to manage the dependencies and to run build tasks.
|
||||
You should run all these tasks from the `angular/aio` folder.
|
||||
Here are the most important tasks you might need to use:
|
||||
|
||||
## Build
|
||||
* `yarn` - install all the dependencies.
|
||||
* `yarn setup` - Install all the dependencies, boilerplate, plunkers, zips and runs dgeni on the docs.
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
|
||||
* `yarn start` - run a development web server that watches the files; then builds the doc-viewer and reloads the page, as necessary.
|
||||
* `yarn lint` - check that the doc-viewer code follows our style rules.
|
||||
* `yarn test` - watch all the source files, for the doc-viewer, and run all the unit tests when any change.
|
||||
* `yarn e2e` - run all the e2e tests for the doc-viewer.
|
||||
|
||||
## Running unit tests
|
||||
* `yarn docs` - generate all the docs from the source files.
|
||||
* `yarn docs-watch` - watch the Angular source and the docs files and run a short-circuited doc-gen for the docs that changed.
|
||||
* `yarn docs-lint` - check that the doc gen code follows our style rules.
|
||||
* `yarn docs-test` - run the unit tests for the doc generation code.
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
* `yarn boilerplate:add` - generate all the boilerplate code for the examples, so that they can be run locally.
|
||||
* `yarn boilerplate:remove` - remove all the boilerplate code that was added via `yarn boilerplate:add`.
|
||||
* `yarn generate-plunkers` - generate the plunker files that are used by the `live-example` tags in the docs.
|
||||
* `yarn generate-zips` - generate the zip files from the examples. Zip available via the `live-example` tags in the docs.
|
||||
|
||||
## Running end-to-end tests
|
||||
* `yarn build-ie-polyfills` - generates a js file of polyfills that can be loaded in Internet Explorer.
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
Before running the tests make sure you are serving the app via `ng serve`.
|
||||
## Using ServiceWorker locally
|
||||
|
||||
## Deploying to GitHub Pages
|
||||
Since abb36e3cb, running `yarn start -- --prod` will no longer set up the ServiceWorker, which
|
||||
would require manually running `yarn sw-manifest` and `yarn sw-copy` (something that is not possible
|
||||
with webpack serving the files from memory).
|
||||
|
||||
Run `ng github-pages:deploy` to deploy to GitHub Pages.
|
||||
If you want to test ServiceWorker locally, you can use `yarn build` and serve the files in `dist/`
|
||||
with `yarn http-server -- dist -p 4200`.
|
||||
|
||||
## Further help
|
||||
For more details see #16745.
|
||||
|
||||
To get more help on the `angular-cli` use `ng help` or go check out the [Angular-CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
|
||||
|
||||
## Guide to authoring
|
||||
|
||||
There are two types of content in the documentatation:
|
||||
|
||||
* **API docs**: descriptions of the modules, classes, interfaces, decorators, etc that make up the Angular platform.
|
||||
API docs are generated directly from the source code.
|
||||
The source code is contained in TypeScript files, located in the `angular/packages` folder.
|
||||
Each API item may have a preceding comment, which contains JSDoc style tags and content.
|
||||
The content is written in markdown.
|
||||
|
||||
* **Other content**: guides, tutorials, and other marketing material.
|
||||
All other content is written using markdown in text files, located in the `angular/aio/content` folder.
|
||||
More specifically, there are sub-folders that contain particular types of content: guides, tutorial and marketing.
|
||||
|
||||
We use the [dgeni](https://github.com/angular/dgeni) tool to convert these files into docs that can be viewed in the doc-viewer.
|
||||
|
||||
### Generating the complete docs
|
||||
|
||||
The main task for generating the docs is `yarn docs`. This will process all the source files (API and other),
|
||||
extracting the documentation and generating JSON files that can be consumed by the doc-viewer.
|
||||
|
||||
### Partial doc generation for editors
|
||||
|
||||
Full doc generation can take up to one minute. That's too slow for efficient document creation and editing.
|
||||
|
||||
You can make small changes in a smart editor that displays formatted markdown:
|
||||
>In VS Code, _Cmd-K, V_ opens markdown preview in side pane; _Cmd-B_ toggles left sidebar
|
||||
|
||||
You also want to see those changes displayed properly in the doc viewer
|
||||
with a quick, edit/view cycle time.
|
||||
|
||||
For this purpose, use the `yarn docs-watch` task, which watches for changes to source files and only
|
||||
re-processes the the files necessary to generate the docs that are related to the file that has changed.
|
||||
Since this task takes shortcuts, it is much faster (often less than 1 second) but it won't produce full
|
||||
fidelity content. For example, links to other docs and code examples may not render correctly. This is
|
||||
most particularly noticed in links to other docs and in the embedded examples, which may not always render
|
||||
correctly.
|
||||
|
||||
The general setup is as follows:
|
||||
|
||||
* Open a terminal, ensure the dependencies are installed; run an initial doc generation; then start the doc-viewer:
|
||||
|
||||
```bash
|
||||
yarn
|
||||
yarn docs
|
||||
yarn start
|
||||
```
|
||||
|
||||
* Open a second terminal and start watching the docs
|
||||
|
||||
```bash
|
||||
yarn docs-watch
|
||||
```
|
||||
|
||||
* Open a browser at https://localhost:4200/ and navigate to the document on which you want to work.
|
||||
You can automatically open the browser by using `yarn start -- -o` in the first terminal.
|
||||
|
||||
* Make changes to the page's associated doc or example files. Every time a file is saved, the doc will
|
||||
be regenerated, the app will rebuild and the page will reload.
|
||||
|
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
|
165
aio/aio-builds-setup/dockerbuild/Dockerfile
Normal file
165
aio/aio-builds-setup/dockerbuild/Dockerfile
Normal file
@ -0,0 +1,165 @@
|
||||
# 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 \
|
||||
AIO_WWW_USER=www-data \
|
||||
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
|
||||
RUN echo "deb http://ftp.debian.org/debian jessie-backports main" | tee /etc/apt/sources.list.d/backports.list
|
||||
|
||||
|
||||
# Install packages
|
||||
RUN apt-get update -y && apt-get install -y \
|
||||
chkconfig \
|
||||
cron \
|
||||
dnsmasq \
|
||||
nano \
|
||||
nodejs \
|
||||
openssl \
|
||||
rsyslog \
|
||||
yarn
|
||||
RUN apt-get install -t jessie-backports -y nginx
|
||||
RUN yarn global add pm2@2
|
||||
|
||||
|
||||
# Set up log rotation
|
||||
COPY logrotate/* /etc/logrotate.d/
|
||||
RUN chmod 0644 /etc/logrotate.d/*
|
||||
|
||||
|
||||
# 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 sed -i -E "s|^user\s+\S+;|user $AIO_WWW_USER;|" /etc/nginx/nginx.conf
|
||||
RUN rm -f /etc/nginx/conf.d/*
|
||||
RUN rm -f /etc/nginx/sites-enabled/*
|
||||
|
||||
COPY nginx/aio-builds.conf /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$AIO_BUILDS_DIR|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_DOMAIN_NAME}}|$AIO_DOMAIN_NAME|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$AIO_LOCALCERTS_DIR|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_LOGS_DIR}}|$AIO_NGINX_LOGS_DIR|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$AIO_NGINX_PORT_HTTP|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$AIO_NGINX_PORT_HTTPS|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_HOSTNAME|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$AIO_UPLOAD_MAX_SIZE|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$AIO_UPLOAD_PORT|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
|
||||
COPY nginx/aio-builds.conf /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$TEST_AIO_BUILDS_DIR|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_DOMAIN_NAME}}|$TEST_AIO_DOMAIN_NAME|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$TEST_AIO_LOCALCERTS_DIR|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_LOGS_DIR}}|$TEST_AIO_NGINX_LOGS_DIR|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$TEST_AIO_NGINX_PORT_HTTP|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$TEST_AIO_NGINX_PORT_HTTPS|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_HOSTNAME|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$TEST_AIO_UPLOAD_MAX_SIZE|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$TEST_AIO_UPLOAD_PORT|g" /etc/nginx/conf.d/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
|
9
aio/aio-builds-setup/dockerbuild/logrotate/aio-misc
Normal file
9
aio/aio-builds-setup/dockerbuild/logrotate/aio-misc
Normal file
@ -0,0 +1,9 @@
|
||||
/var/log/aio/clean-up.log /var/log/aio/init.log /var/log/aio/verify-setup.log {
|
||||
compress
|
||||
create
|
||||
delaycompress
|
||||
missingok
|
||||
monthly
|
||||
notifempty
|
||||
rotate 6
|
||||
}
|
13
aio/aio-builds-setup/dockerbuild/logrotate/aio-nginx
Normal file
13
aio/aio-builds-setup/dockerbuild/logrotate/aio-nginx
Normal file
@ -0,0 +1,13 @@
|
||||
/var/log/aio/nginx/*.log /var/log/aio/nginx-test/*.log {
|
||||
compress
|
||||
create
|
||||
delaycompress
|
||||
missingok
|
||||
monthly
|
||||
notifempty
|
||||
rotate 6
|
||||
sharedscripts
|
||||
postrotate
|
||||
service nginx rotate >/dev/null 2>&1
|
||||
endscript
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
/var/log/aio/upload-server-*.log {
|
||||
compress
|
||||
copytruncate
|
||||
delaycompress
|
||||
missingok
|
||||
monthly
|
||||
notifempty
|
||||
rotate 6
|
||||
}
|
95
aio/aio-builds-setup/dockerbuild/nginx/aio-builds.conf
Normal file
95
aio/aio-builds-setup/dockerbuild/nginx/aio-builds.conf
Normal file
@ -0,0 +1,95 @@
|
||||
# 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 http2;
|
||||
listen [::]:{{$AIO_NGINX_PORT_HTTPS}} ssl http2;
|
||||
|
||||
ssl_certificate {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.crt;
|
||||
ssl_certificate_key {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.key;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
|
||||
|
||||
root {{$AIO_BUILDS_DIR}}/$pr/$sha;
|
||||
disable_symlinks on from=$document_root;
|
||||
index index.html;
|
||||
|
||||
gzip on;
|
||||
gzip_comp_level 7;
|
||||
gzip_types *;
|
||||
|
||||
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 http2 default_server;
|
||||
listen [::]:{{$AIO_NGINX_PORT_HTTPS}} ssl http2;
|
||||
|
||||
ssl_certificate {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.crt;
|
||||
ssl_certificate_key {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.key;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
|
||||
|
||||
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
|
2
aio/aio-builds-setup/dockerbuild/scripts-js/.gitignore
vendored
Normal file
2
aio/aio-builds-setup/dockerbuild/scripts-js/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/dist
|
||||
/node_modules
|
@ -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(409, `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: An error occurred.
|
||||
// - 2: The PR author is not a member.
|
||||
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, 2);
|
||||
}
|
||||
}).
|
||||
catch(err => onError(err, 1));
|
||||
}
|
||||
|
||||
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,34 @@
|
||||
// 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');
|
||||
const AIO_WWW_USER = getEnvVar('AIO_WWW_USER');
|
||||
|
||||
// Run
|
||||
process.setuid(AIO_WWW_USER); // TODO(gkalpak): Find more suitable way to run as `www-data`.
|
||||
_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} 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 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');
|
||||
const WWW_USER = getEnvVar('AIO_WWW_USER');
|
||||
|
||||
// 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 wwwUser() { return WWW_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.wwwUser} ${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.wwwUser} ${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.wwwUser} ${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.wwwUser} ${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(409)).
|
||||
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(409, /^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.wwwUser}'`, 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.wwwUser}`),
|
||||
])).
|
||||
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 clean-dist",
|
||||
"build": "tsc",
|
||||
"build-watch": "yarn tsc -- --watch",
|
||||
"clean-dist": "node --eval \"require('shelljs').rm('-rf', 'dist')\"",
|
||||
"dev": "concurrently --kill-others --raw --success first \"yarn build-watch\" \"yarn test-watch\"",
|
||||
"lint": "tslint --project tsconfig.json",
|
||||
"pre~~test-only": "yarn lint",
|
||||
"~~test-only": "node dist/test",
|
||||
"pretest": "yarn build",
|
||||
"test": "yarn ~~test-only",
|
||||
"pretest-watch": "yarn build",
|
||||
"test-watch": "nodemon --exec \"yarn ~~test-only\" --watch dist"
|
||||
},
|
||||
"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, 409, `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 1234567890 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
|
32
aio/aio-builds-setup/docs/_TOC.md
Normal file
32
aio/aio-builds-setup/docs/_TOC.md
Normal file
@ -0,0 +1,32 @@
|
||||
# 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
|
||||
- [Start docker container](vm-setup--start-docker-container.md)
|
||||
|
||||
|
||||
## Updating the docker container
|
||||
- [Update docker container](vm-setup--update-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/resource`
|
||||
- nginx maps the subdomain to the correct sub-directory 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.
|
58
aio/aio-builds-setup/docs/overview--scripts-and-commands.md
Normal file
58
aio/aio-builds-setup/docs/overview--scripts-and-commands.md
Normal file
@ -0,0 +1,58 @@
|
||||
# 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:
|
||||
|
||||
- `create-image.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 is 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.
|
||||
|
||||
- `update-preview-server.sh`
|
||||
Can be used for updating the docker container (and image) based on the latest changes checked out
|
||||
from a git repository. See [here](vm-setup--update-docker-container.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/create-image.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 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,75 @@
|
||||
# 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). Log files are
|
||||
rotated and retained for 6 months.
|
||||
|
||||
The following 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 \
|
||||
--detach \
|
||||
--dns 127.0.0.1 \
|
||||
--name <instance-name> \
|
||||
--publish 80:80 \
|
||||
--publish 443:443 \
|
||||
--restart unless-stopped \
|
||||
[--volume <host-cert-dir>:/etc/ssl/localcerts:ro] \
|
||||
--volume <host-secrets-dir>:/aio-secrets:ro \
|
||||
--volume <host-builds-dir>:/var/www/aio-builds \
|
||||
[--volume <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.
|
||||
--detach \
|
||||
|
||||
# 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 host VM (left) to ports of the docker container (right)
|
||||
--publish 80:80 \
|
||||
--publish 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.
|
||||
[--volume <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.)
|
||||
--volume <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.)
|
||||
--volume <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.
|
||||
[--volume <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 \
|
||||
--detach \
|
||||
--dns 127.0.0.1 \
|
||||
--name foobar-builds-1 \
|
||||
--publish 80:80 \
|
||||
--publish 443:443 \
|
||||
--restart unless-stopped \
|
||||
--volume /etc/ssl/localcerts:/etc/ssl/localcerts:ro \
|
||||
--volume /foobar-secrets:/aio-secrets:ro \
|
||||
--volume /mnt/disks/foobar-builds:/var/www/aio-builds \
|
||||
--volume /foobar-logs:/var/log/aio \
|
||||
foobar-builds
|
||||
```
|
@ -0,0 +1,52 @@
|
||||
# VM setup - Update docker container
|
||||
|
||||
|
||||
## Overview
|
||||
Assuming you have cloned the repository containing the preview server code (as described
|
||||
[here](vm-setup--create-docker-image.md)), you can use the `update-preview-server.sh` script on the
|
||||
VM host to update the preview server based on changes in the source code.
|
||||
|
||||
The script will pull the latest changes from the origin's master branch and examine if there have
|
||||
been any changes in files inside the preview server source code directory (see below). If there are,
|
||||
it will create a new image and verify that is works as expected. Finally, it will stop and remove
|
||||
the old docker container and image, create and new container based on the new image and start it.
|
||||
|
||||
The script assumes that the preview server source code is in the repository's
|
||||
`aio/aio-builds-setup/` directory and expects the following inputs:
|
||||
|
||||
- **$1**: `HOST_REPO_DIR`
|
||||
- **$2**: `HOST_LOCALCERTS_DIR`
|
||||
- **$3**: `HOST_SECRETS_DIR`
|
||||
- **$4**: `HOST_BUILDS_DIR`
|
||||
- **$5**: `HOST_LOGS_DIR`
|
||||
|
||||
See [here](vm-setup--create-host-dirs-and-files.md) for more info on what each input directory is
|
||||
used for.
|
||||
|
||||
**Note 1:** The script has to execute docker commands with `sudo`.
|
||||
|
||||
**Note 2:** Make sure the user that executes the script has access to update the repository
|
||||
|
||||
|
||||
## Run the script manually
|
||||
You may choose to manually run the script, when necessary. Example:
|
||||
|
||||
```
|
||||
update-preview-server.sh \
|
||||
/path/to/repo \
|
||||
/path/to/localcerts \
|
||||
/path/to/secrets \
|
||||
/path/to/builds \
|
||||
/path/to/logs
|
||||
```
|
||||
|
||||
|
||||
## Run the script automatically
|
||||
You may choose to automatically trigger the script, e.g. using a cronjob. For example, the following
|
||||
cronjob entry would run the script every hour and update the preview server (assuming the user has
|
||||
the necessary permissions):
|
||||
|
||||
```
|
||||
# Periodically check for changes and update the preview server (if necessary)
|
||||
*/30 * * * * /path/to/update-preview-server.sh /path/to/repo /path/to/localcerts /path/to/secrets /path/to/builds /path/to/logs
|
||||
```
|
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"
|
10
aio/aio-builds-setup/scripts/create-image.sh
Executable file
10
aio/aio-builds-setup/scripts/create-image.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
set -eux -o pipefail
|
||||
|
||||
# Set up env
|
||||
source "`dirname $0`/_env.sh"
|
||||
readonly defaultImageNameAndTag="aio-builds:latest"
|
||||
|
||||
# Create docker image
|
||||
readonly nameAndOptionalTag=${1:-$defaultImageNameAndTag}
|
||||
sudo docker build --tag $nameAndOptionalTag ${@:2} $DOCKERBUILD_DIR
|
12
aio/aio-builds-setup/scripts/test.sh
Executable file
12
aio/aio-builds-setup/scripts/test.sh
Executable file
@ -0,0 +1,12 @@
|
||||
#!/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
|
||||
)
|
20
aio/aio-builds-setup/scripts/travis-preverify-pr.sh
Executable file
20
aio/aio-builds-setup/scripts/travis-preverify-pr.sh
Executable file
@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
set -eux -o pipefail
|
||||
|
||||
# Set up env
|
||||
source "`dirname $0`/_env.sh"
|
||||
|
||||
# Build `scripts-js/`
|
||||
(
|
||||
cd "$SCRIPTS_JS_DIR"
|
||||
yarn install
|
||||
yarn build
|
||||
)
|
||||
|
||||
# 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"
|
70
aio/aio-builds-setup/scripts/update-preview-server.sh
Executable file
70
aio/aio-builds-setup/scripts/update-preview-server.sh
Executable file
@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eux -o pipefail
|
||||
exec 3>&1
|
||||
|
||||
echo "[`date`] - Updating the preview server..."
|
||||
|
||||
# Input
|
||||
readonly HOST_REPO_DIR=$1
|
||||
readonly HOST_LOCALCERTS_DIR=$2
|
||||
readonly HOST_SECRETS_DIR=$3
|
||||
readonly HOST_BUILDS_DIR=$4
|
||||
readonly HOST_LOGS_DIR=$5
|
||||
|
||||
# Constants
|
||||
readonly PROVISIONAL_IMAGE_NAME=aio-builds:provisional
|
||||
readonly LATEST_IMAGE_NAME=aio-builds:latest
|
||||
readonly CONTAINER_NAME=aio
|
||||
|
||||
# Run
|
||||
(
|
||||
cd "$HOST_REPO_DIR"
|
||||
|
||||
readonly lastDeployedCommit=$(git rev-parse HEAD)
|
||||
echo "Currently at commit $lastDeployedCommit."
|
||||
|
||||
# Pull latest master from origin.
|
||||
git pull origin master
|
||||
|
||||
# Do not update the server unless files inside `aio-builds-setup/` have changed
|
||||
# or the last attempt failed (identified by the provisional image still being around).
|
||||
readonly relevantChangedFilesCount=$(git diff --name-only $lastDeployedCommit...HEAD | grep -P "^aio/aio-builds-setup/" | wc -l)
|
||||
readonly lastAttemptFailed=$(sudo docker rmi "$PROVISIONAL_IMAGE_NAME" >> /dev/fd/3 && echo "true" || echo "false")
|
||||
if [[ $relevantChangedFilesCount -eq 0 ]] && [[ "$lastAttemptFailed" != "true" ]]; then
|
||||
echo "Skipping update because no relevant files have been touched."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create and verify a new docker image.
|
||||
aio/aio-builds-setup/scripts/create-image.sh "$PROVISIONAL_IMAGE_NAME"
|
||||
readonly imageVerified=$(sudo docker run --dns 127.0.0.1 --rm --volume $HOST_SECRETS_DIR:/aio-secrets:ro "$PROVISIONAL_IMAGE_NAME" /bin/bash -c "aio-init && aio-health-check && aio-verify-setup" >> /dev/fd/3 && echo "true" || echo "false")
|
||||
|
||||
if [[ "$imageVerified" != "true" ]]; then
|
||||
echo "Failed to verify new docker image. Aborting update!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove the old container and replace the docker image.
|
||||
sudo docker stop "$CONTAINER_NAME" || true
|
||||
sudo docker rm "$CONTAINER_NAME" || true
|
||||
sudo docker rmi "$LATEST_IMAGE_NAME" || true
|
||||
sudo docker tag "$PROVISIONAL_IMAGE_NAME" "$LATEST_IMAGE_NAME"
|
||||
sudo docker rmi "$PROVISIONAL_IMAGE_NAME"
|
||||
|
||||
# Create and start a docker container based on the new image.
|
||||
sudo docker run \
|
||||
--detach \
|
||||
--dns 127.0.0.1 \
|
||||
--name "$CONTAINER_NAME" \
|
||||
--publish 80:80 \
|
||||
--publish 443:443 \
|
||||
--restart unless-stopped \
|
||||
--volume $HOST_LOCALCERTS_DIR:/etc/ssl/localcerts:ro \
|
||||
--volume $HOST_SECRETS_DIR:/aio-secrets:ro \
|
||||
--volume $HOST_BUILDS_DIR:/var/www/aio-builds \
|
||||
--volume $HOST_LOGS_DIR:/var/log/aio \
|
||||
"$LATEST_IMAGE_NAME"
|
||||
|
||||
echo "The new docker image has been successfully deployed."
|
||||
)
|
@ -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.
|
File diff suppressed because it is too large
Load Diff
@ -1,623 +0,0 @@
|
||||
@title
|
||||
Ahead-of-Time Compilation
|
||||
|
||||
@intro
|
||||
Learn how to use Ahead-of-time compilation
|
||||
|
||||
@description
|
||||
This cookbook describes how to radically improve performance by compiling _Ahead of Time_ (AOT)
|
||||
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)
|
||||
|
||||
|
||||
{@a overview}
|
||||
|
||||
## Overview
|
||||
|
||||
An Angular application consist largely of components and their HTML templates.
|
||||
Before the browser can render the application,
|
||||
the components and templates must be converted to executable JavaScript by the _Angular compiler_.
|
||||
<a href="https://www.youtube.com/watch?v=kW9cJsvcsGo" target="_blank">Watch compiler author Tobias Bosch explain the Angular Compiler</a> at AngularConnect 2016.You can compile the app in the browser, at runtime, as the application loads, using the **_Just-in-Time_ (JIT) compiler**.
|
||||
This is the standard development approach shown throughout the documentation.
|
||||
It's great .. but it has shortcomings.
|
||||
|
||||
JIT compilation incurs a runtime performance penalty.
|
||||
Views take longer to render because of the in-browser compilation step.
|
||||
The application is bigger because it includes the Angular compiler
|
||||
and a lot of library code that the application won't actually need.
|
||||
Bigger apps take longer to transmit and are slower to load.
|
||||
|
||||
Compilation can uncover many component-template binding errors.
|
||||
JIT compilation discovers them at runtime which is later than we'd like.
|
||||
|
||||
The **_Ahead-of-Time_ (AOT) compiler** can catch template errors early and improve performance
|
||||
by compiling at build time as you'll learn in this chapter.
|
||||
|
||||
|
||||
|
||||
{@a aot-jit}
|
||||
|
||||
## _Ahead-of-time_ (AOT) vs _Just-in-time_ (JIT)
|
||||
|
||||
There is actually only one Angular compiler. The difference between AOT and JIT is a matter of timing and tooling.
|
||||
With AOT, the compiler runs once at build time using one set of libraries;
|
||||
With JIT it runs every time for every user at runtime using a different set of libraries.
|
||||
|
||||
### Why do AOT compilation?
|
||||
|
||||
*Faster rendering*
|
||||
|
||||
With AOT, the browser downloads a pre-compiled version of the application.
|
||||
The browser loads executable code so it can render the application immediately, without waiting to compile the app first.
|
||||
|
||||
*Fewer asynchronous requests*
|
||||
|
||||
The compiler _inlines_ external html templates and css style sheets within the application JavaScript,
|
||||
eliminating separate ajax requests for those source files.
|
||||
|
||||
*Smaller Angular framework download size*
|
||||
|
||||
There's no need to download the Angular compiler if the app is already compiled.
|
||||
The compiler is roughly half of Angular itself, so omitting it dramatically reduces the application payload.
|
||||
|
||||
|
||||
*Detect template errors earlier*
|
||||
|
||||
The AOT compiler detects and reports template binding errors during the build step
|
||||
before users can see them.
|
||||
|
||||
|
||||
*Better security*
|
||||
|
||||
AOT compiles HTML templates and components into JavaScript files long before they are served to the client.
|
||||
With no templates to read and no risky client-side HTML or JavaScript evaluation,
|
||||
there are fewer opportunities for injection attacks.
|
||||
|
||||
|
||||
{@a compile}
|
||||
|
||||
## Compile with AOT
|
||||
|
||||
### Prepare for offline compilation
|
||||
Take the <a href='../guide/setup.html'>Setup</a> as a starting point.
|
||||
A few minor changes to the lone `app.component` lead to these two class and html files:
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="src/app/app.component.html">
|
||||
{@example 'cb-aot-compiler/ts/src/app/app.component.html'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="src/app/app.component.ts">
|
||||
{@example 'cb-aot-compiler/ts/src/app/app.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
Install a few new npm dependencies with the following command:
|
||||
<code-example language="none" class="code-shell">
|
||||
npm install @angular/compiler-cli @angular/platform-server --save
|
||||
</code-example>
|
||||
|
||||
You will run the `ngc` compiler provided in the `@angular/compiler-cli` npm package
|
||||
instead of the TypeScript compiler (`tsc`).
|
||||
|
||||
`ngc` is a drop-in replacement for `tsc` and is configured much the same way.
|
||||
|
||||
`ngc` requires its own `tsconfig.json` with AOT-oriented settings.
|
||||
Copy the original `src/tsconfig.json` to a file called `tsconfig-aot.json` (on the project root),
|
||||
then modify it to look as follows.
|
||||
|
||||
|
||||
{@example 'cb-aot-compiler/ts/tsconfig-aot.json'}
|
||||
|
||||
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.
|
||||
|
||||
What's really new is the `ngc` section at the bottom called `angularCompilerOptions`.
|
||||
Its `"genDir"` property tells the compiler
|
||||
to store the compiled output files in a new `aot` folder.
|
||||
|
||||
The `"skipMetadataEmit" : true` property prevents the compiler from generating metadata files with the compiled application.
|
||||
Metadata files are not necessary when targeting TypeScript files, so there is no reason to include them.
|
||||
***Component-relative Template URLS***
|
||||
|
||||
The AOT compiler requires that `@Component` URLS for external templates and css files be _component-relative_.
|
||||
That means that the value of `@Component.templateUrl` is a URL value _relative_ to the component class file.
|
||||
For example, an `'app.component.html'` URL means that the template file is a sibling of its companion `app.component.ts` file.
|
||||
|
||||
While JIT app URLs are more flexible, stick with _component-relative_ URLs for compatibility with AOT compilation.
|
||||
|
||||
JIT-compiled applications that use the SystemJS loader and _component-relative_ URLs *must set the* `@Component.moduleId` *property to* `module.id`.
|
||||
The `module` object is undefined when an AOT-compiled app runs.
|
||||
The app fails with a null reference error unless you assign a global `module` value in the `index.html` like this:
|
||||
|
||||
{@example 'cb-aot-compiler/ts/src/index.html' region='moduleId'}
|
||||
|
||||
|
||||
Setting a global `module` is a temporary expedient.
|
||||
### Compiling the application
|
||||
|
||||
Initiate AOT compilation from the command line using the previously installed `ngc` compiler by executing:
|
||||
<code-example language="none" class="code-shell">
|
||||
node_modules/.bin/ngc -p tsconfig-aot.json
|
||||
</code-example>
|
||||
|
||||
|
||||
Windows users should surround the `ngc` command in double quotes:
|
||||
<code-example format='.'>
|
||||
"node_modules/.bin/ngc" -p tsconfig-aot.json
|
||||
</code-example>
|
||||
|
||||
`ngc` expects the `-p` switch to point to a `tsconfig.json` file or a folder containing a `tsconfig.json` file.
|
||||
|
||||
After `ngc` completes, look for a collection of _NgFactory_ files in the `aot` folder (the folder specified as `genDir` in `tsconfig-aot.json`).
|
||||
|
||||
These factory files are essential to the compiled application.
|
||||
Each component factory creates an instance of the component at runtime by combining the original class file
|
||||
and a JavaScript representation of the component's template.
|
||||
Note that the original component class is still referenced internally by the generated factory.
|
||||
The curious can open the `aot/app.component.ngfactory.ts` to see the original Angular template syntax
|
||||
in its intermediate, compiled-to-TypeScript form.
|
||||
|
||||
JIT compilation generates these same _NgFactories_ in memory where they are largely invisible.
|
||||
AOT compilation reveals them as separate, physical files.
|
||||
|
||||
|
||||
~~~ {.alert.is-important}
|
||||
|
||||
Do not edit the _NgFactories_! Re-compilation replaces these files and all edits will be lost.
|
||||
|
||||
|
||||
~~~
|
||||
|
||||
|
||||
|
||||
{@a bootstrap}
|
||||
|
||||
## Bootstrap
|
||||
|
||||
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").
|
||||
|
||||
Open `main.ts` and convert it to AOT compilation.
|
||||
Switch from the `platformBrowserDynamic.bootstrap` used in JIT compilation to
|
||||
`platformBrowser().bootstrapModuleFactory` and pass in the AOT-generated `AppModuleNgFactory`.
|
||||
|
||||
Here is AOT bootstrap in `main.ts` next to the original JIT version:
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="src/main.ts">
|
||||
{@example 'cb-aot-compiler/ts/src/main.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="src/main-jit.ts">
|
||||
{@example 'cb-aot-compiler/ts/src/main-jit.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
Be sure to recompile with `ngc`!
|
||||
|
||||
|
||||
{@a tree-shaking}
|
||||
## Tree Shaking
|
||||
|
||||
AOT compilation sets the stage for further optimization through a process called _Tree Shaking_.
|
||||
A Tree Shaker walks the dependency graph, top to bottom, and _shakes out_ unused code like
|
||||
dead needles in a Christmas tree.
|
||||
|
||||
Tree Shaking can greatly reduce the downloaded size of the application
|
||||
by removing unused portions of both source and library code.
|
||||
In fact, most of the reduction in small apps comes from removing unreferenced Angular features.
|
||||
|
||||
For example, this demo application doesn't use anything from the `@angular/forms` library.
|
||||
There is no reason to download Forms-related Angular code and tree shaking ensures that you don't.
|
||||
|
||||
Tree Shaking and AOT compilation are separate steps.
|
||||
Tree Shaking can only target JavaScript code.
|
||||
AOT compilation converts more of the application to JavaScript,
|
||||
which in turn makes more of the application "Tree Shakable".
|
||||
|
||||
### Rollup
|
||||
|
||||
This cookbook illustrates a Tree Shaking utility called _Rollup_.
|
||||
|
||||
Rollup statically analyzes the application by following the trail of `import` and `export` statements.
|
||||
It produces a final code _bundle_ that excludes code that is exported, but never imported.
|
||||
|
||||
Rollup can only Tree Shake `ES2015` modules which have `import` and `export` statements.
|
||||
Recall that `tsconfig-aot.json` is configured to produce `ES2015` modules.
|
||||
It's not important that the code itself be written with `ES2015` syntax such as `class` and `const`.
|
||||
What matters is that the code uses ES `import` and `export` statements rather than `require` statements.Install the Rollup dependencies with this command:
|
||||
<code-example format='.'>
|
||||
npm install rollup rollup-plugin-node-resolve rollup-plugin-commonjs rollup-plugin-uglify --save-dev
|
||||
</code-example>
|
||||
|
||||
Next, create a configuration file (`rollup-config.js`)
|
||||
in the project root directory to tell Rollup how to process the application.
|
||||
The cookbook configuration file looks like this.
|
||||
|
||||
|
||||
{@example 'cb-aot-compiler/ts/rollup-config.js'}
|
||||
|
||||
It tells Rollup that the app entry point is `src/app/main.js` .
|
||||
The `dest` attribute tells Rollup to create a bundle called `build.js` in the `dist` folder.
|
||||
It overrides the default `onwarn` method in order to skip annoying messages about the AOT compiler's use of the `this` keyword.
|
||||
|
||||
Then there are plugins.
|
||||
### Rollup Plugins
|
||||
|
||||
Optional plugins filter and transform the Rollup inputs and output.
|
||||
|
||||
*RxJS*
|
||||
Rollup expects application source code to use `ES2015` modules.
|
||||
Not all external dependencies are published as `ES2015` modules.
|
||||
In fact, most are not. Many of them are published as _CommonJS_ modules.
|
||||
|
||||
The _RxJs_ observable library is an essential Angular dependency published as an ES5 JavaScript _CommonJS_ module.
|
||||
|
||||
Luckily there is a Rollup plugin that modifies _RxJs_
|
||||
to use the ES `import` and `export` statements that Rollup requires.
|
||||
Rollup then preserves in the final bundle the parts of `RxJS` referenced by the application.
|
||||
|
||||
|
||||
{@example 'cb-aot-compiler/ts/rollup-config.js' region='commonjs'}
|
||||
|
||||
*Minification*
|
||||
|
||||
Rollup Tree Shaking reduces code size considerably. Minification makes it smaller still.
|
||||
This cookbook relies on the _uglify_ Rollup plugin to minify and mangle the code.
|
||||
|
||||
|
||||
{@example 'cb-aot-compiler/ts/rollup-config.js' region='uglify'}
|
||||
|
||||
|
||||
In a production setting, you would also enable gzip on the web server to compress
|
||||
the code into an even smaller package going over the wire.
|
||||
### Run Rollup
|
||||
Execute the Rollup process with this command:
|
||||
<code-example format='.'>
|
||||
node_modules/.bin/rollup -c rollup-config.js
|
||||
</code-example>
|
||||
|
||||
|
||||
Windows users should surround the `rollup` command in double quotes:
|
||||
<code-example format='.'>
|
||||
"node_modules/.bin/rollup" -c rollup-config.js
|
||||
</code-example>
|
||||
|
||||
|
||||
|
||||
{@a load}
|
||||
|
||||
## Load the Bundle
|
||||
|
||||
Loading the generated application bundle does not require a module loader like SystemJS.
|
||||
Remove the scripts that concern SystemJS.
|
||||
Instead, load the bundle file using a single `script` tag **_after_** the `</body>` tag:
|
||||
|
||||
|
||||
{@example 'cb-aot-compiler/ts/src/index.html' region='bundle'}
|
||||
|
||||
|
||||
|
||||
{@a serve}
|
||||
|
||||
## Serve the app
|
||||
|
||||
You'll need a web server to host the application.
|
||||
Use the same _Lite Server_ employed elsewhere in the documentation:
|
||||
<code-example language="none" class="code-shell">
|
||||
npm run lite
|
||||
</code-example>
|
||||
|
||||
The server starts, launches a browser, and the app should appear.
|
||||
|
||||
|
||||
{@a source-code}
|
||||
|
||||
## AOT QuickStart Source Code
|
||||
|
||||
Here's the pertinent source code:
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="src/app/app.component.html">
|
||||
{@example 'cb-aot-compiler/ts/src/app/app.component.html'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="src/app/app.component.ts">
|
||||
{@example 'cb-aot-compiler/ts/src/app/app.component.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="src/main.ts">
|
||||
{@example 'cb-aot-compiler/ts/src/main.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="src/index.html">
|
||||
{@example 'cb-aot-compiler/ts/src/index.html'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="tsconfig-aot.json">
|
||||
{@example 'cb-aot-compiler/ts/tsconfig-aot.json'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="rollup-config.js">
|
||||
{@example 'cb-aot-compiler/ts/rollup-config.js'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
|
||||
|
||||
{@a workflow}
|
||||
|
||||
## Workflow and convenience script
|
||||
|
||||
You'll rebuild the AOT version of the application every time you make a change.
|
||||
Those _npm_ commands are long and difficult to remember.
|
||||
|
||||
Add the following _npm_ convenience script to the `package.json` so you can compile and rollup in one command.Open a terminal window and try it.
|
||||
<code-example language="none" class="code-shell">
|
||||
npm run build:aot
|
||||
|
||||
</code-example>
|
||||
|
||||
|
||||
|
||||
{@a run-jit}
|
||||
### And JIT too!
|
||||
|
||||
AOT compilation and rollup together take several seconds.
|
||||
You may be able to develop iteratively a little faster with SystemJS and JIT.
|
||||
The same source code can be built both ways. Here's one way to do that.
|
||||
|
||||
* Make a copy of `index.html` and call it `index-jit.html`.
|
||||
* Delete the script at the bottom of `index-jit.html` that loads `bundle.js`
|
||||
* Restore the SystemJS scripts like this:
|
||||
|
||||
{@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)
|
||||
Open a _different_ terminal window and enter.
|
||||
<code-example language="none" class="code-shell">
|
||||
npm start
|
||||
</code-example>
|
||||
|
||||
That compiles the app with JIT and launches the server.
|
||||
The server loads `index.html` which is still the AOT version (confirm in the browser console).
|
||||
Change the address bar to `index-jit.html` and it loads the JIT version (confirm in the browser console).
|
||||
|
||||
Develop as usual.
|
||||
The server and TypeScript compiler are in "watch mode" so your changes are reflected immediately in the browser.
|
||||
|
||||
To see those changes in AOT, switch to the original terminal and re-run `npm run build:aot`.
|
||||
When it finishes, go back to the browser and back-button to the AOT version in the (default) `index.html`.
|
||||
|
||||
Now you can develop JIT and AOT, side-by-side.
|
||||
|
||||
|
||||
|
||||
{@a toh}
|
||||
|
||||
## Tour of Heroes
|
||||
|
||||
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).
|
||||
|
||||
### JIT in development, AOT in production
|
||||
|
||||
Today AOT compilation and Tree Shaking take more time than is practical for development. That will change soon.
|
||||
For now, it's best to JIT compile in development and switch to AOT compilation before deploying to production.
|
||||
|
||||
Fortunately, the source code can be compiled either way without change _if_ you account for a few key differences.
|
||||
|
||||
***index.html***
|
||||
|
||||
The JIT and AOT apps require their own `index.html` files because they setup and launch so differently.
|
||||
|
||||
Here they are for comparison:
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="aot/index.html (AOT)">
|
||||
{@example 'toh-6/ts/aot/index.html'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="src/index.html (JIT)">
|
||||
{@example 'toh-6/ts/src/index.html'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
The JIT version relies on `SystemJS` to load individual modules.
|
||||
Its scripts appear in its `index.html`.
|
||||
|
||||
The AOT version loads the entire application in a single script, `aot/dist/build.js`.
|
||||
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,
|
||||
are evident in these `main` files which can and should reside in the same folder:
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="main-aot.ts (AOT)">
|
||||
{@example 'toh-6/ts/src/main-aot.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="main.ts (JIT)">
|
||||
{@example 'toh-6/ts/src/main.ts'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
***TypeScript configuration***
|
||||
|
||||
JIT-compiled applications transpile to `commonjs` modules.
|
||||
AOT-compiled applications transpile to _ES2015_/_ES6_ modules to facilitate Tree Shaking.
|
||||
AOT requires its own TypeScript configuration settings as well.
|
||||
|
||||
You'll need separate TypeScript configuration files such as these:
|
||||
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="tsconfig-aot.json (AOT)">
|
||||
{@example 'toh-6/ts/tsconfig-aot.json'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="src/tsconfig.json (JIT)">
|
||||
{@example 'toh-6/ts/src/tsconfig.1.json'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
|
||||
|
||||
~~~ {.callout.is-helpful}
|
||||
|
||||
|
||||
<header>
|
||||
@Types and node modules
|
||||
</header>
|
||||
|
||||
In the file structure of _this particular sample project_,
|
||||
the `node_modules` folder happens to be two levels up from the project root.
|
||||
Therefore, `"typeRoots"` must be set to `"../../node_modules/@types/"`.
|
||||
|
||||
In a more typical project, `node_modules` would be a sibling of `tsconfig-aot.json`
|
||||
and `"typeRoots"` would be set to `"node_modules/@types/"`.
|
||||
Edit your `tsconfig-aot.json` to fit your project's file structure.
|
||||
|
||||
|
||||
~~~
|
||||
|
||||
### Tree Shaking
|
||||
|
||||
Rollup does the Tree Shaking as before.
|
||||
|
||||
|
||||
{@example 'toh-6/ts/rollup-config.js'}
|
||||
|
||||
### Running the application
|
||||
|
||||
|
||||
~~~ {.alert.is-important}
|
||||
|
||||
The general audience instructions for running the AOT build of the Tour of Heroes app are not ready.
|
||||
|
||||
The following instructions presuppose that you have cloned the
|
||||
<a href="https://github.com/angular/angular.io" target="_blank">angular.io</a>
|
||||
github repository and prepared it for development as explained in the repo's README.md.
|
||||
|
||||
The _Tour of Heroes_ source code is in the `public/docs/_examples/toh-6/ts` folder.
|
||||
|
||||
~~~
|
||||
|
||||
Run the JIT-compiled app with `npm start` as for all other JIT examples.
|
||||
|
||||
Compiling with AOT presupposes certain supporting files, most of them discussed above.
|
||||
<md-tab-group>
|
||||
|
||||
<md-tab label="src/index.html">
|
||||
{@example 'toh-6/ts/src/index.html'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="copy-dist-files.js">
|
||||
{@example 'toh-6/ts/copy-dist-files.js'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="rollup-config.js">
|
||||
{@example 'toh-6/ts/rollup-config.js'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
<md-tab label="tsconfig-aot.json">
|
||||
{@example 'toh-6/ts/tsconfig-aot.json'}
|
||||
</md-tab>
|
||||
|
||||
|
||||
</md-tab-group>
|
||||
|
||||
Extend the `scripts` section of the `package.json` with these npm scripts:Copy the AOT distribution files into the `/aot` folder with the node script:
|
||||
<code-example language="none" class="code-shell">
|
||||
node copy-dist-files
|
||||
</code-example>
|
||||
|
||||
|
||||
You won't do that again until there are updates to `zone.js` or the `core-js` shim for old browsers.Now AOT-compile the app and launch it with the `lite` server:
|
||||
<code-example language="none" class="code-shell">
|
||||
npm run build:aot && npm run serve:aot
|
||||
|
||||
</code-example>
|
||||
|
||||
### Inspect the Bundle
|
||||
|
||||
It's fascinating to see what the generated JavaScript bundle looks like after Rollup.
|
||||
The code is minified, so you won't learn much from inspecting the bundle directly.
|
||||
But the <a href="https://github.com/danvk/source-map-explorer/blob/master/README.md" target="_blank">source-map-explorer</a>
|
||||
tool can be quite revealing.
|
||||
|
||||
Install it:
|
||||
<code-example language="none" class="code-shell">
|
||||
npm install source-map-explorer --save-dev
|
||||
</code-example>
|
||||
|
||||
Run the following command to generate the map.
|
||||
|
||||
<code-example language="none" class="code-shell">
|
||||
node_modules/.bin/source-map-explorer aot/dist/build.js
|
||||
|
||||
</code-example>
|
||||
|
||||
The `source-map-explorer` analyzes the source map generated with the bundle and draws a map of all dependencies,
|
||||
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">
|
||||
<figure class='image-display'>
|
||||
<img src="/resources/images/cookbooks/aot-compiler/toh6-bundle.png" alt="TOH-6-bundle"> </img>
|
||||
</figure>
|
||||
|
||||
</a>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user