mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-13 19:45:26 +00:00
Compare commits
569 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0588ed9ab | ||
|
|
01dd08849a | ||
|
|
534a96214e | ||
|
|
9725767fe2 | ||
|
|
90ec61f14d | ||
|
|
4f9919e95c | ||
|
|
b089ca42ff | ||
|
|
6cb4030b2b | ||
|
|
a89f7b01bb | ||
|
|
96a068245b | ||
|
|
0862d0e2c8 | ||
|
|
a26f8600c1 | ||
|
|
f9d1dc5d3f | ||
|
|
52c4106d2c | ||
|
|
0fd332135e | ||
|
|
9bb8bfa884 | ||
|
|
01c0097acb | ||
|
|
dcf726a89b | ||
|
|
9e60a71cf5 | ||
|
|
3a9d457389 | ||
|
|
1e9cd84854 | ||
|
|
d2cda4b6c0 | ||
|
|
0dd2d2eb4a | ||
|
|
6bf3dd04dd | ||
|
|
6d5aa9a35d | ||
|
|
7be7e1eb9f | ||
|
|
ca7973465b | ||
|
|
f7c88df3be | ||
|
|
74c66b784f | ||
|
|
9e9bd10333 | ||
|
|
8858301fed | ||
|
|
9540383796 | ||
|
|
b5edf0edd5 | ||
|
|
6dad750942 | ||
|
|
eee9d21900 | ||
|
|
55505571f8 | ||
|
|
4fe3c96c2d | ||
|
|
411e2594af | ||
|
|
e0de230e13 | ||
|
|
c637687922 | ||
|
|
57fe5bdfa2 | ||
|
|
b2b476abb1 | ||
|
|
ad0ebb8a69 | ||
|
|
130eafbd0a | ||
|
|
9d2ce6d852 | ||
|
|
a476d21c9b | ||
|
|
30b4b6660b | ||
|
|
cfd10c6f61 | ||
|
|
82a4973943 | ||
|
|
0609a92f3a | ||
|
|
37780656fd | ||
|
|
351ab13d29 | ||
|
|
795da23418 | ||
|
|
1f1211b7b4 | ||
|
|
acafeac1cc | ||
|
|
82e637ea7c | ||
|
|
14ace57612 | ||
|
|
84a0ae562f | ||
|
|
8870b58766 | ||
|
|
54506c1eb6 | ||
|
|
4f6b2dcb7c | ||
|
|
0a87204593 | ||
|
|
720cf8a859 | ||
|
|
ca034d0857 | ||
|
|
96b9d19715 | ||
|
|
293da8bcd1 | ||
|
|
7150c7e088 | ||
|
|
a85e1424e3 | ||
|
|
0628ea99ab | ||
|
|
3d7cfbbf3d | ||
|
|
66be631572 | ||
|
|
c964500118 | ||
|
|
91ea0335ae | ||
|
|
11d3da3aed | ||
|
|
7fefc59929 | ||
|
|
3521eff4f4 | ||
|
|
b7fce99438 | ||
|
|
85c86b5562 | ||
|
|
81bbfce448 | ||
|
|
6e8ba295d4 | ||
|
|
0168d8b70c | ||
|
|
548de702ac | ||
|
|
7ba1a0a9fa | ||
|
|
3bab780f88 | ||
|
|
263bb08334 | ||
|
|
3445ec66ac | ||
|
|
4940c5d5be | ||
|
|
83c8d7f855 | ||
|
|
fdfe282130 | ||
|
|
b5fbc7e0e8 | ||
|
|
64eb3f856a | ||
|
|
18d8e61ecb | ||
|
|
2a635f92a9 | ||
|
|
44bcbeb7d6 | ||
|
|
4023831b18 | ||
|
|
969169ae20 | ||
|
|
79fa15da3d | ||
|
|
ddc08d94cc | ||
|
|
f587161d7d | ||
|
|
d486542a8b | ||
|
|
c990c771d8 | ||
|
|
90f3393ebf | ||
|
|
76ce7f227f | ||
|
|
1f9efd353c | ||
|
|
3499631e8e | ||
|
|
2a825269e9 | ||
|
|
83f7102f6b | ||
|
|
e4319bee0e | ||
|
|
da540ef15f | ||
|
|
c00af6dad0 | ||
|
|
92206742d4 | ||
|
|
14fc6b3176 | ||
|
|
e2193a6080 | ||
|
|
f66c7dcb0d | ||
|
|
06f1a0e6c0 | ||
|
|
7726325b4b | ||
|
|
b6bc90cea9 | ||
|
|
cfba9b2168 | ||
|
|
b25c642bf1 | ||
|
|
82a023c302 | ||
|
|
f97400caaa | ||
|
|
03af9b1a16 | ||
|
|
02cef046aa | ||
|
|
c2aeac34b6 | ||
|
|
abd2f7fd39 | ||
|
|
e7cf9bf1b6 | ||
|
|
1fdd0e3ad2 | ||
|
|
af0a7e04f9 | ||
|
|
ed19a19fed | ||
|
|
8ebfcc9a54 | ||
|
|
af2a792508 | ||
|
|
14e809af6e | ||
|
|
03c5f5b446 | ||
|
|
6655b1b49d | ||
|
|
eebf076f23 | ||
|
|
198be28f4b | ||
|
|
4f720446b2 | ||
|
|
e69cb3639a | ||
|
|
1b1a86764f | ||
|
|
f50f83c4a4 | ||
|
|
eadefd996b | ||
|
|
ab52aabd1d | ||
|
|
c5a80543b3 | ||
|
|
cfd95ac024 | ||
|
|
73ff95c677 | ||
|
|
382fe6b7b9 | ||
|
|
3b714c0de3 | ||
|
|
28186227a9 | ||
|
|
2ac0f2bf3d | ||
|
|
4991f3b359 | ||
|
|
65128118c7 | ||
|
|
3eced6fa4d | ||
|
|
c4aee66c32 | ||
|
|
410c52df41 | ||
|
|
ffd56e2a20 | ||
|
|
1e5a87cf71 | ||
|
|
281e183c61 | ||
|
|
3fe7b55955 | ||
|
|
464924ed67 | ||
|
|
8108642845 | ||
|
|
e5d7d8ab2b | ||
|
|
d3e560325d | ||
|
|
64e5065aa5 | ||
|
|
cb45bb0fbe | ||
|
|
f39455014a | ||
|
|
4fe8df3696 | ||
|
|
4fdc7055c1 | ||
|
|
cca417a85a | ||
|
|
635c13a175 | ||
|
|
2e3aef8b21 | ||
|
|
44736754ad | ||
|
|
a399d87d9b | ||
|
|
a68c62ce91 | ||
|
|
1b9b559d82 | ||
|
|
abcac75826 | ||
|
|
4118c566d1 | ||
|
|
0b76f12394 | ||
|
|
e51cb85c7c | ||
|
|
6ae9a5e7c5 | ||
|
|
52f40f9e63 | ||
|
|
5e59855a15 | ||
|
|
18acef3190 | ||
|
|
d7f6ca8b4d | ||
|
|
00321932ef | ||
|
|
7eb1ec727c | ||
|
|
3f586e19a1 | ||
|
|
05b702938f | ||
|
|
3026e7c36e | ||
|
|
27a5aed83e | ||
|
|
bb04af41bd | ||
|
|
9403799393 | ||
|
|
472c0c198a | ||
|
|
02256ae214 | ||
|
|
e2d325ec9f | ||
|
|
954bcba333 | ||
|
|
709c9992e3 | ||
|
|
9773781262 | ||
|
|
48b1fafc33 | ||
|
|
d1624add66 | ||
|
|
e2206d0e0d | ||
|
|
9cd4da4c39 | ||
|
|
6ded1a5975 | ||
|
|
584ddd1c85 | ||
|
|
4071acf7c0 | ||
|
|
e8773d3196 | ||
|
|
01a519a4d9 | ||
|
|
d9ad1d1b10 | ||
|
|
0826a58995 | ||
|
|
6a74097b0c | ||
|
|
278be67228 | ||
|
|
64921dfc3c | ||
|
|
c8962ccf8c | ||
|
|
664c093391 | ||
|
|
4375feac83 | ||
|
|
4d7d448597 | ||
|
|
2e2b8dcd83 | ||
|
|
b915bab070 | ||
|
|
8530d06d00 | ||
|
|
a43764a971 | ||
|
|
4f6efbda79 | ||
|
|
5573258a10 | ||
|
|
2185f89669 | ||
|
|
e30842caa8 | ||
|
|
ff24877d8f | ||
|
|
9cf90ce01d | ||
|
|
e7f9ddae86 | ||
|
|
ffa1e1f704 | ||
|
|
80bb958b47 | ||
|
|
532f559fc5 | ||
|
|
0bd1b0b8e2 | ||
|
|
10c5ff4169 | ||
|
|
273823dfd7 | ||
|
|
8f064fe5ac | ||
|
|
f29e7efbd6 | ||
|
|
e66e3cd7eb | ||
|
|
ff415b391b | ||
|
|
91182ad0a2 | ||
|
|
467041a3cd | ||
|
|
520949c5e1 | ||
|
|
8c07d9d2ac | ||
|
|
7bd684a80e | ||
|
|
cbb5e8755b | ||
|
|
7ef72dbb77 | ||
|
|
87ff9c2aeb | ||
|
|
2d1f535314 | ||
|
|
cd1b53b80d | ||
|
|
0a7be736c0 | ||
|
|
5ee53a98c6 | ||
|
|
0e8ed6612c | ||
|
|
5ec748b95d | ||
|
|
64b16715cd | ||
|
|
9ea5e5e251 | ||
|
|
68b78f56d6 | ||
|
|
68e93a8fa0 | ||
|
|
e377515867 | ||
|
|
99ae39b8a8 | ||
|
|
1b489f4687 | ||
|
|
91f2c6e49c | ||
|
|
f5e07e9f78 | ||
|
|
17297a9794 | ||
|
|
9ef4f33e38 | ||
|
|
30966ee7f2 | ||
|
|
ae90f774e1 | ||
|
|
0706b5f50a | ||
|
|
499b377bfd | ||
|
|
1baeb79d4b | ||
|
|
0cc5ff95d6 | ||
|
|
e90525c1e8 | ||
|
|
6ccae0cd33 | ||
|
|
8b60d568dc | ||
|
|
64c2394c9d | ||
|
|
b28037291a | ||
|
|
bf9cbe2f3b | ||
|
|
2c507b5a2e | ||
|
|
4a92fbbbfa | ||
|
|
b16440ff0f | ||
|
|
64223b572b | ||
|
|
1b6dfd3580 | ||
|
|
c0b833862a | ||
|
|
45f4c47a3e | ||
|
|
48b87edb5b | ||
|
|
f9f283603e | ||
|
|
a56ee6bdb7 | ||
|
|
a18af2a473 | ||
|
|
fe5a8fb1e7 | ||
|
|
57a26e3511 | ||
|
|
bebb0dd595 | ||
|
|
afba0491ed | ||
|
|
350d37e210 | ||
|
|
96fc19e939 | ||
|
|
5be3809d9e | ||
|
|
15cc4bf296 | ||
|
|
c7cce985e3 | ||
|
|
e00b315dec | ||
|
|
afcd643035 | ||
|
|
7cc2a03aae | ||
|
|
002718b609 | ||
|
|
45d216c64d | ||
|
|
3d69d483d7 | ||
|
|
54da524467 | ||
|
|
ee95240ca6 | ||
|
|
74d5d1f9c0 | ||
|
|
8c2688b1be | ||
|
|
e170e4af9b | ||
|
|
bc31c73c33 | ||
|
|
7798f8f684 | ||
|
|
70fd7038b0 | ||
|
|
c6af2ddfa3 | ||
|
|
65d0172abe | ||
|
|
1e22031765 | ||
|
|
46bf8f2b78 | ||
|
|
553db81b7b | ||
|
|
67cc29ed38 | ||
|
|
36f930f489 | ||
|
|
3b098caa6e | ||
|
|
d63b9437b5 | ||
|
|
e51c733e3f | ||
|
|
708fe4c74b | ||
|
|
4259fc8e58 | ||
|
|
f05a8e52a0 | ||
|
|
76cb6d97f3 | ||
|
|
5d0b7af47b | ||
|
|
bb737eeb32 | ||
|
|
86dee17040 | ||
|
|
01a709e017 | ||
|
|
f87f0e3b80 | ||
|
|
8226e9bc1f | ||
|
|
d6d0a15015 | ||
|
|
ddec58ebd4 | ||
|
|
9d904b3911 | ||
|
|
c36df0d650 | ||
|
|
ae19288797 | ||
|
|
de308d5bb0 | ||
|
|
99b096a5c4 | ||
|
|
36fc8a947b | ||
|
|
6393a30b13 | ||
|
|
458be49b35 | ||
|
|
f8d742bdd0 | ||
|
|
7e698f1613 | ||
|
|
70b5cc86dc | ||
|
|
19a5dcad1d | ||
|
|
85e25d6380 | ||
|
|
6e2383b66b | ||
|
|
200c5fcbd2 | ||
|
|
9cbb8bdc33 | ||
|
|
3d39836cfb | ||
|
|
24f2855f89 | ||
|
|
d5bf206d2a | ||
|
|
8abc01551e | ||
|
|
62b8a79e9f | ||
|
|
7f255563a4 | ||
|
|
32f4a50f25 | ||
|
|
18e6f29de7 | ||
|
|
b1a9be2dc3 | ||
|
|
c5456d3033 | ||
|
|
9b33dad589 | ||
|
|
4315a46ff2 | ||
|
|
0fb5827968 | ||
|
|
f9bf23dc43 | ||
|
|
611017fd28 | ||
|
|
72b265a35f | ||
|
|
e0d533f2ea | ||
|
|
dba83641e4 | ||
|
|
d0c6b9cdca | ||
|
|
14e5c8e761 | ||
|
|
cbbf4a95db | ||
|
|
55b7ffd2ed | ||
|
|
9f4d4e70de | ||
|
|
deef1bbfcf | ||
|
|
17b0fc0d0c | ||
|
|
3c44c511e4 | ||
|
|
7cf1b9613a | ||
|
|
99e5a9601c | ||
|
|
4c9c1dccf5 | ||
|
|
2cddd5afa0 | ||
|
|
8c6a0c4773 | ||
|
|
af5063552d | ||
|
|
419d154794 | ||
|
|
abda059be5 | ||
|
|
444908cac5 | ||
|
|
c6da26e1a0 | ||
|
|
aad87857ce | ||
|
|
666c113297 | ||
|
|
84a58f34a0 | ||
|
|
75f5b5c721 | ||
|
|
a176a3b79b | ||
|
|
1627a5767a | ||
|
|
6ff3a9e9ab | ||
|
|
3ce2bd30f9 | ||
|
|
de4f99807f | ||
|
|
eae8351f77 | ||
|
|
d386308301 | ||
|
|
13afbc84d7 | ||
|
|
31327b3780 | ||
|
|
81f483d7b8 | ||
|
|
1bcf20511b | ||
|
|
003fa8ab2c | ||
|
|
d1fcd07c92 | ||
|
|
47a6d6f662 | ||
|
|
4b2b6440d0 | ||
|
|
fc855f30f8 | ||
|
|
4a75fff4ec | ||
|
|
7b7b695938 | ||
|
|
2fca2e295f | ||
|
|
eae991d369 | ||
|
|
c2b393d3fd | ||
|
|
566addd406 | ||
|
|
3d77dacbd6 | ||
|
|
51b4f9cbbf | ||
|
|
1d611be83e | ||
|
|
1782904994 | ||
|
|
e3ddbfc181 | ||
|
|
8e3830b646 | ||
|
|
b28cdff233 | ||
|
|
7f586ff1a3 | ||
|
|
731d4f535c | ||
|
|
f8c731ca21 | ||
|
|
39eb3be01e | ||
|
|
d0bb728e25 | ||
|
|
58de0f9c99 | ||
|
|
b75ae7ba96 | ||
|
|
091e38082e | ||
|
|
74acf93c7a | ||
|
|
0e3775397f | ||
|
|
8dc52a04e4 | ||
|
|
008b607c01 | ||
|
|
d01a407137 | ||
|
|
c0c717d9ec | ||
|
|
4f011bfc37 | ||
|
|
6c4ab32cf0 | ||
|
|
09b4dc1b9a | ||
|
|
bdd5a54527 | ||
|
|
b7bb4db8c1 | ||
|
|
5b5f33d3cf | ||
|
|
9611656b76 | ||
|
|
e9a71774d5 | ||
|
|
2205b8aec5 | ||
|
|
5c4e4d5d36 | ||
|
|
e87247493f | ||
|
|
0aeb63a05b | ||
|
|
57eeb5659a | ||
|
|
f821c19528 | ||
|
|
b9a120164a | ||
|
|
087356f781 | ||
|
|
31110a5326 | ||
|
|
667a68768c | ||
|
|
5075981a90 | ||
|
|
611dc4f6be | ||
|
|
0b43eaa992 | ||
|
|
47cdc1c4c8 | ||
|
|
31c071ad72 | ||
|
|
e91edaee12 | ||
|
|
34c1fee5db | ||
|
|
5375422633 | ||
|
|
1dafe934b0 | ||
|
|
e1a33c6a9b | ||
|
|
0800fa016b | ||
|
|
8f3454c3b1 | ||
|
|
f0d7a1aaab | ||
|
|
8240595d70 | ||
|
|
658d8dd50c | ||
|
|
9eeeaa24c1 | ||
|
|
9c4a3592ff | ||
|
|
0e2c2ca101 | ||
|
|
8015be8069 | ||
|
|
e39ad59589 | ||
|
|
be7cff4818 | ||
|
|
d6632cf312 | ||
|
|
f6e012ec73 | ||
|
|
087acd5765 | ||
|
|
43bf6e8d8a | ||
|
|
78be6aa343 | ||
|
|
8ba57969c2 | ||
|
|
58d2f605fc | ||
|
|
e0b4319c7d | ||
|
|
98d0df4320 | ||
|
|
6aa3ce4f05 | ||
|
|
046db325f1 | ||
|
|
1a4bace5f4 | ||
|
|
0dbf75f100 | ||
|
|
4f49fd8d5c | ||
|
|
86f71e1faf | ||
|
|
147e2c64b5 | ||
|
|
9063057b0c | ||
|
|
25f0d94595 | ||
|
|
8155df2e22 | ||
|
|
bb51470004 | ||
|
|
53446e2ec7 | ||
|
|
c4c09550eb | ||
|
|
da47fdec96 | ||
|
|
971342ac7a | ||
|
|
3becc1cbe2 | ||
|
|
8ee5346900 | ||
|
|
9e4b79c6ff | ||
|
|
4ec1c65395 | ||
|
|
c743d11b3b | ||
|
|
b525eb6480 | ||
|
|
210d03fb06 | ||
|
|
0685cc4cbe | ||
|
|
9e2d0a7fbc | ||
|
|
a751a3b731 | ||
|
|
bc7faeeab9 | ||
|
|
a88c1de2f6 | ||
|
|
a231795c4c | ||
|
|
ba7525c676 | ||
|
|
4843e9a40a | ||
|
|
ca4a1ca9b0 | ||
|
|
ad7c872ee3 | ||
|
|
4f98324a8a | ||
|
|
1fcf3f79ee | ||
|
|
0b5741194a | ||
|
|
9653d4c6ce | ||
|
|
4fa5ed5e2c | ||
|
|
f4eac2515f | ||
|
|
1a5364ae9d | ||
|
|
d7a9074c69 | ||
|
|
00adb53802 | ||
|
|
b10edddc96 | ||
|
|
93b2d12438 | ||
|
|
7303e811be | ||
|
|
992d89027d | ||
|
|
3531cc587d | ||
|
|
562a91fa57 | ||
|
|
93128a0156 | ||
|
|
d7a2132966 | ||
|
|
56ac1aea9d | ||
|
|
37a9fb62e2 | ||
|
|
a70e88b1f4 | ||
|
|
6f9ce9ce85 | ||
|
|
917dd60cc3 | ||
|
|
b13cc6f39a | ||
|
|
2758382c68 | ||
|
|
dd7f1ff99c | ||
|
|
45142cd037 | ||
|
|
ceb1caf1de | ||
|
|
7e263a3076 | ||
|
|
ec88d4a430 | ||
|
|
909ea72108 | ||
|
|
cd09d75d99 | ||
|
|
0e3abe489a | ||
|
|
a5d141d2f1 | ||
|
|
87eae6164b | ||
|
|
97e00f5a9c | ||
|
|
bd55558b5e | ||
|
|
25dc4b3a4c | ||
|
|
72c27b78a3 | ||
|
|
25809ec409 | ||
|
|
3220c522d7 | ||
|
|
d5d25f1d30 | ||
|
|
7ccd6ba329 | ||
|
|
35c30ae2c5 | ||
|
|
2c61f6d396 | ||
|
|
86233a8eab | ||
|
|
c140b128ee | ||
|
|
a6b1527fd3 | ||
|
|
c2f3a2aac0 | ||
|
|
66ac9773c0 | ||
|
|
e60714bb98 | ||
|
|
52dfd6e939 | ||
|
|
fc2312eba6 | ||
|
|
cf90476255 | ||
|
|
1e8d7d10ab | ||
|
|
cf6621b83c | ||
|
|
9812cbd71c | ||
|
|
67a81a3cac | ||
|
|
67463683cc | ||
|
|
b94481b716 | ||
|
|
a95ddee48d | ||
|
|
953694f77f |
14
.gitignore
vendored
14
.gitignore
vendored
@@ -4,11 +4,13 @@ yarn-error.log
|
|||||||
node_modules
|
node_modules
|
||||||
cypress/screenshots
|
cypress/screenshots
|
||||||
test/app/.sapper
|
test/app/.sapper
|
||||||
test/app/app/manifest
|
test/app/src/manifest
|
||||||
|
__sapper__
|
||||||
test/app/export
|
test/app/export
|
||||||
test/app/build
|
test/app/build
|
||||||
*.js
|
sapper
|
||||||
*.js.map
|
runtime.js
|
||||||
*.ts.js
|
dist
|
||||||
*.ts.js.map
|
!rollup.config.js
|
||||||
!rollup.config.js
|
/runtime/app.mjs
|
||||||
|
/runtime/server.mjs
|
||||||
@@ -3,7 +3,6 @@ sudo: false
|
|||||||
language: node_js
|
language: node_js
|
||||||
|
|
||||||
node_js:
|
node_js:
|
||||||
- "6"
|
|
||||||
- "stable"
|
- "stable"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -18,4 +17,4 @@ addons:
|
|||||||
install:
|
install:
|
||||||
- export DISPLAY=':99.0'
|
- export DISPLAY=':99.0'
|
||||||
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||||
- npm install
|
- npm ci || npm i
|
||||||
|
|||||||
358
CHANGELOG.md
358
CHANGELOG.md
@@ -1,5 +1,363 @@
|
|||||||
# sapper changelog
|
# sapper changelog
|
||||||
|
|
||||||
|
## 0.25.0
|
||||||
|
|
||||||
|
* Force refresh on `goto(current_url)` ([#484](https://github.com/sveltejs/sapper/pull/484))
|
||||||
|
* Fix preloading navigation bug ([#532](https://github.com/sveltejs/sapper/issues/532))
|
||||||
|
* Don't mutate opts.headers ([#528](https://github.com/sveltejs/sapper/issues/528))
|
||||||
|
* Don't crawl hundreds of pages simultaneously ([#369](https://github.com/sveltejs/sapper/pull/369))
|
||||||
|
|
||||||
|
## 0.24.3
|
||||||
|
|
||||||
|
* Add service-worker-index.html shell file for offline support ([#422](https://github.com/sveltejs/sapper/issues/422))
|
||||||
|
* Don't cache .map files ([#534](https://github.com/sveltejs/sapper/issues/534))
|
||||||
|
|
||||||
|
## 0.24.2
|
||||||
|
|
||||||
|
* Support Rollup 1.0 ([#541](https://github.com/sveltejs/sapper/pull/541))
|
||||||
|
|
||||||
|
## 0.24.1
|
||||||
|
|
||||||
|
* Include CSS chunks in webpack build info to avoid duplication ([#529](https://github.com/sveltejs/sapper/pull/529))
|
||||||
|
* Fix preload `as` for styles ([#530](https://github.com/sveltejs/sapper/pull/530))
|
||||||
|
|
||||||
|
## 0.24.0
|
||||||
|
|
||||||
|
* Handle external URLs in `this.redirect` ([#490](https://github.com/sveltejs/sapper/issues/490))
|
||||||
|
* Strip leading `/` from basepath ([#495](https://github.com/sveltejs/sapper/issues/495))
|
||||||
|
* Treat duplicate query string parameters as arrays ([#497](https://github.com/sveltejs/sapper/issues/497))
|
||||||
|
* Don't buffer `stdout` and `stderr` ([#305](https://github.com/sveltejs/sapper/issues/305))
|
||||||
|
* Posixify `build_dir` ([#498](https://github.com/sveltejs/sapper/pull/498))
|
||||||
|
* Use `page[XY]Offset` instead of `scroll[XY]` ([#480](https://github.com/sveltejs/sapper/issues/480))
|
||||||
|
|
||||||
|
## 0.23.5
|
||||||
|
|
||||||
|
* Include lazily-imported CSS in main CSS chunk ([#492](https://github.com/sveltejs/sapper/pull/492))
|
||||||
|
* Make search param decoding spec-compliant ([#493](https://github.com/sveltejs/sapper/pull/493))
|
||||||
|
* Handle async route errors ([#488](https://github.com/sveltejs/sapper/pull/488))
|
||||||
|
|
||||||
|
## 0.23.4
|
||||||
|
|
||||||
|
* Ignore empty anchors when exporting ([#491](https://github.com/sveltejs/sapper/pull/491))
|
||||||
|
|
||||||
|
## 0.23.3
|
||||||
|
|
||||||
|
* Clear `error` and `status` on successful render ([#477](https://github.com/sveltejs/sapper/pull/477))
|
||||||
|
|
||||||
|
## 0.23.2
|
||||||
|
|
||||||
|
* Fix entry point CSS ([#471](https://github.com/sveltejs/sapper/pull/471))
|
||||||
|
|
||||||
|
## 0.23.1
|
||||||
|
|
||||||
|
* Scroll to deeplink that matches current URL ([#472](https://github.com/sveltejs/sapper/pull/472))
|
||||||
|
* Scroll to deeplink on another page ([#341](https://github.com/sveltejs/sapper/issues/341))
|
||||||
|
|
||||||
|
## 0.23.0
|
||||||
|
|
||||||
|
* Overhaul internal APIs ([#468](https://github.com/sveltejs/sapper/pull/468))
|
||||||
|
* Remove unused `sapper start` and `sapper upgrade` ([#468](https://github.com/sveltejs/sapper/pull/468))
|
||||||
|
* Remove magic environment variables ([#469](https://github.com/sveltejs/sapper/pull/469))
|
||||||
|
* Preserve SSI comments ([#470](https://github.com/sveltejs/sapper/pull/470))
|
||||||
|
|
||||||
|
## 0.22.10
|
||||||
|
|
||||||
|
* Handle `sapper-noscroll` attribute on `<a>` elements ([#376](https://github.com/sveltejs/sapper/issues/376))
|
||||||
|
* Fix CSS paths when using a base path ([#466](https://github.com/sveltejs/sapper/pull/466))
|
||||||
|
|
||||||
|
## 0.22.9
|
||||||
|
|
||||||
|
* Fix legacy builds ([#462](https://github.com/sveltejs/sapper/pull/462))
|
||||||
|
|
||||||
|
## 0.22.8
|
||||||
|
|
||||||
|
* Ensure CSS placeholders are overwritten ([#462](https://github.com/sveltejs/sapper/pull/462))
|
||||||
|
|
||||||
|
## 0.22.7
|
||||||
|
|
||||||
|
* Fix cookies ([#460](https://github.com/sveltejs/sapper/pull/460))
|
||||||
|
|
||||||
|
## 0.22.6
|
||||||
|
|
||||||
|
* Normalise chunk filenames on Windows ([#456](https://github.com/sveltejs/sapper/pull/456))
|
||||||
|
* Load modules with credentials ([#458](https://github.com/sveltejs/sapper/pull/458))
|
||||||
|
|
||||||
|
## 0.22.5
|
||||||
|
|
||||||
|
* Fix `sapper dev`. Oops.
|
||||||
|
|
||||||
|
## 0.22.4
|
||||||
|
|
||||||
|
* Ensure launcher does not overwrite a module ([#455](https://github.com/sveltejs/sapper/pull/455))
|
||||||
|
|
||||||
|
## 0.22.3
|
||||||
|
|
||||||
|
* Prevent server from accidentally importing dev client
|
||||||
|
|
||||||
|
## 0.22.2
|
||||||
|
|
||||||
|
* Make paths in generated code relative to project
|
||||||
|
|
||||||
|
## 0.22.1
|
||||||
|
|
||||||
|
* Fix `pkg.files`
|
||||||
|
|
||||||
|
## 0.22.0
|
||||||
|
|
||||||
|
* Move generated files into `__sapper__` ([#453](https://github.com/sveltejs/sapper/pull/453))
|
||||||
|
* Change default build and export directories to `__sapper__/build` and `__sapper__/export` ([#453](https://github.com/sveltejs/sapper/pull/453))
|
||||||
|
|
||||||
|
## 0.21.1
|
||||||
|
|
||||||
|
* Read template from build directory in production
|
||||||
|
|
||||||
|
## 0.21.0
|
||||||
|
|
||||||
|
* Change project folder structure ([#432](https://github.com/sveltejs/sapper/issues/432))
|
||||||
|
* Escape filenames ([#446](https://github.com/sveltejs/sapper/pull/446/))
|
||||||
|
|
||||||
|
## 0.20.4
|
||||||
|
|
||||||
|
* Fix legacy build CSS ([#439](https://github.com/sveltejs/sapper/issues/439))
|
||||||
|
* Enable debugging in Chrome and VSCode ([#435](https://github.com/sveltejs/sapper/issues/435))
|
||||||
|
|
||||||
|
## 0.20.3
|
||||||
|
|
||||||
|
* Inject `nonce` attribute if `res.locals.nonce` is present ([#424](https://github.com/sveltejs/sapper/pull/424))
|
||||||
|
* Prevent service worker caching ([#428](https://github.com/sveltejs/sapper/pull/428))
|
||||||
|
* Consistent caching for HTML responses ([#429](https://github.com/sveltejs/sapper/pull/429))
|
||||||
|
|
||||||
|
## 0.20.2
|
||||||
|
|
||||||
|
* Add `immutable` cache control header for hashed assets ([#425](https://github.com/sveltejs/sapper/pull/425))
|
||||||
|
* Handle value-less query string params ([#426](https://github.com/sveltejs/sapper/issues/426))
|
||||||
|
|
||||||
|
## 0.20.1
|
||||||
|
|
||||||
|
* Update shimport
|
||||||
|
|
||||||
|
## 0.20.0
|
||||||
|
|
||||||
|
* Decode `req.params` and `req.query` ([#417](https://github.com/sveltejs/sapper/issues/417))
|
||||||
|
* Decode URLs before writing files in `sapper export` ([#414](https://github.com/sveltejs/sapper/pull/414))
|
||||||
|
* Generate server sourcemaps for Rollup apps in dev mode ([#418](https://github.com/sveltejs/sapper/pull/418))
|
||||||
|
|
||||||
|
## 0.19.3
|
||||||
|
|
||||||
|
* Better unicode route handling ([#347](https://github.com/sveltejs/sapper/issues/347))
|
||||||
|
|
||||||
|
## 0.19.2
|
||||||
|
|
||||||
|
* Ignore editor tmp files ([#220](https://github.com/sveltejs/sapper/issues/220))
|
||||||
|
* Ignore clicks an `<a>` element without `href` ([#235](https://github.com/sveltejs/sapper/issues/235))
|
||||||
|
* Allow routes that are reserved JavaScript words ([#315](https://github.com/sveltejs/sapper/issues/315))
|
||||||
|
* Print out webpack errors ([#403](https://github.com/sveltejs/sapper/issues/403))
|
||||||
|
|
||||||
|
## 0.19.1
|
||||||
|
|
||||||
|
* Don't include local origin in export redirects ([#409](https://github.com/sveltejs/sapper/pull/409))
|
||||||
|
|
||||||
|
## 0.19.0
|
||||||
|
|
||||||
|
* Extract styles out of JS into .css files, for Rollup apps ([#388](https://github.com/sveltejs/sapper/issues/388))
|
||||||
|
* Fix `prefetchRoutes` ([#380](https://github.com/sveltejs/sapper/issues/380))
|
||||||
|
|
||||||
|
## 0.18.7
|
||||||
|
|
||||||
|
* Support differential bundling for Rollup apps via a `--legacy` flag ([#280](https://github.com/sveltejs/sapper/issues/280))
|
||||||
|
|
||||||
|
## 0.18.6
|
||||||
|
|
||||||
|
* Bundle missing dependency
|
||||||
|
|
||||||
|
## 0.18.5
|
||||||
|
|
||||||
|
* Bugfix
|
||||||
|
|
||||||
|
## 0.18.4
|
||||||
|
|
||||||
|
* Handle non-Sapper responses when exporting ([#382](https://github.com/sveltejs/sapper/issues/392))
|
||||||
|
* Add `--dev-port` flag to `sapper dev` ([#381](https://github.com/sveltejs/sapper/issues/381))
|
||||||
|
|
||||||
|
## 0.18.3
|
||||||
|
|
||||||
|
* Fix service worker Rollup build config
|
||||||
|
|
||||||
|
## 0.18.2
|
||||||
|
|
||||||
|
* Update `pkg.files`
|
||||||
|
|
||||||
|
## 0.18.1
|
||||||
|
|
||||||
|
* Add live reloading ([#385](https://github.com/sveltejs/sapper/issues/385))
|
||||||
|
|
||||||
|
## 0.18.0
|
||||||
|
|
||||||
|
* Rollup support ([#379](https://github.com/sveltejs/sapper/pull/379))
|
||||||
|
* Fail `export` if a page times out (configurable with `--timeout`) ([#378](https://github.com/sveltejs/sapper/pull/378))
|
||||||
|
|
||||||
|
## 0.17.1
|
||||||
|
|
||||||
|
* Print which file is causing build errors/warnings ([#371](https://github.com/sveltejs/sapper/pull/371))
|
||||||
|
|
||||||
|
## 0.17.0
|
||||||
|
|
||||||
|
* Use `cheap-watch` instead of `chokidar` ([#364](https://github.com/sveltejs/sapper/issues/364))
|
||||||
|
|
||||||
|
## 0.16.1
|
||||||
|
|
||||||
|
* Fix file watching regression in previous version
|
||||||
|
|
||||||
|
## 0.16.0
|
||||||
|
|
||||||
|
* Slim down installed package ([#363](https://github.com/sveltejs/sapper/pull/363))
|
||||||
|
|
||||||
|
## 0.15.8
|
||||||
|
|
||||||
|
* Only set `preloading: true` on navigation, not prefetch ([#352](https://github.com/sveltejs/sapper/issues/352))
|
||||||
|
* Provide fallback for missing preload errors ([#361](https://github.com/sveltejs/sapper/pull/361))
|
||||||
|
|
||||||
|
## 0.15.7
|
||||||
|
|
||||||
|
* Strip leading slash from redirects ([#291](https://github.com/sveltejs/sapper/issues/291))
|
||||||
|
* Pass `(req, res)` to store getter ([#344](https://github.com/sveltejs/sapper/issues/344))
|
||||||
|
|
||||||
|
## 0.15.6
|
||||||
|
|
||||||
|
* Fix exporting with custom basepath ([#342](https://github.com/sveltejs/sapper/pull/342))
|
||||||
|
|
||||||
|
## 0.15.5
|
||||||
|
|
||||||
|
* Faster `export` with more explanatory output ([#335](https://github.com/sveltejs/sapper/pull/335))
|
||||||
|
* Only blur `activeElement` if it exists ([#332](https://github.com/sveltejs/sapper/issues/332))
|
||||||
|
* Don't emit `client_info.json` or `server_info.json` ([#318](https://github.com/sveltejs/sapper/issues/318))
|
||||||
|
|
||||||
|
## 0.15.4
|
||||||
|
|
||||||
|
* Add `ignore` option ([#326](https://github.com/sveltejs/sapper/pull/326))
|
||||||
|
|
||||||
|
## 0.15.3
|
||||||
|
|
||||||
|
* Crawl pages in parallel when exporting ([#329](https://github.com/sveltejs/sapper/pull/329))
|
||||||
|
* Don't minify inline JS when exporting ([#328](https://github.com/sveltejs/sapper/pull/328))
|
||||||
|
|
||||||
|
## 0.15.2
|
||||||
|
|
||||||
|
* Collapse component chains where no intermediate layout component is specified ([#312](https://github.com/sveltejs/sapper/issues/312))
|
||||||
|
|
||||||
|
## 0.15.1
|
||||||
|
|
||||||
|
* Prevent confusing error when no root layout is specified
|
||||||
|
|
||||||
|
## 0.15.0
|
||||||
|
|
||||||
|
* Nested routes (consult [migration guide](https://sapper.svelte.technology/guide#0-14-to-0-15) and docs on [layouts](https://sapper.svelte.technology/guide#layouts)) ([#262](https://github.com/sveltejs/sapper/issues/262))
|
||||||
|
|
||||||
|
## 0.14.2
|
||||||
|
|
||||||
|
* Prevent unsafe replacements ([#307](https://github.com/sveltejs/sapper/pull/307))
|
||||||
|
|
||||||
|
## 0.14.1
|
||||||
|
|
||||||
|
* Route parameters can be qualified with regex characters ([#283](https://github.com/sveltejs/sapper/pull/283))
|
||||||
|
|
||||||
|
## 0.14.0
|
||||||
|
|
||||||
|
* `4xx.html` and `5xx.html` are replaced with `_error.html` ([#209](https://github.com/sveltejs/sapper/issues/209))
|
||||||
|
* Treat `foo/index.json.js` and `foo.json.js` as equivalents ([#297](https://github.com/sveltejs/sapper/issues/297))
|
||||||
|
* Return a promise from `goto` ([#270](https://github.com/sveltejs/sapper/issues/270))
|
||||||
|
* Use store when rendering error pages ([#293](https://github.com/sveltejs/sapper/issues/293))
|
||||||
|
* Prevent console errors when visiting an error page ([#279](https://github.com/sveltejs/sapper/issues/279))
|
||||||
|
|
||||||
|
## 0.13.6
|
||||||
|
|
||||||
|
* Fix `baseUrl` synthesis ([#296](https://github.com/sveltejs/sapper/issues/296))
|
||||||
|
|
||||||
|
## 0.13.5
|
||||||
|
|
||||||
|
* Fix handling of fatal errors ([#289](https://github.com/sveltejs/sapper/issues/289))
|
||||||
|
|
||||||
|
## 0.13.4
|
||||||
|
|
||||||
|
* Focus `<body>` after navigation ([#287](https://github.com/sveltejs/sapper/issues/287))
|
||||||
|
* Fix timing of hot reload updates
|
||||||
|
* Emit `fatal` event if server crashes ([#285](https://github.com/sveltejs/sapper/pull/285))
|
||||||
|
* Emit `stdout` and `stderr` events on dev watcher ([#285](https://github.com/sveltejs/sapper/pull/285))
|
||||||
|
* Always refresh client assets in dev ([#286](https://github.com/sveltejs/sapper/pull/286))
|
||||||
|
* Correctly initialise rebuild stats
|
||||||
|
|
||||||
|
## 0.13.3
|
||||||
|
|
||||||
|
* Make `fatal` events clonable for IPC purposes
|
||||||
|
|
||||||
|
## 0.13.2
|
||||||
|
|
||||||
|
* Emit a `basepath` event ([#284](https://github.com/sveltejs/sapper/pull/284))
|
||||||
|
|
||||||
|
## 0.13.1
|
||||||
|
|
||||||
|
* Reinstate ten-second interval between dev server heartbeats ([#276](https://github.com/sveltejs/sapper/issues/276))
|
||||||
|
|
||||||
|
## 0.13.0
|
||||||
|
|
||||||
|
* Expose `dev`, `build`, `export` and `find_page` APIs ([#272](https://github.com/sveltejs/sapper/issues/272))
|
||||||
|
|
||||||
|
## 0.12.0
|
||||||
|
|
||||||
|
* Each app has a single `<App>` component. See the [migration guide](https://sapper.svelte.technology/guide#0-11-to-0-12) for more information ([#157](https://github.com/sveltejs/sapper/issues/157))
|
||||||
|
* Process exits with error code 1 if build/export fails ([#208](https://github.com/sveltejs/sapper/issues/208))
|
||||||
|
|
||||||
|
## 0.11.1
|
||||||
|
|
||||||
|
* Limit routes with leading dots to `.well-known` URIs ([#252](https://github.com/sveltejs/sapper/issues/252))
|
||||||
|
* Allow server routes to sit in front of pages ([#236](https://github.com/sveltejs/sapper/pull/236))
|
||||||
|
|
||||||
|
## 0.11.0
|
||||||
|
|
||||||
|
* Create launcher file ([#240](https://github.com/sveltejs/sapper/issues/240))
|
||||||
|
* Only keep necessary parts of webpack stats ([#251](https://github.com/sveltejs/sapper/pull/251))
|
||||||
|
* Allow `NODE_ENV` to be overridden when building ([#241](https://github.com/sveltejs/sapper/issues/241))
|
||||||
|
|
||||||
|
## 0.10.7
|
||||||
|
|
||||||
|
* Allow routes to have a leading `.` ([#243](https://github.com/sveltejs/sapper/pull/243))
|
||||||
|
* Only encode necessary characters in routes ([#234](https://github.com/sveltejs/sapper/pull/234))
|
||||||
|
* Preserve existing `process.env` when exporting ([#245](https://github.com/sveltejs/sapper/pull/245))
|
||||||
|
|
||||||
|
## 0.10.6
|
||||||
|
|
||||||
|
* Fix error reporting in `sapper start`
|
||||||
|
|
||||||
|
## 0.10.5
|
||||||
|
|
||||||
|
* Fix missing service worker ([#231](https://github.com/sveltejs/sapper/pull/231))
|
||||||
|
|
||||||
|
## 0.10.4
|
||||||
|
|
||||||
|
* Upgrade chokidar, this time with a fix ([#227](https://github.com/sveltejs/sapper/pull/227))
|
||||||
|
|
||||||
|
## 0.10.3
|
||||||
|
|
||||||
|
* Downgrade chokidar ([#212](https://github.com/sveltejs/sapper/issues/212))
|
||||||
|
|
||||||
|
## 0.10.2
|
||||||
|
|
||||||
|
* Attach `store` to error pages
|
||||||
|
* Fix sorting edge case ([#215](https://github.com/sveltejs/sapper/pull/215))
|
||||||
|
|
||||||
|
## 0.10.1
|
||||||
|
|
||||||
|
* Fix server-side `fetch` paths ([#207](https://github.com/sveltejs/sapper/pull/207))
|
||||||
|
|
||||||
|
## 0.10.0
|
||||||
|
|
||||||
|
* Support mounting on a path (this requires `app/template.html` to include `%sapper.base%`) ([#180](https://github.com/sveltejs/sapper/issues/180))
|
||||||
|
* Support per-request server-side `Store` with client-side hydration ([#178](https://github.com/sveltejs/sapper/issues/178))
|
||||||
|
* Add `this.fetch` to `preload`, with credentials support ([#178](https://github.com/sveltejs/sapper/issues/178))
|
||||||
|
* Exclude sourcemaps from preload links and `<script>` block ([#204](https://github.com/sveltejs/sapper/pull/204))
|
||||||
|
* Register service worker in `<script>` block
|
||||||
|
|
||||||
|
|
||||||
## 0.9.6
|
## 0.9.6
|
||||||
|
|
||||||
* Whoops — `tslib` is a runtime dependency
|
* Whoops — `tslib` is a runtime dependency
|
||||||
|
|||||||
42
README.md
42
README.md
@@ -11,9 +11,11 @@ Sapper is a framework for building high-performance universal web apps. [Read th
|
|||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
Clone the [starter project template](https://github.com/sveltejs/sapper-template) with [degit](https://github.com/rich-harris/degit)...
|
Clone the [starter project template](https://github.com/sveltejs/sapper-template) with [degit](https://github.com/rich-harris/degit)...
|
||||||
|
When cloning you have to choose between rollup or webpack:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx degit sveltejs/sapper-template my-app
|
npx degit "sveltejs/sapper-template#rollup" my-app
|
||||||
|
# or: npx degit "sveltejs/sapper-template#webpack" my-app
|
||||||
```
|
```
|
||||||
|
|
||||||
...then install dependencies and start the dev server...
|
...then install dependencies and start the dev server...
|
||||||
@@ -31,6 +33,44 @@ npm run build
|
|||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Pull requests are encouraged and always welcome. [Pick an issue](https://github.com/sveltejs/sapper/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) and help us out!
|
||||||
|
|
||||||
|
To install and work on Sapper locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone git@github.com:sveltejs/sapper.git
|
||||||
|
cd sapper
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linking to a Live Project
|
||||||
|
|
||||||
|
You can make changes locally to Sapper and test it against a local Sapper project. For a quick project that takes almost no setup, use the default [sapper-template](https://github.com/sveltejs/sapper-template) project. Instruction on setup are found in that project repository.
|
||||||
|
|
||||||
|
To link Sapper to your project, from the root of your local Sapper git checkout:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd sapper
|
||||||
|
npm link
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, to link from `sapper-template` (or any other given project):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd sapper-template
|
||||||
|
npm link sapper
|
||||||
|
```
|
||||||
|
|
||||||
|
You should be good to test changes locally.
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test
|
||||||
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ build: off
|
|||||||
environment:
|
environment:
|
||||||
matrix:
|
matrix:
|
||||||
# node.js
|
# node.js
|
||||||
- nodejs_version: stable
|
- nodejs_version: 11
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- ps: Install-Product node $env:nodejs_version
|
- ps: Install-Product node $env:nodejs_version
|
||||||
- npm install
|
- npm ci
|
||||||
|
|
||||||
test_script:
|
test_script:
|
||||||
- node --version && npm --version
|
- node --version && npm --version
|
||||||
|
|||||||
1
config/rollup.js
Normal file
1
config/rollup.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
module.exports = require('../dist/rollup.js');
|
||||||
1
config/webpack.js
Normal file
1
config/webpack.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
module.exports = require('../dist/webpack.js');
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"baseUrl": "http://localhost:3000",
|
|
||||||
"videoRecording": false,
|
|
||||||
"fixturesFolder": "test/cypress/fixtures",
|
|
||||||
"integrationFolder": "test/cypress/integration",
|
|
||||||
"pluginsFile": false,
|
|
||||||
"screenshotsFolder": "test/cypress/screenshots",
|
|
||||||
"supportFile": "test/cypress/support/index.js"
|
|
||||||
}
|
|
||||||
1
index.js
Normal file
1
index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
throw new Error(`As of Sapper 0.22, you should not import 'sapper' directly. See https://sapper.svelte.technology/guide#0-21-to-0-22 for more information`);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
--require source-map-support/register
|
--require source-map-support/register
|
||||||
|
--require sucrase/register
|
||||||
--recursive
|
--recursive
|
||||||
test/unit/**/*.js
|
test/apps/*/test.ts
|
||||||
test/common/test.js
|
|
||||||
5539
package-lock.json
generated
Normal file
5539
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
99
package.json
99
package.json
@@ -1,77 +1,76 @@
|
|||||||
{
|
{
|
||||||
"name": "sapper",
|
"name": "sapper",
|
||||||
"version": "0.9.6",
|
"version": "0.26.0-alpha.12",
|
||||||
"description": "Military-grade apps, engineered by Svelte",
|
"description": "Military-grade apps, engineered by Svelte",
|
||||||
"main": "dist/middleware.ts.js",
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"sapper": "./sapper"
|
"sapper": "./sapper"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"*.js",
|
"*.js",
|
||||||
"*.ts.js",
|
|
||||||
"runtime",
|
|
||||||
"webpack",
|
"webpack",
|
||||||
|
"config",
|
||||||
"sapper",
|
"sapper",
|
||||||
"dist"
|
"dist/*.js",
|
||||||
|
"runtime/*.mjs",
|
||||||
|
"runtime/internal"
|
||||||
],
|
],
|
||||||
"directories": {
|
"directories": {
|
||||||
"test": "test"
|
"test": "test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cheerio": "^1.0.0-rc.2",
|
"html-minifier": "^3.5.21",
|
||||||
"chokidar": "^1.7.0",
|
"http-link-header": "^1.0.2",
|
||||||
"clorox": "^1.0.3",
|
"shimport": "0.0.14",
|
||||||
"devalue": "^1.0.1",
|
"sourcemap-codec": "^1.4.4",
|
||||||
"glob": "^7.1.2",
|
"string-hash": "^1.1.3"
|
||||||
"html-minifier": "^3.5.10",
|
|
||||||
"mkdirp": "^0.5.1",
|
|
||||||
"node-fetch": "^1.7.3",
|
|
||||||
"port-authority": "^1.0.2",
|
|
||||||
"pretty-bytes": "^4.0.2",
|
|
||||||
"pretty-ms": "^3.1.0",
|
|
||||||
"require-relative": "^0.8.7",
|
|
||||||
"rimraf": "^2.6.2",
|
|
||||||
"sade": "^1.4.0",
|
|
||||||
"sander": "^0.6.0",
|
|
||||||
"source-map-support": "^0.5.3",
|
|
||||||
"tslib": "^1.9.0",
|
|
||||||
"url-parse": "^1.2.0",
|
|
||||||
"walk-sync": "^0.3.2",
|
|
||||||
"webpack-format-messages": "^1.0.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@std/esm": "^0.19.7",
|
"@types/mocha": "^5.2.5",
|
||||||
"@types/glob": "^5.0.34",
|
"@types/node": "^10.12.21",
|
||||||
"@types/mkdirp": "^0.5.2",
|
"@types/puppeteer": "^1.11.3",
|
||||||
"@types/rimraf": "^2.0.2",
|
"agadoo": "^1.0.1",
|
||||||
"compression": "^1.7.1",
|
"cheap-watch": "^1.0.2",
|
||||||
"eslint": "^4.13.1",
|
"cookie": "^0.3.1",
|
||||||
"eslint-plugin-import": "^2.8.0",
|
"devalue": "^1.1.0",
|
||||||
"get-port": "^3.2.0",
|
"eslint": "^5.12.1",
|
||||||
"mocha": "^4.0.1",
|
"eslint-plugin-import": "^2.16.0",
|
||||||
"nightmare": "^2.10.0",
|
"kleur": "^3.0.1",
|
||||||
"npm-run-all": "^4.1.2",
|
"mocha": "^5.2.0",
|
||||||
"polka": "^0.3.4",
|
"node-fetch": "^2.3.0",
|
||||||
"rollup": "^0.56.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"rollup-plugin-commonjs": "^8.3.0",
|
"polka": "^0.5.1",
|
||||||
"rollup-plugin-json": "^2.3.0",
|
"port-authority": "^1.0.5",
|
||||||
|
"pretty-bytes": "^5.1.0",
|
||||||
|
"puppeteer": "^1.12.0",
|
||||||
|
"require-relative": "^0.8.7",
|
||||||
|
"rollup": "^1.1.2",
|
||||||
|
"rollup-plugin-commonjs": "^9.2.0",
|
||||||
|
"rollup-plugin-json": "^3.1.0",
|
||||||
|
"rollup-plugin-node-resolve": "^4.0.0",
|
||||||
|
"rollup-plugin-replace": "^2.1.0",
|
||||||
"rollup-plugin-string": "^2.0.2",
|
"rollup-plugin-string": "^2.0.2",
|
||||||
"rollup-plugin-typescript": "^0.8.1",
|
"rollup-plugin-sucrase": "^2.1.0",
|
||||||
"serve-static": "^1.13.2",
|
"rollup-plugin-svelte": "^5.0.3",
|
||||||
"svelte": "^1.49.1",
|
"sade": "^1.4.2",
|
||||||
"svelte-loader": "^2.3.2",
|
"sirv": "^0.2.2",
|
||||||
"ts-node": "^4.1.0",
|
"sucrase": "^3.9.5",
|
||||||
"typescript": "^2.6.2",
|
"svelte": "^3.0.0-beta.11",
|
||||||
"webpack": "^4.1.0"
|
"svelte-loader": "^2.13.3",
|
||||||
|
"webpack": "^4.29.0",
|
||||||
|
"webpack-format-messages": "^2.0.5",
|
||||||
|
"yootils": "0.0.14"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^3.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"cy:open": "cypress open",
|
|
||||||
"test": "mocha --opts mocha.opts",
|
"test": "mocha --opts mocha.opts",
|
||||||
"pretest": "npm run build",
|
"pretest": "npm run build",
|
||||||
"build": "rollup -c",
|
"build": "rm -rf dist && rollup -c",
|
||||||
|
"prepare": "npm run build",
|
||||||
"dev": "rollup -cw",
|
"dev": "rollup -cw",
|
||||||
"prepublishOnly": "npm test",
|
"prepublishOnly": "npm test",
|
||||||
"update_mime_types": "curl http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types | grep -e \"^[^#]\" > src/middleware/mime-types.md"
|
"update_mime_types": "curl http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types | grep -e \"^[^#]\" > runtime/src/server/middleware/mime-types.md"
|
||||||
},
|
},
|
||||||
"repository": "https://github.com/sveltejs/sapper",
|
"repository": "https://github.com/sveltejs/sapper",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,31 +1,54 @@
|
|||||||
import typescript from 'rollup-plugin-typescript';
|
import sucrase from 'rollup-plugin-sucrase';
|
||||||
import string from 'rollup-plugin-string';
|
import string from 'rollup-plugin-string';
|
||||||
import json from 'rollup-plugin-json';
|
import json from 'rollup-plugin-json';
|
||||||
|
import resolve from 'rollup-plugin-node-resolve';
|
||||||
import commonjs from 'rollup-plugin-commonjs';
|
import commonjs from 'rollup-plugin-commonjs';
|
||||||
import pkg from './package.json';
|
import pkg from './package.json';
|
||||||
|
import { builtinModules } from 'module';
|
||||||
|
|
||||||
const external = [].concat(
|
const external = [].concat(
|
||||||
Object.keys(pkg.dependencies),
|
Object.keys(pkg.dependencies),
|
||||||
Object.keys(process.binding('natives')),
|
Object.keys(process.binding('natives')),
|
||||||
'sapper/core.js'
|
'sapper/core.js',
|
||||||
|
'svelte/compiler'
|
||||||
);
|
);
|
||||||
|
|
||||||
export default [
|
function template(kind, external) {
|
||||||
{
|
return {
|
||||||
input: `src/runtime/index.ts`,
|
input: `runtime/src/${kind}/index.ts`,
|
||||||
output: {
|
output: {
|
||||||
file: `runtime.js`,
|
file: `runtime/${kind}.mjs`,
|
||||||
format: 'es'
|
format: 'es',
|
||||||
|
paths: id => id.replace('@sapper', '.')
|
||||||
},
|
},
|
||||||
|
external,
|
||||||
plugins: [
|
plugins: [
|
||||||
typescript({
|
resolve({
|
||||||
typescript: require('typescript')
|
extensions: ['.mjs', '.js', '.ts']
|
||||||
|
}),
|
||||||
|
commonjs(),
|
||||||
|
string({
|
||||||
|
include: '**/*.md'
|
||||||
|
}),
|
||||||
|
sucrase({
|
||||||
|
transforms: ['typescript']
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
},
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default [
|
||||||
|
template('app', id => /^(svelte\/?|@sapper\/)/.test(id)),
|
||||||
|
template('server', id => /^(svelte\/?|@sapper\/)/.test(id) || builtinModules.includes(id)),
|
||||||
|
|
||||||
{
|
{
|
||||||
input: [`src/cli.ts`, `src/core.ts`, `src/middleware.ts`, `src/webpack.ts`],
|
input: [
|
||||||
|
`src/api.ts`,
|
||||||
|
`src/cli.ts`,
|
||||||
|
`src/core.ts`,
|
||||||
|
`src/config/rollup.ts`,
|
||||||
|
`src/config/webpack.ts`
|
||||||
|
],
|
||||||
output: {
|
output: {
|
||||||
dir: 'dist',
|
dir: 'dist',
|
||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
@@ -33,16 +56,14 @@ export default [
|
|||||||
},
|
},
|
||||||
external,
|
external,
|
||||||
plugins: [
|
plugins: [
|
||||||
string({
|
|
||||||
include: '**/*.md'
|
|
||||||
}),
|
|
||||||
json(),
|
json(),
|
||||||
|
resolve({
|
||||||
|
extensions: ['.mjs', '.js', '.ts']
|
||||||
|
}),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
typescript({
|
sucrase({
|
||||||
typescript: require('typescript')
|
transforms: ['typescript']
|
||||||
})
|
})
|
||||||
],
|
]
|
||||||
experimentalCodeSplitting: true,
|
|
||||||
experimentalDynamicImport: true
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
271
runtime.js
271
runtime.js
@@ -1,271 +0,0 @@
|
|||||||
function detach(node) {
|
|
||||||
node.parentNode.removeChild(node);
|
|
||||||
}
|
|
||||||
function findAnchor(node) {
|
|
||||||
while (node && node.nodeName.toUpperCase() !== 'A')
|
|
||||||
node = node.parentNode; // SVG <a> elements have a lowercase name
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
function which(event) {
|
|
||||||
return event.which === null ? event.button : event.which;
|
|
||||||
}
|
|
||||||
function scroll_state() {
|
|
||||||
return {
|
|
||||||
x: window.scrollX,
|
|
||||||
y: window.scrollY
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var component;
|
|
||||||
var target;
|
|
||||||
var routes;
|
|
||||||
var errors;
|
|
||||||
var history = typeof window !== 'undefined' ? window.history : {
|
|
||||||
pushState: function (state, title, href) { },
|
|
||||||
replaceState: function (state, title, href) { },
|
|
||||||
scrollRestoration: ''
|
|
||||||
};
|
|
||||||
var scroll_history = {};
|
|
||||||
var uid = 1;
|
|
||||||
var cid;
|
|
||||||
if ('scrollRestoration' in history) {
|
|
||||||
history.scrollRestoration = 'manual';
|
|
||||||
}
|
|
||||||
function select_route(url) {
|
|
||||||
if (url.origin !== window.location.origin)
|
|
||||||
return null;
|
|
||||||
var _loop_1 = function (route) {
|
|
||||||
var match = route.pattern.exec(url.pathname);
|
|
||||||
if (match) {
|
|
||||||
if (route.ignore)
|
|
||||||
return { value: null };
|
|
||||||
var params = route.params(match);
|
|
||||||
var query_1 = {};
|
|
||||||
if (url.search.length > 0) {
|
|
||||||
url.search.slice(1).split('&').forEach(function (searchParam) {
|
|
||||||
var _a = /([^=]+)=(.*)/.exec(searchParam), key = _a[1], value = _a[2];
|
|
||||||
query_1[key] = value || true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return { value: { url: url, route: route, data: { params: params, query: query_1 } } };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
for (var _i = 0, routes_1 = routes; _i < routes_1.length; _i++) {
|
|
||||||
var route = routes_1[_i];
|
|
||||||
var state_1 = _loop_1(route);
|
|
||||||
if (typeof state_1 === "object")
|
|
||||||
return state_1.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var current_token;
|
|
||||||
function render(Component, data, scroll, token) {
|
|
||||||
if (current_token !== token)
|
|
||||||
return;
|
|
||||||
if (component) {
|
|
||||||
component.destroy();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// first load — remove SSR'd <head> contents
|
|
||||||
var start = document.querySelector('#sapper-head-start');
|
|
||||||
var end = document.querySelector('#sapper-head-end');
|
|
||||||
if (start && end) {
|
|
||||||
while (start.nextSibling !== end)
|
|
||||||
detach(start.nextSibling);
|
|
||||||
detach(start);
|
|
||||||
detach(end);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
component = new Component({
|
|
||||||
target: target,
|
|
||||||
data: data,
|
|
||||||
hydrate: !component
|
|
||||||
});
|
|
||||||
if (scroll) {
|
|
||||||
window.scrollTo(scroll.x, scroll.y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function prepare_route(Component, data) {
|
|
||||||
var redirect = null;
|
|
||||||
var error = null;
|
|
||||||
if (!Component.preload) {
|
|
||||||
return { Component: Component, data: data, redirect: redirect, error: error };
|
|
||||||
}
|
|
||||||
if (!component && window.__SAPPER__ && window.__SAPPER__.preloaded) {
|
|
||||||
return { Component: Component, data: Object.assign(data, window.__SAPPER__.preloaded), redirect: redirect, error: error };
|
|
||||||
}
|
|
||||||
return Promise.resolve(Component.preload.call({
|
|
||||||
redirect: function (statusCode, location) {
|
|
||||||
redirect = { statusCode: statusCode, location: location };
|
|
||||||
},
|
|
||||||
error: function (statusCode, message) {
|
|
||||||
error = { statusCode: statusCode, message: message };
|
|
||||||
}
|
|
||||||
}, data))["catch"](function (err) {
|
|
||||||
error = { statusCode: 500, message: err };
|
|
||||||
}).then(function (preloaded) {
|
|
||||||
if (error) {
|
|
||||||
var route = error.statusCode >= 400 && error.statusCode < 500
|
|
||||||
? errors['4xx']
|
|
||||||
: errors['5xx'];
|
|
||||||
return route.load().then(function (_a) {
|
|
||||||
var Component = _a["default"];
|
|
||||||
var err = error.message instanceof Error ? error.message : new Error(error.message);
|
|
||||||
Object.assign(data, { status: error.statusCode, error: err });
|
|
||||||
return { Component: Component, data: data, redirect: null };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Object.assign(data, preloaded);
|
|
||||||
return { Component: Component, data: data, redirect: redirect };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function navigate(target, id) {
|
|
||||||
if (id) {
|
|
||||||
// popstate or initial navigation
|
|
||||||
cid = id;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// clicked on a link. preserve scroll state
|
|
||||||
scroll_history[cid] = scroll_state();
|
|
||||||
id = cid = ++uid;
|
|
||||||
scroll_history[cid] = { x: 0, y: 0 };
|
|
||||||
}
|
|
||||||
cid = id;
|
|
||||||
var loaded = prefetching && prefetching.href === target.url.href ?
|
|
||||||
prefetching.promise :
|
|
||||||
target.route.load().then(function (mod) { return prepare_route(mod["default"], target.data); });
|
|
||||||
prefetching = null;
|
|
||||||
var token = current_token = {};
|
|
||||||
return loaded.then(function (_a) {
|
|
||||||
var Component = _a.Component, data = _a.data, redirect = _a.redirect;
|
|
||||||
if (redirect) {
|
|
||||||
return goto(redirect.location, { replaceState: true });
|
|
||||||
}
|
|
||||||
render(Component, data, scroll_history[id], token);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function handle_click(event) {
|
|
||||||
// Adapted from https://github.com/visionmedia/page.js
|
|
||||||
// MIT license https://github.com/visionmedia/page.js#license
|
|
||||||
if (which(event) !== 1)
|
|
||||||
return;
|
|
||||||
if (event.metaKey || event.ctrlKey || event.shiftKey)
|
|
||||||
return;
|
|
||||||
if (event.defaultPrevented)
|
|
||||||
return;
|
|
||||||
var a = findAnchor(event.target);
|
|
||||||
if (!a)
|
|
||||||
return;
|
|
||||||
// check if link is inside an svg
|
|
||||||
// in this case, both href and target are always inside an object
|
|
||||||
var svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
|
|
||||||
var href = String(svg ? a.href.baseVal : a.href);
|
|
||||||
if (href === window.location.href) {
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Ignore if tag has
|
|
||||||
// 1. 'download' attribute
|
|
||||||
// 2. rel='external' attribute
|
|
||||||
if (a.hasAttribute('download') || a.getAttribute('rel') === 'external')
|
|
||||||
return;
|
|
||||||
// Ignore if <a> has a target
|
|
||||||
if (svg ? a.target.baseVal : a.target)
|
|
||||||
return;
|
|
||||||
var url = new URL(href);
|
|
||||||
// Don't handle hash changes
|
|
||||||
if (url.pathname === window.location.pathname && url.search === window.location.search)
|
|
||||||
return;
|
|
||||||
var target = select_route(url);
|
|
||||||
if (target) {
|
|
||||||
navigate(target, null);
|
|
||||||
event.preventDefault();
|
|
||||||
history.pushState({ id: cid }, '', url.href);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function handle_popstate(event) {
|
|
||||||
scroll_history[cid] = scroll_state();
|
|
||||||
if (event.state) {
|
|
||||||
var url = new URL(window.location.href);
|
|
||||||
var target_1 = select_route(url);
|
|
||||||
navigate(target_1, event.state.id);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// hashchange
|
|
||||||
cid = ++uid;
|
|
||||||
history.replaceState({ id: cid }, '', window.location.href);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var prefetching = null;
|
|
||||||
function prefetch(href) {
|
|
||||||
var selected = select_route(new URL(href));
|
|
||||||
if (selected) {
|
|
||||||
prefetching = {
|
|
||||||
href: href,
|
|
||||||
promise: selected.route.load().then(function (mod) { return prepare_route(mod["default"], selected.data); })
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function handle_touchstart_mouseover(event) {
|
|
||||||
var a = findAnchor(event.target);
|
|
||||||
if (!a || a.rel !== 'prefetch')
|
|
||||||
return;
|
|
||||||
prefetch(a.href);
|
|
||||||
}
|
|
||||||
var inited;
|
|
||||||
function init(_target, _routes) {
|
|
||||||
target = _target;
|
|
||||||
routes = _routes.filter(function (r) { return !r.error; });
|
|
||||||
errors = {
|
|
||||||
'4xx': _routes.find(function (r) { return r.error === '4xx'; }),
|
|
||||||
'5xx': _routes.find(function (r) { return r.error === '5xx'; })
|
|
||||||
};
|
|
||||||
if (!inited) {
|
|
||||||
window.addEventListener('click', handle_click);
|
|
||||||
window.addEventListener('popstate', handle_popstate);
|
|
||||||
// prefetch
|
|
||||||
window.addEventListener('touchstart', handle_touchstart_mouseover);
|
|
||||||
window.addEventListener('mouseover', handle_touchstart_mouseover);
|
|
||||||
inited = true;
|
|
||||||
}
|
|
||||||
return Promise.resolve().then(function () {
|
|
||||||
var _a = window.location, hash = _a.hash, href = _a.href;
|
|
||||||
var deep_linked = hash && document.getElementById(hash.slice(1));
|
|
||||||
scroll_history[uid] = deep_linked ?
|
|
||||||
{ x: 0, y: deep_linked.getBoundingClientRect().top } :
|
|
||||||
scroll_state();
|
|
||||||
history.replaceState({ id: uid }, '', href);
|
|
||||||
var target = select_route(new URL(window.location.href));
|
|
||||||
return navigate(target, uid);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function goto(href, opts) {
|
|
||||||
if (opts === void 0) { opts = { replaceState: false }; }
|
|
||||||
var target = select_route(new URL(href, window.location.href));
|
|
||||||
if (target) {
|
|
||||||
navigate(target, null);
|
|
||||||
if (history)
|
|
||||||
history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
window.location.href = href;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function prefetchRoutes(pathnames) {
|
|
||||||
if (!routes)
|
|
||||||
throw new Error("You must call init() first");
|
|
||||||
return routes
|
|
||||||
.filter(function (route) {
|
|
||||||
if (!pathnames)
|
|
||||||
return true;
|
|
||||||
return pathnames.some(function (pathname) {
|
|
||||||
return route.error
|
|
||||||
? route.error === pathname
|
|
||||||
: route.pattern.test(pathname);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.reduce(function (promise, route) {
|
|
||||||
return promise.then(route.load);
|
|
||||||
}, Promise.resolve());
|
|
||||||
}
|
|
||||||
|
|
||||||
export { component, prefetch, init, goto, prefetchRoutes, prefetchRoutes as preloadRoutes };
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
This directory exists for legacy reasons and should be deleted before releasing version 1.
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
console.error('sapper/runtime/app.js has been deprecated in favour of sapper/runtime.js');
|
|
||||||
export * from '../runtime.js';
|
|
||||||
12
runtime/internal/Sapper.svelte
Normal file
12
runtime/internal/Sapper.svelte
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script>
|
||||||
|
import { setContext } from 'svelte';
|
||||||
|
import { CONTEXT_KEY } from './shared';
|
||||||
|
|
||||||
|
export let Root;
|
||||||
|
export let props;
|
||||||
|
export let session;
|
||||||
|
|
||||||
|
setContext(CONTEXT_KEY, session);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Root {...props}/>
|
||||||
7
runtime/internal/error.svelte
Normal file
7
runtime/internal/error.svelte
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<h1>{status}</h1>
|
||||||
|
|
||||||
|
<p>{error.message}</p>
|
||||||
|
|
||||||
|
{#if process.env.NODE_ENV === 'development'}
|
||||||
|
<pre>{error.stack}</pre>
|
||||||
|
{/if}
|
||||||
1
runtime/internal/layout.svelte
Normal file
1
runtime/internal/layout.svelte
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<slot></slot>
|
||||||
10
runtime/internal/shared.mjs
Normal file
10
runtime/internal/shared.mjs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export const stores = {
|
||||||
|
preloading: writable(false),
|
||||||
|
page: writable(null)
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CONTEXT_KEY = {};
|
||||||
|
|
||||||
|
export const preload = () => ({});
|
||||||
349
runtime/src/app/app.ts
Normal file
349
runtime/src/app/app.ts
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
import { writable } from 'svelte/store.mjs';
|
||||||
|
import App from '@sapper/internal/App.svelte';
|
||||||
|
import { stores } from '@sapper/internal/shared';
|
||||||
|
import { Root, root_preload, ErrorComponent, ignore, components, routes } from '@sapper/internal/manifest-client';
|
||||||
|
import {
|
||||||
|
Target,
|
||||||
|
ScrollPosition,
|
||||||
|
Component,
|
||||||
|
Redirect,
|
||||||
|
ComponentLoader,
|
||||||
|
ComponentConstructor,
|
||||||
|
Route,
|
||||||
|
Query,
|
||||||
|
Page
|
||||||
|
} from './types';
|
||||||
|
import goto from './goto';
|
||||||
|
|
||||||
|
declare const __SAPPER__;
|
||||||
|
export const initial_data = typeof __SAPPER__ !== 'undefined' && __SAPPER__;
|
||||||
|
|
||||||
|
let ready = false;
|
||||||
|
let root_component: Component;
|
||||||
|
let current_token: {};
|
||||||
|
let root_preloaded: Promise<any>;
|
||||||
|
let current_branch = [];
|
||||||
|
|
||||||
|
const session = writable(initial_data && initial_data.session);
|
||||||
|
|
||||||
|
let $session;
|
||||||
|
let session_dirty: boolean;
|
||||||
|
|
||||||
|
session.subscribe(async value => {
|
||||||
|
$session = value;
|
||||||
|
|
||||||
|
if (!ready) return;
|
||||||
|
session_dirty = true;
|
||||||
|
|
||||||
|
const target = select_target(new URL(location.href));
|
||||||
|
|
||||||
|
const token = current_token = {};
|
||||||
|
const { redirect, props, branch } = await hydrate_target(target);
|
||||||
|
if (token !== current_token) return; // a secondary navigation happened while we were loading
|
||||||
|
|
||||||
|
await render(redirect, branch, props, target.page);
|
||||||
|
});
|
||||||
|
|
||||||
|
export let prefetching: {
|
||||||
|
href: string;
|
||||||
|
promise: Promise<{ redirect?: Redirect, data?: any }>;
|
||||||
|
} = null;
|
||||||
|
export function set_prefetching(href, promise) {
|
||||||
|
prefetching = { href, promise };
|
||||||
|
}
|
||||||
|
|
||||||
|
export let store;
|
||||||
|
export function set_store(fn) {
|
||||||
|
store = fn(initial_data.store);
|
||||||
|
}
|
||||||
|
|
||||||
|
export let target: Node;
|
||||||
|
export function set_target(element) {
|
||||||
|
target = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let uid = 1;
|
||||||
|
export function set_uid(n) {
|
||||||
|
uid = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let cid: number;
|
||||||
|
export function set_cid(n) {
|
||||||
|
cid = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _history = typeof history !== 'undefined' ? history : {
|
||||||
|
pushState: (state: any, title: string, href: string) => {},
|
||||||
|
replaceState: (state: any, title: string, href: string) => {},
|
||||||
|
scrollRestoration: ''
|
||||||
|
};
|
||||||
|
export { _history as history };
|
||||||
|
|
||||||
|
export const scroll_history: Record<string, ScrollPosition> = {};
|
||||||
|
|
||||||
|
export function extract_query(search: string) {
|
||||||
|
const query = Object.create(null);
|
||||||
|
if (search.length > 0) {
|
||||||
|
search.slice(1).split('&').forEach(searchParam => {
|
||||||
|
let [, key, value] = /([^=]*)(?:=(.*))?/.exec(decodeURIComponent(searchParam));
|
||||||
|
value = (value || '').replace(/\+/g, ' ');
|
||||||
|
if (typeof query[key] === 'string') query[key] = [<string>query[key]];
|
||||||
|
if (typeof query[key] === 'object') (query[key] as string[]).push(value);
|
||||||
|
else query[key] = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function select_target(url: URL): Target {
|
||||||
|
if (url.origin !== location.origin) return null;
|
||||||
|
if (!url.pathname.startsWith(initial_data.baseUrl)) return null;
|
||||||
|
|
||||||
|
const path = url.pathname.slice(initial_data.baseUrl.length);
|
||||||
|
|
||||||
|
// avoid accidental clashes between server routes and page routes
|
||||||
|
if (ignore.some(pattern => pattern.test(path))) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < routes.length; i += 1) {
|
||||||
|
const route = routes[i];
|
||||||
|
|
||||||
|
const match = route.pattern.exec(path);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const query: Query = extract_query(url.search);
|
||||||
|
const part = route.parts[route.parts.length - 1];
|
||||||
|
const params = part.params ? part.params(match) : {};
|
||||||
|
|
||||||
|
const page = { path, query, params };
|
||||||
|
|
||||||
|
return { href: url.href, route, match, page };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handle_error(url: URL) {
|
||||||
|
const { pathname, search } = location;
|
||||||
|
const { session, preloaded, status, error } = initial_data;
|
||||||
|
|
||||||
|
if (!root_preloaded) {
|
||||||
|
root_preloaded = preloaded && preloaded[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
error,
|
||||||
|
status,
|
||||||
|
session,
|
||||||
|
level0: {
|
||||||
|
props: root_preloaded
|
||||||
|
},
|
||||||
|
level1: {
|
||||||
|
props: {
|
||||||
|
status,
|
||||||
|
error
|
||||||
|
},
|
||||||
|
component: ErrorComponent
|
||||||
|
},
|
||||||
|
segments: preloaded
|
||||||
|
|
||||||
|
}
|
||||||
|
const query = extract_query(search);
|
||||||
|
render(null, [], props, { path: pathname, query, params: {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scroll_state() {
|
||||||
|
return {
|
||||||
|
x: pageXOffset,
|
||||||
|
y: pageYOffset
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function navigate(target: Target, id: number, noscroll?: boolean, hash?: string): Promise<any> {
|
||||||
|
if (id) {
|
||||||
|
// popstate or initial navigation
|
||||||
|
cid = id;
|
||||||
|
} else {
|
||||||
|
const current_scroll = scroll_state();
|
||||||
|
|
||||||
|
// clicked on a link. preserve scroll state
|
||||||
|
scroll_history[cid] = current_scroll;
|
||||||
|
|
||||||
|
id = cid = ++uid;
|
||||||
|
scroll_history[cid] = noscroll ? current_scroll : { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
cid = id;
|
||||||
|
|
||||||
|
if (root_component) stores.preloading.set(true);
|
||||||
|
|
||||||
|
const loaded = prefetching && prefetching.href === target.href ?
|
||||||
|
prefetching.promise :
|
||||||
|
hydrate_target(target);
|
||||||
|
|
||||||
|
prefetching = null;
|
||||||
|
|
||||||
|
const token = current_token = {};
|
||||||
|
const { redirect, props, branch } = await loaded;
|
||||||
|
if (token !== current_token) return; // a secondary navigation happened while we were loading
|
||||||
|
|
||||||
|
await render(redirect, branch, props, target.page);
|
||||||
|
if (document.activeElement) document.activeElement.blur();
|
||||||
|
|
||||||
|
if (!noscroll) {
|
||||||
|
let scroll = scroll_history[id];
|
||||||
|
|
||||||
|
if (hash) {
|
||||||
|
// scroll is an element id (from a hash), we need to compute y.
|
||||||
|
const deep_linked = document.getElementById(hash.slice(1));
|
||||||
|
|
||||||
|
if (deep_linked) {
|
||||||
|
scroll = {
|
||||||
|
x: 0,
|
||||||
|
y: deep_linked.getBoundingClientRect().top
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scroll_history[cid] = scroll;
|
||||||
|
if (scroll) scrollTo(scroll.x, scroll.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function render(redirect: Redirect, branch: any[], props: any, page: Page) {
|
||||||
|
if (redirect) return goto(redirect.location, { replaceState: true });
|
||||||
|
|
||||||
|
stores.page.set(page);
|
||||||
|
stores.preloading.set(false);
|
||||||
|
|
||||||
|
if (root_component) {
|
||||||
|
root_component.$set(props);
|
||||||
|
} else {
|
||||||
|
props.session = session;
|
||||||
|
props.level0 = {
|
||||||
|
props: await root_preloaded
|
||||||
|
};
|
||||||
|
|
||||||
|
// first load — remove SSR'd <head> contents
|
||||||
|
const start = document.querySelector('#sapper-head-start');
|
||||||
|
const end = document.querySelector('#sapper-head-end');
|
||||||
|
|
||||||
|
if (start && end) {
|
||||||
|
while (start.nextSibling !== end) detach(start.nextSibling);
|
||||||
|
detach(start);
|
||||||
|
detach(end);
|
||||||
|
}
|
||||||
|
|
||||||
|
root_component = new App({
|
||||||
|
target,
|
||||||
|
props,
|
||||||
|
hydrate: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
current_branch = branch;
|
||||||
|
ready = true;
|
||||||
|
session_dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function hydrate_target(target: Target): Promise<{
|
||||||
|
redirect?: Redirect;
|
||||||
|
props?: any;
|
||||||
|
branch?: Array<{ Component: ComponentConstructor, preload: (page) => Promise<any>, segment: string }>;
|
||||||
|
}> {
|
||||||
|
const { route, page } = target;
|
||||||
|
const segments = page.path.split('/').filter(Boolean);
|
||||||
|
|
||||||
|
let redirect: Redirect = null;
|
||||||
|
|
||||||
|
const props = { error: null, status: 200, segments: [segments[0]] };
|
||||||
|
|
||||||
|
const preload_context = {
|
||||||
|
fetch: (url: string, opts?: any) => fetch(url, opts),
|
||||||
|
redirect: (statusCode: number, location: string) => {
|
||||||
|
if (redirect && (redirect.statusCode !== statusCode || redirect.location !== location)) {
|
||||||
|
throw new Error(`Conflicting redirects`);
|
||||||
|
}
|
||||||
|
redirect = { statusCode, location };
|
||||||
|
},
|
||||||
|
error: (status: number, error: Error | string) => {
|
||||||
|
props.error = typeof error === 'string' ? new Error(error) : error;
|
||||||
|
props.status = status;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!root_preloaded) {
|
||||||
|
root_preloaded = initial_data.preloaded[0] || root_preload.call(preload_context, {
|
||||||
|
path: page.path,
|
||||||
|
query: page.query,
|
||||||
|
params: {}
|
||||||
|
}, $session);
|
||||||
|
}
|
||||||
|
|
||||||
|
let branch;
|
||||||
|
let l = 1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
branch = await Promise.all(route.parts.map(async (part, i) => {
|
||||||
|
props.segments[l] = segments[i + 1]; // TODO make this less confusing
|
||||||
|
if (!part) return null;
|
||||||
|
|
||||||
|
const j = l++;
|
||||||
|
|
||||||
|
const segment = segments[i];
|
||||||
|
if (!session_dirty && current_branch[i] && current_branch[i].segment === segment && current_branch[i].part === part.i) return current_branch[i];
|
||||||
|
|
||||||
|
const { default: component, preload } = await load_component(components[part.i]);
|
||||||
|
|
||||||
|
let preloaded;
|
||||||
|
if (ready || !initial_data.preloaded[i + 1]) {
|
||||||
|
preloaded = preload
|
||||||
|
? await preload.call(preload_context, {
|
||||||
|
path: page.path,
|
||||||
|
query: page.query,
|
||||||
|
params: part.params ? part.params(target.match) : {}
|
||||||
|
}, $session)
|
||||||
|
: {};
|
||||||
|
} else {
|
||||||
|
preloaded = initial_data.preloaded[i + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (props[`level${j}`] = { component, props: preloaded, segment, part: part.i });
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
props.error = error;
|
||||||
|
props.status = 500;
|
||||||
|
branch = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { redirect, props, branch };
|
||||||
|
}
|
||||||
|
|
||||||
|
function load_css(chunk: string) {
|
||||||
|
const href = `client/${chunk}`;
|
||||||
|
if (document.querySelector(`link[href="${href}"]`)) return;
|
||||||
|
|
||||||
|
return new Promise((fulfil, reject) => {
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.rel = 'stylesheet';
|
||||||
|
link.href = href;
|
||||||
|
|
||||||
|
link.onload = () => fulfil();
|
||||||
|
link.onerror = reject;
|
||||||
|
|
||||||
|
document.head.appendChild(link);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function load_component(component: ComponentLoader): Promise<{
|
||||||
|
default: ComponentConstructor,
|
||||||
|
preload?: (input: any) => any
|
||||||
|
}> {
|
||||||
|
// TODO this is temporary — once placeholders are
|
||||||
|
// always rewritten, scratch the ternary
|
||||||
|
const promises: Array<Promise<any>> = (typeof component.css === 'string' ? [] : component.css.map(load_css));
|
||||||
|
promises.unshift(component.js());
|
||||||
|
return Promise.all(promises).then(values => values[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function detach(node: Node) {
|
||||||
|
node.parentNode.removeChild(node);
|
||||||
|
}
|
||||||
13
runtime/src/app/goto/index.ts
Normal file
13
runtime/src/app/goto/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { history, select_target, navigate, cid } from '../app';
|
||||||
|
|
||||||
|
export default function goto(href: string, opts = { replaceState: false }) {
|
||||||
|
const target = select_target(new URL(href, document.baseURI));
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
|
||||||
|
return navigate(target, null).then(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
location.href = href;
|
||||||
|
return new Promise(f => {}); // never resolves
|
||||||
|
}
|
||||||
12
runtime/src/app/index.ts
Normal file
12
runtime/src/app/index.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { getContext } from 'svelte';
|
||||||
|
import { CONTEXT_KEY, stores } from '@sapper/internal/shared';
|
||||||
|
|
||||||
|
export const preloading = { subscribe: stores.preloading.subscribe };
|
||||||
|
export const page = { subscribe: stores.page.subscribe };
|
||||||
|
|
||||||
|
export const getSession = () => getContext(CONTEXT_KEY);
|
||||||
|
|
||||||
|
export { default as start } from './start/index';
|
||||||
|
export { default as goto } from './goto/index';
|
||||||
|
export { default as prefetch } from './prefetch/index';
|
||||||
|
export { default as prefetchRoutes } from './prefetchRoutes/index';
|
||||||
14
runtime/src/app/prefetch/index.ts
Normal file
14
runtime/src/app/prefetch/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { select_target, prefetching, set_prefetching, hydrate_target } from '../app';
|
||||||
|
import { Target } from '../types';
|
||||||
|
|
||||||
|
export default function prefetch(href: string) {
|
||||||
|
const target: Target = select_target(new URL(href, document.baseURI));
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
if (!prefetching || href !== prefetching.href) {
|
||||||
|
set_prefetching(href, hydrate_target(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefetching.promise;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
runtime/src/app/prefetchRoutes/index.ts
Normal file
13
runtime/src/app/prefetchRoutes/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { components, routes } from '@sapper/internal/manifest-client';
|
||||||
|
import { load_component } from '../app';
|
||||||
|
|
||||||
|
export default function prefetchRoutes(pathnames: string[]) {
|
||||||
|
return routes
|
||||||
|
.filter(pathnames
|
||||||
|
? route => pathnames.some(pathname => route.pattern.test(pathname))
|
||||||
|
: () => true
|
||||||
|
)
|
||||||
|
.reduce((promise: Promise<any>, route) => promise.then(() => {
|
||||||
|
return Promise.all(route.parts.map(part => part && load_component(components[part.i])));
|
||||||
|
}), Promise.resolve());
|
||||||
|
}
|
||||||
133
runtime/src/app/start/index.ts
Normal file
133
runtime/src/app/start/index.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import {
|
||||||
|
cid,
|
||||||
|
history,
|
||||||
|
initial_data,
|
||||||
|
navigate,
|
||||||
|
scroll_history,
|
||||||
|
scroll_state,
|
||||||
|
select_target,
|
||||||
|
handle_error,
|
||||||
|
set_target,
|
||||||
|
uid,
|
||||||
|
set_uid,
|
||||||
|
set_cid
|
||||||
|
} from '../app';
|
||||||
|
import prefetch from '../prefetch/index';
|
||||||
|
|
||||||
|
export default function start(opts: {
|
||||||
|
target: Node
|
||||||
|
}) {
|
||||||
|
if ('scrollRestoration' in history) {
|
||||||
|
history.scrollRestoration = 'manual';
|
||||||
|
}
|
||||||
|
|
||||||
|
set_target(opts.target);
|
||||||
|
|
||||||
|
addEventListener('click', handle_click);
|
||||||
|
addEventListener('popstate', handle_popstate);
|
||||||
|
|
||||||
|
// prefetch
|
||||||
|
addEventListener('touchstart', trigger_prefetch);
|
||||||
|
addEventListener('mousemove', handle_mousemove);
|
||||||
|
|
||||||
|
return Promise.resolve().then(() => {
|
||||||
|
const { hash, href } = location;
|
||||||
|
|
||||||
|
history.replaceState({ id: uid }, '', href);
|
||||||
|
|
||||||
|
const url = new URL(location.href);
|
||||||
|
|
||||||
|
if (initial_data.error) return handle_error(url);
|
||||||
|
|
||||||
|
const target = select_target(url);
|
||||||
|
if (target) return navigate(target, uid, false, hash);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mousemove_timeout: NodeJS.Timer;
|
||||||
|
|
||||||
|
function handle_mousemove(event: MouseEvent) {
|
||||||
|
clearTimeout(mousemove_timeout);
|
||||||
|
mousemove_timeout = setTimeout(() => {
|
||||||
|
trigger_prefetch(event);
|
||||||
|
}, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
function trigger_prefetch(event: MouseEvent | TouchEvent) {
|
||||||
|
const a: HTMLAnchorElement = <HTMLAnchorElement>find_anchor(<Node>event.target);
|
||||||
|
if (!a || a.rel !== 'prefetch') return;
|
||||||
|
|
||||||
|
prefetch(a.href);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_click(event: MouseEvent) {
|
||||||
|
// Adapted from https://github.com/visionmedia/page.js
|
||||||
|
// MIT license https://github.com/visionmedia/page.js#license
|
||||||
|
if (which(event) !== 1) return;
|
||||||
|
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
|
||||||
|
if (event.defaultPrevented) return;
|
||||||
|
|
||||||
|
const a: HTMLAnchorElement | SVGAElement = <HTMLAnchorElement | SVGAElement>find_anchor(<Node>event.target);
|
||||||
|
if (!a) return;
|
||||||
|
|
||||||
|
if (!a.href) return;
|
||||||
|
|
||||||
|
// check if link is inside an svg
|
||||||
|
// in this case, both href and target are always inside an object
|
||||||
|
const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
|
||||||
|
const href = String(svg ? (<SVGAElement>a).href.baseVal : a.href);
|
||||||
|
|
||||||
|
if (href === location.href) {
|
||||||
|
if (!location.hash) event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore if tag has
|
||||||
|
// 1. 'download' attribute
|
||||||
|
// 2. rel='external' attribute
|
||||||
|
if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
|
||||||
|
|
||||||
|
// Ignore if <a> has a target
|
||||||
|
if (svg ? (<SVGAElement>a).target.baseVal : a.target) return;
|
||||||
|
|
||||||
|
const url = new URL(href);
|
||||||
|
|
||||||
|
// Don't handle hash changes
|
||||||
|
if (url.pathname === location.pathname && url.search === location.search) return;
|
||||||
|
|
||||||
|
const target = select_target(url);
|
||||||
|
if (target) {
|
||||||
|
const noscroll = a.hasAttribute('sapper-noscroll');
|
||||||
|
navigate(target, null, noscroll, url.hash);
|
||||||
|
event.preventDefault();
|
||||||
|
history.pushState({ id: cid }, '', url.href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function which(event: MouseEvent) {
|
||||||
|
return event.which === null ? event.button : event.which;
|
||||||
|
}
|
||||||
|
|
||||||
|
function find_anchor(node: Node) {
|
||||||
|
while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG <a> elements have a lowercase name
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_popstate(event: PopStateEvent) {
|
||||||
|
scroll_history[cid] = scroll_state();
|
||||||
|
|
||||||
|
if (event.state) {
|
||||||
|
const url = new URL(location.href);
|
||||||
|
const target = select_target(url);
|
||||||
|
if (target) {
|
||||||
|
navigate(target, event.state.id);
|
||||||
|
} else {
|
||||||
|
location.href = location.href;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// hashchange
|
||||||
|
set_uid(uid + 1);
|
||||||
|
set_cid(uid);
|
||||||
|
history.replaceState({ id: cid }, '', location.href);
|
||||||
|
}
|
||||||
|
}
|
||||||
62
runtime/src/app/types.ts
Normal file
62
runtime/src/app/types.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
export type Params = Record<string, string>;
|
||||||
|
export type Query = Record<string, string | true>;
|
||||||
|
export type RouteData = { params: Params, query: Query, path: string };
|
||||||
|
|
||||||
|
type Child = {
|
||||||
|
segment?: string;
|
||||||
|
props?: any;
|
||||||
|
component?: Component;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ComponentConstructor {
|
||||||
|
new (options: { target: Node, props: any, hydrate: boolean }): Component;
|
||||||
|
preload: (props: { params: Params, query: Query }) => Promise<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Component {
|
||||||
|
$set: (data: any) => void;
|
||||||
|
$destroy: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComponentLoader = {
|
||||||
|
js: () => Promise<{ default: ComponentConstructor }>,
|
||||||
|
css: string[]
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Route = {
|
||||||
|
pattern: RegExp;
|
||||||
|
parts: Array<{
|
||||||
|
i: number;
|
||||||
|
params?: (match: RegExpExecArray) => Record<string, string>;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Manifest = {
|
||||||
|
ignore: RegExp[];
|
||||||
|
root: ComponentConstructor;
|
||||||
|
error: () => Promise<{ default: ComponentConstructor }>;
|
||||||
|
pages: Route[]
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScrollPosition = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Target = {
|
||||||
|
href: string;
|
||||||
|
route: Route;
|
||||||
|
match: RegExpExecArray;
|
||||||
|
page: Page;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Redirect = {
|
||||||
|
statusCode: number;
|
||||||
|
location: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Page = {
|
||||||
|
path: string;
|
||||||
|
params: Record<string, string>;
|
||||||
|
query: Record<string, string | string[]>;
|
||||||
|
};
|
||||||
1
runtime/src/server/constants.js
Normal file
1
runtime/src/server/constants.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const IGNORE = '__SAPPER__IGNORE__';
|
||||||
1
runtime/src/server/index.ts
Normal file
1
runtime/src/server/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as middleware } from './middleware/index';
|
||||||
367
runtime/src/server/middleware/get_page_handler.ts
Normal file
367
runtime/src/server/middleware/get_page_handler.ts
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
import { writable } from 'svelte/store.mjs';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import cookie from 'cookie';
|
||||||
|
import devalue from 'devalue';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import URL from 'url';
|
||||||
|
import { IGNORE } from '../constants';
|
||||||
|
import { Manifest, Page, Props, Req, Res } from './types';
|
||||||
|
import { build_dir, dev, src_dir } from '@sapper/internal/manifest-server';
|
||||||
|
import { stores } from '@sapper/internal/shared';
|
||||||
|
import App from '@sapper/internal/App.svelte';
|
||||||
|
|
||||||
|
export function get_page_handler(
|
||||||
|
manifest: Manifest,
|
||||||
|
session_getter: (req: Req, res: Res) => any
|
||||||
|
) {
|
||||||
|
const get_build_info = dev
|
||||||
|
? () => JSON.parse(fs.readFileSync(path.join(build_dir, 'build.json'), 'utf-8'))
|
||||||
|
: (assets => () => assets)(JSON.parse(fs.readFileSync(path.join(build_dir, 'build.json'), 'utf-8')));
|
||||||
|
|
||||||
|
const template = dev
|
||||||
|
? () => read_template(src_dir)
|
||||||
|
: (str => () => str)(read_template(build_dir));
|
||||||
|
|
||||||
|
const has_service_worker = fs.existsSync(path.join(build_dir, 'service-worker.js'));
|
||||||
|
|
||||||
|
const { server_routes, pages } = manifest;
|
||||||
|
const error_route = manifest.error;
|
||||||
|
|
||||||
|
function handle_error(req: Req, res: Res, statusCode: number, error: Error | string) {
|
||||||
|
handle_page({
|
||||||
|
pattern: null,
|
||||||
|
parts: [
|
||||||
|
{ name: null, component: error_route }
|
||||||
|
]
|
||||||
|
}, req, res, statusCode, error || new Error('Unknown error in preload function'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handle_page(page: Page, req: Req, res: Res, status = 200, error: Error | string = null) {
|
||||||
|
const is_service_worker_index = req.path === '/service-worker-index.html';
|
||||||
|
const build_info: {
|
||||||
|
bundler: 'rollup' | 'webpack',
|
||||||
|
shimport: string | null,
|
||||||
|
assets: Record<string, string | string[]>,
|
||||||
|
legacy_assets?: Record<string, string>
|
||||||
|
} = get_build_info();
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/html');
|
||||||
|
res.setHeader('Cache-Control', dev ? 'no-cache' : 'max-age=600');
|
||||||
|
|
||||||
|
// preload main.js and current route
|
||||||
|
// TODO detect other stuff we can preload? images, CSS, fonts?
|
||||||
|
let preloaded_chunks = Array.isArray(build_info.assets.main) ? build_info.assets.main : [build_info.assets.main];
|
||||||
|
if (!error && !is_service_worker_index) {
|
||||||
|
page.parts.forEach(part => {
|
||||||
|
if (!part) return;
|
||||||
|
|
||||||
|
// using concat because it could be a string or an array. thanks webpack!
|
||||||
|
preloaded_chunks = preloaded_chunks.concat(build_info.assets[part.name]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (build_info.bundler === 'rollup') {
|
||||||
|
// TODO add dependencies and CSS
|
||||||
|
const link = preloaded_chunks
|
||||||
|
.filter(file => file && !file.match(/\.map$/))
|
||||||
|
.map(file => `<${req.baseUrl}/client/${file}>;rel="modulepreload"`)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
res.setHeader('Link', link);
|
||||||
|
} else {
|
||||||
|
const link = preloaded_chunks
|
||||||
|
.filter(file => file && !file.match(/\.map$/))
|
||||||
|
.map((file) => {
|
||||||
|
const as = /\.css$/.test(file) ? 'style' : 'script';
|
||||||
|
return `<${req.baseUrl}/client/${file}>;rel="preload";as="${as}"`;
|
||||||
|
})
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
res.setHeader('Link', link);
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = session_getter(req, res);
|
||||||
|
|
||||||
|
let redirect: { statusCode: number, location: string };
|
||||||
|
let preload_error: { statusCode: number, message: Error | string };
|
||||||
|
|
||||||
|
const preload_context = {
|
||||||
|
redirect: (statusCode: number, location: string) => {
|
||||||
|
if (redirect && (redirect.statusCode !== statusCode || redirect.location !== location)) {
|
||||||
|
throw new Error(`Conflicting redirects`);
|
||||||
|
}
|
||||||
|
location = location.replace(/^\//g, ''); // leading slash (only)
|
||||||
|
redirect = { statusCode, location };
|
||||||
|
},
|
||||||
|
error: (statusCode: number, message: Error | string) => {
|
||||||
|
preload_error = { statusCode, message };
|
||||||
|
},
|
||||||
|
fetch: (url: string, opts?: any) => {
|
||||||
|
const parsed = new URL.URL(url, `http://127.0.0.1:${process.env.PORT}${req.baseUrl ? req.baseUrl + '/' :''}`);
|
||||||
|
|
||||||
|
if (opts) {
|
||||||
|
opts = Object.assign({}, opts);
|
||||||
|
|
||||||
|
const include_cookies = (
|
||||||
|
opts.credentials === 'include' ||
|
||||||
|
opts.credentials === 'same-origin' && parsed.origin === `http://127.0.0.1:${process.env.PORT}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (include_cookies) {
|
||||||
|
opts.headers = Object.assign({}, opts.headers);
|
||||||
|
|
||||||
|
const cookies = Object.assign(
|
||||||
|
{},
|
||||||
|
cookie.parse(req.headers.cookie || ''),
|
||||||
|
cookie.parse(opts.headers.cookie || '')
|
||||||
|
);
|
||||||
|
|
||||||
|
const set_cookie = res.getHeader('Set-Cookie');
|
||||||
|
(Array.isArray(set_cookie) ? set_cookie : [set_cookie]).forEach(str => {
|
||||||
|
const match = /([^=]+)=([^;]+)/.exec(<string>str);
|
||||||
|
if (match) cookies[match[1]] = match[2];
|
||||||
|
});
|
||||||
|
|
||||||
|
const str = Object.keys(cookies)
|
||||||
|
.map(key => `${key}=${cookies[key]}`)
|
||||||
|
.join('; ');
|
||||||
|
|
||||||
|
opts.headers.cookie = str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(parsed.href, opts);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let preloaded;
|
||||||
|
let match;
|
||||||
|
let params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const root_preloaded = manifest.root_preload
|
||||||
|
? manifest.root_preload.call(preload_context, {
|
||||||
|
path: req.path,
|
||||||
|
query: req.query,
|
||||||
|
params: {}
|
||||||
|
}, session)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
match = error ? null : page.pattern.exec(req.path);
|
||||||
|
|
||||||
|
|
||||||
|
let toPreload = [root_preloaded];
|
||||||
|
if (!is_service_worker_index) {
|
||||||
|
toPreload = toPreload.concat(page.parts.map(part => {
|
||||||
|
if (!part) return null;
|
||||||
|
|
||||||
|
// the deepest level is used below, to initialise the store
|
||||||
|
params = part.params ? part.params(match) : {};
|
||||||
|
|
||||||
|
return part.preload
|
||||||
|
? part.preload.call(preload_context, {
|
||||||
|
path: req.path,
|
||||||
|
query: req.query,
|
||||||
|
params
|
||||||
|
}, session)
|
||||||
|
: {};
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
preloaded = await Promise.all(toPreload);
|
||||||
|
} catch (err) {
|
||||||
|
preload_error = { statusCode: 500, message: err };
|
||||||
|
preloaded = []; // appease TypeScript
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (redirect) {
|
||||||
|
const location = URL.resolve(req.baseUrl || '/', redirect.location);
|
||||||
|
|
||||||
|
res.statusCode = redirect.statusCode;
|
||||||
|
res.setHeader('Location', location);
|
||||||
|
res.end();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preload_error) {
|
||||||
|
handle_error(req, res, preload_error.statusCode, preload_error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = req.path.split('/').filter(Boolean);
|
||||||
|
|
||||||
|
// TODO make this less confusing
|
||||||
|
const layout_segments = [segments[0]];
|
||||||
|
let l = 1;
|
||||||
|
|
||||||
|
page.parts.forEach((part, i) => {
|
||||||
|
layout_segments[l] = segments[i + 1];
|
||||||
|
if (!part) return null;
|
||||||
|
l++;
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
segments: layout_segments,
|
||||||
|
status: error ? status : 200,
|
||||||
|
error: error ? error instanceof Error ? error : { message: error } : null,
|
||||||
|
session: writable(session),
|
||||||
|
level0: {
|
||||||
|
props: preloaded[0]
|
||||||
|
},
|
||||||
|
level1: {
|
||||||
|
segment: segments[0],
|
||||||
|
props: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!is_service_worker_index) {
|
||||||
|
let l = 1;
|
||||||
|
for (let i = 0; i < page.parts.length; i += 1) {
|
||||||
|
const part = page.parts[i];
|
||||||
|
if (!part) continue;
|
||||||
|
|
||||||
|
props[`level${l++}`] = {
|
||||||
|
component: part.component,
|
||||||
|
props: preloaded[i + 1] || {},
|
||||||
|
segment: segments[i]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stores.page.set({
|
||||||
|
path: req.path,
|
||||||
|
query: req.query,
|
||||||
|
params: params
|
||||||
|
});
|
||||||
|
|
||||||
|
const { html, head, css } = App.render(props);
|
||||||
|
|
||||||
|
const serialized = {
|
||||||
|
preloaded: `[${preloaded.map(data => try_serialize(data)).join(',')}]`,
|
||||||
|
session: session && try_serialize(session, err => {
|
||||||
|
throw new Error(`Failed to serialize session data: ${err.message}`);
|
||||||
|
}),
|
||||||
|
error: error && try_serialize(props.error)
|
||||||
|
};
|
||||||
|
|
||||||
|
let script = `__SAPPER__={${[
|
||||||
|
error && `error:${serialized.error},status:${status}`,
|
||||||
|
`baseUrl:"${req.baseUrl}"`,
|
||||||
|
serialized.preloaded && `preloaded:${serialized.preloaded}`,
|
||||||
|
serialized.session && `session:${serialized.session}`
|
||||||
|
].filter(Boolean).join(',')}};`;
|
||||||
|
|
||||||
|
if (has_service_worker) {
|
||||||
|
script += `if('serviceWorker' in navigator)navigator.serviceWorker.register('${req.baseUrl}/service-worker.js');`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = [].concat(build_info.assets.main).filter(file => file && /\.js$/.test(file))[0];
|
||||||
|
const main = `${req.baseUrl}/client/${file}`;
|
||||||
|
|
||||||
|
if (build_info.bundler === 'rollup') {
|
||||||
|
if (build_info.legacy_assets) {
|
||||||
|
const legacy_main = `${req.baseUrl}/client/legacy/${build_info.legacy_assets.main}`;
|
||||||
|
script += `(function(){try{eval("async function x(){}");var main="${main}"}catch(e){main="${legacy_main}"};var s=document.createElement("script");try{new Function("if(0)import('')")();s.src=main;s.type="module";s.crossOrigin="use-credentials";}catch(e){s.src="${req.baseUrl}/client/shimport@${build_info.shimport}.js";s.setAttribute("data-main",main);}document.head.appendChild(s);}());`;
|
||||||
|
} else {
|
||||||
|
script += `var s=document.createElement("script");try{new Function("if(0)import('')")();s.src="${main}";s.type="module";s.crossOrigin="use-credentials";}catch(e){s.src="${req.baseUrl}/client/shimport@${build_info.shimport}.js";s.setAttribute("data-main","${main}")}document.head.appendChild(s)`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
script += `</script><script src="${main}">`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let styles: string;
|
||||||
|
|
||||||
|
// TODO make this consistent across apps
|
||||||
|
// TODO embed build_info in placeholder.ts
|
||||||
|
if (build_info.css && build_info.css.main) {
|
||||||
|
const css_chunks = new Set();
|
||||||
|
if (build_info.css.main) css_chunks.add(build_info.css.main);
|
||||||
|
page.parts.forEach(part => {
|
||||||
|
if (!part) return;
|
||||||
|
const css_chunks_for_part = build_info.css.chunks[part.file];
|
||||||
|
|
||||||
|
if (css_chunks_for_part) {
|
||||||
|
css_chunks_for_part.forEach(file => {
|
||||||
|
css_chunks.add(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
styles = Array.from(css_chunks)
|
||||||
|
.map(href => `<link rel="stylesheet" href="client/${href}">`)
|
||||||
|
.join('')
|
||||||
|
} else {
|
||||||
|
styles = (css && css.code ? `<style>${css.code}</style>` : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// users can set a CSP nonce using res.locals.nonce
|
||||||
|
const nonce_attr = (res.locals && res.locals.nonce) ? ` nonce="${res.locals.nonce}"` : '';
|
||||||
|
|
||||||
|
const body = template()
|
||||||
|
.replace('%sapper.base%', () => `<base href="${req.baseUrl}/">`)
|
||||||
|
.replace('%sapper.scripts%', () => `<script${nonce_attr}>${script}</script>`)
|
||||||
|
.replace('%sapper.html%', () => html)
|
||||||
|
.replace('%sapper.head%', () => `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`)
|
||||||
|
.replace('%sapper.styles%', () => styles);
|
||||||
|
|
||||||
|
res.statusCode = status;
|
||||||
|
res.end(body);
|
||||||
|
} catch(err) {
|
||||||
|
console.log(err);
|
||||||
|
if (error) {
|
||||||
|
// we encountered an error while rendering the error page — oops
|
||||||
|
res.statusCode = 500;
|
||||||
|
res.end(`<pre>${escape_html(err.message)}</pre>`);
|
||||||
|
} else {
|
||||||
|
handle_error(req, res, 500, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return function find_route(req: Req, res: Res, next: () => void) {
|
||||||
|
if (req[IGNORE]) return next();
|
||||||
|
|
||||||
|
if (req.path === '/service-worker-index.html') {
|
||||||
|
const homePage = pages.find(page => page.pattern.test('/'));
|
||||||
|
handle_page(homePage, req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const page of pages) {
|
||||||
|
if (page.pattern.test(req.path)) {
|
||||||
|
handle_page(page, req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_error(req, res, 404, 'Not found');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function read_template(dir = build_dir) {
|
||||||
|
return fs.readFileSync(`${dir}/template.html`, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function try_serialize(data: any, fail?: (err) => void) {
|
||||||
|
try {
|
||||||
|
return devalue(data);
|
||||||
|
} catch (err) {
|
||||||
|
if (fail) fail(err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function escape_html(html: string) {
|
||||||
|
const chars: Record<string, string> = {
|
||||||
|
'"' : 'quot',
|
||||||
|
"'": '#39',
|
||||||
|
'&': 'amp',
|
||||||
|
'<' : 'lt',
|
||||||
|
'>' : 'gt'
|
||||||
|
};
|
||||||
|
|
||||||
|
return html.replace(/["'&<>]/g, c => `&${chars[c]};`);
|
||||||
|
}
|
||||||
78
runtime/src/server/middleware/get_server_route_handler.ts
Normal file
78
runtime/src/server/middleware/get_server_route_handler.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { IGNORE } from '../constants';
|
||||||
|
import { Req, Res, ServerRoute } from './types';
|
||||||
|
|
||||||
|
export function get_server_route_handler(routes: ServerRoute[]) {
|
||||||
|
async function handle_route(route: ServerRoute, req: Req, res: Res, next: () => void) {
|
||||||
|
req.params = route.params(route.pattern.exec(req.path));
|
||||||
|
|
||||||
|
const method = req.method.toLowerCase();
|
||||||
|
// 'delete' cannot be exported from a module because it is a keyword,
|
||||||
|
// so check for 'del' instead
|
||||||
|
const method_export = method === 'delete' ? 'del' : method;
|
||||||
|
const handle_method = route.handlers[method_export];
|
||||||
|
if (handle_method) {
|
||||||
|
if (process.env.SAPPER_EXPORT) {
|
||||||
|
const { write, end, setHeader } = res;
|
||||||
|
const chunks: any[] = [];
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
|
||||||
|
// intercept data so that it can be exported
|
||||||
|
res.write = function(chunk: any) {
|
||||||
|
chunks.push(Buffer.from(chunk));
|
||||||
|
write.apply(res, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
res.setHeader = function(name: string, value: string) {
|
||||||
|
headers[name.toLowerCase()] = value;
|
||||||
|
setHeader.apply(res, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
res.end = function(chunk?: any) {
|
||||||
|
if (chunk) chunks.push(Buffer.from(chunk));
|
||||||
|
end.apply(res, arguments);
|
||||||
|
|
||||||
|
process.send({
|
||||||
|
__sapper__: true,
|
||||||
|
event: 'file',
|
||||||
|
url: req.url,
|
||||||
|
method: req.method,
|
||||||
|
status: res.statusCode,
|
||||||
|
type: headers['content-type'],
|
||||||
|
body: Buffer.concat(chunks).toString()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const handle_next = (err?: Error) => {
|
||||||
|
if (err) {
|
||||||
|
res.statusCode = 500;
|
||||||
|
res.end(err.message);
|
||||||
|
} else {
|
||||||
|
process.nextTick(next);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await handle_method(req, res, handle_next);
|
||||||
|
} catch (err) {
|
||||||
|
handle_next(err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// no matching handler for method
|
||||||
|
process.nextTick(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return function find_route(req: Req, res: Res, next: () => void) {
|
||||||
|
if (req[IGNORE]) return next();
|
||||||
|
|
||||||
|
for (const route of routes) {
|
||||||
|
if (route.pattern.test(req.path)) {
|
||||||
|
handle_route(route, req, res, next);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
}
|
||||||
141
runtime/src/server/middleware/index.ts
Normal file
141
runtime/src/server/middleware/index.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { build_dir, dev, manifest } from '@sapper/internal/manifest-server';
|
||||||
|
import { Handler, Req, Res } from './types';
|
||||||
|
import { get_server_route_handler } from './get_server_route_handler';
|
||||||
|
import { get_page_handler } from './get_page_handler';
|
||||||
|
import { lookup } from './mime';
|
||||||
|
import { IGNORE } from '../constants';
|
||||||
|
|
||||||
|
export default function middleware(opts: {
|
||||||
|
session?: (req: Req, res: Res) => any,
|
||||||
|
ignore?: any
|
||||||
|
} = {}) {
|
||||||
|
const { session, ignore } = opts;
|
||||||
|
|
||||||
|
let emitted_basepath = false;
|
||||||
|
|
||||||
|
return compose_handlers([
|
||||||
|
ignore && ((req: Req, res: Res, next: () => void) => {
|
||||||
|
req[IGNORE] = should_ignore(req.path, ignore);
|
||||||
|
next();
|
||||||
|
}),
|
||||||
|
|
||||||
|
(req: Req, res: Res, next: () => void) => {
|
||||||
|
if (req[IGNORE]) return next();
|
||||||
|
|
||||||
|
if (req.baseUrl === undefined) {
|
||||||
|
let { originalUrl } = req;
|
||||||
|
if (req.url === '/' && originalUrl[originalUrl.length - 1] !== '/') {
|
||||||
|
originalUrl += '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
req.baseUrl = originalUrl
|
||||||
|
? originalUrl.slice(0, -req.url.length)
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!emitted_basepath && process.send) {
|
||||||
|
process.send({
|
||||||
|
__sapper__: true,
|
||||||
|
event: 'basepath',
|
||||||
|
basepath: req.baseUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
emitted_basepath = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.path === undefined) {
|
||||||
|
req.path = req.url.replace(/\?.*/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
|
||||||
|
fs.existsSync(path.join(build_dir, 'service-worker.js')) && serve({
|
||||||
|
pathname: '/service-worker.js',
|
||||||
|
cache_control: 'no-cache, no-store, must-revalidate'
|
||||||
|
}),
|
||||||
|
|
||||||
|
fs.existsSync(path.join(build_dir, 'service-worker.js.map')) && serve({
|
||||||
|
pathname: '/service-worker.js.map',
|
||||||
|
cache_control: 'no-cache, no-store, must-revalidate'
|
||||||
|
}),
|
||||||
|
|
||||||
|
serve({
|
||||||
|
prefix: '/client/',
|
||||||
|
cache_control: dev ? 'no-cache' : 'max-age=31536000, immutable'
|
||||||
|
}),
|
||||||
|
|
||||||
|
get_server_route_handler(manifest.server_routes),
|
||||||
|
|
||||||
|
get_page_handler(manifest, session || noop)
|
||||||
|
].filter(Boolean));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compose_handlers(handlers: Handler[]) {
|
||||||
|
return (req: Req, res: Res, next: () => void) => {
|
||||||
|
let i = 0;
|
||||||
|
function go() {
|
||||||
|
const handler = handlers[i];
|
||||||
|
|
||||||
|
if (handler) {
|
||||||
|
handler(req, res, () => {
|
||||||
|
i += 1;
|
||||||
|
go();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function should_ignore(uri: string, val: any) {
|
||||||
|
if (Array.isArray(val)) return val.some(x => should_ignore(uri, x));
|
||||||
|
if (val instanceof RegExp) return val.test(uri);
|
||||||
|
if (typeof val === 'function') return val(uri);
|
||||||
|
return uri.startsWith(val.charCodeAt(0) === 47 ? val : `/${val}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serve({ prefix, pathname, cache_control }: {
|
||||||
|
prefix?: string,
|
||||||
|
pathname?: string,
|
||||||
|
cache_control: string
|
||||||
|
}) {
|
||||||
|
const filter = pathname
|
||||||
|
? (req: Req) => req.path === pathname
|
||||||
|
: (req: Req) => req.path.startsWith(prefix);
|
||||||
|
|
||||||
|
const cache: Map<string, Buffer> = new Map();
|
||||||
|
|
||||||
|
const read = dev
|
||||||
|
? (file: string) => fs.readFileSync(path.resolve(build_dir, file))
|
||||||
|
: (file: string) => (cache.has(file) ? cache : cache.set(file, fs.readFileSync(path.resolve(build_dir, file)))).get(file)
|
||||||
|
|
||||||
|
return (req: Req, res: Res, next: () => void) => {
|
||||||
|
if (req[IGNORE]) return next();
|
||||||
|
|
||||||
|
if (filter(req)) {
|
||||||
|
const type = lookup(req.path);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const file = decodeURIComponent(req.path.slice(1));
|
||||||
|
const data = read(file);
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', type);
|
||||||
|
res.setHeader('Cache-Control', cache_control);
|
||||||
|
res.end(data);
|
||||||
|
} catch (err) {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end('not found');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function noop(){}
|
||||||
@@ -29,7 +29,7 @@ application/java-archive jar
|
|||||||
application/java-serialized-object ser
|
application/java-serialized-object ser
|
||||||
application/java-vm class
|
application/java-vm class
|
||||||
application/javascript js
|
application/javascript js
|
||||||
application/json json
|
application/json json map
|
||||||
application/jsonml+json jsonml
|
application/jsonml+json jsonml
|
||||||
application/lost+xml lostxml
|
application/lost+xml lostxml
|
||||||
application/mac-binhex40 hqx
|
application/mac-binhex40 hqx
|
||||||
63
runtime/src/server/middleware/types.ts
Normal file
63
runtime/src/server/middleware/types.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { ClientRequest, ServerResponse } from 'http';
|
||||||
|
|
||||||
|
export type ServerRoute = {
|
||||||
|
pattern: RegExp;
|
||||||
|
handlers: Record<string, Handler>;
|
||||||
|
params: (match: RegExpMatchArray) => Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Page = {
|
||||||
|
pattern: RegExp;
|
||||||
|
parts: Array<{
|
||||||
|
name: string;
|
||||||
|
component: Component;
|
||||||
|
params?: (match: RegExpMatchArray) => Record<string, string>;
|
||||||
|
preload?: (data: any) => any | Promise<any>;
|
||||||
|
}>
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Manifest = {
|
||||||
|
server_routes: ServerRoute[];
|
||||||
|
pages: Page[];
|
||||||
|
root: Component;
|
||||||
|
root_preload?: (data: any) => any | Promise<any>;
|
||||||
|
error: Component;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Handler = (req: Req, res: Res, next: () => void) => void;
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
error?: { message: string };
|
||||||
|
status?: number;
|
||||||
|
child: {
|
||||||
|
segment: string;
|
||||||
|
component: Component;
|
||||||
|
props: Props;
|
||||||
|
};
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Req extends ClientRequest {
|
||||||
|
url: string;
|
||||||
|
baseUrl: string;
|
||||||
|
originalUrl: string;
|
||||||
|
method: string;
|
||||||
|
path: string;
|
||||||
|
params: Record<string, string>;
|
||||||
|
query: Record<string, string>;
|
||||||
|
headers: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Res extends ServerResponse {
|
||||||
|
write: (data: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ServerResponse };
|
||||||
|
|
||||||
|
interface Component {
|
||||||
|
render: (data: any) => {
|
||||||
|
head: string;
|
||||||
|
css: { code: string, map: any };
|
||||||
|
html: string
|
||||||
|
}
|
||||||
|
}
|
||||||
2
sapper
2
sapper
@@ -1,2 +1,2 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
require('./dist/cli.ts.js');
|
require('./dist/cli.js');
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
let source;
|
let source;
|
||||||
|
|
||||||
function check() {
|
function check() {
|
||||||
|
if (typeof module === 'undefined') return;
|
||||||
|
|
||||||
if (module.hot.status() === 'idle') {
|
if (module.hot.status() === 'idle') {
|
||||||
module.hot.check(true).then(modules => {
|
module.hot.check(true).then(modules => {
|
||||||
console.log(`[SAPPER] applied HMR update`);
|
console.log(`[SAPPER] applied HMR update`);
|
||||||
|
|||||||
4
src/api.ts
Normal file
4
src/api.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { dev } from './api/dev';
|
||||||
|
export { build } from './api/build';
|
||||||
|
export { export } from './api/export';
|
||||||
|
export { find_page } from './api/find_page';
|
||||||
141
src/api/build.ts
Normal file
141
src/api/build.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import minify_html from './utils/minify_html';
|
||||||
|
import { create_compilers, create_app, create_manifest_data, create_serviceworker_manifest } from '../core';
|
||||||
|
import { copy_shimport } from './utils/copy_shimport';
|
||||||
|
import read_template from '../core/read_template';
|
||||||
|
import { CompileResult } from '../core/create_compilers/interfaces';
|
||||||
|
import { noop } from './utils/noop';
|
||||||
|
import validate_bundler from './utils/validate_bundler';
|
||||||
|
import { copy_runtime } from './utils/copy_runtime';
|
||||||
|
import { rimraf, mkdirp } from './utils/fs_utils';
|
||||||
|
|
||||||
|
type Opts = {
|
||||||
|
cwd?: string;
|
||||||
|
src?: string;
|
||||||
|
routes?: string;
|
||||||
|
dest?: string;
|
||||||
|
output?: string;
|
||||||
|
static?: string;
|
||||||
|
legacy?: boolean;
|
||||||
|
bundler?: 'rollup' | 'webpack';
|
||||||
|
oncompile?: ({ type, result }: { type: string, result: CompileResult }) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function build({
|
||||||
|
cwd,
|
||||||
|
src = 'src',
|
||||||
|
routes = 'src/routes',
|
||||||
|
output = 'src/node_modules/@sapper',
|
||||||
|
static: static_files = 'static',
|
||||||
|
dest = '__sapper__/build',
|
||||||
|
|
||||||
|
bundler,
|
||||||
|
legacy = false,
|
||||||
|
oncompile = noop
|
||||||
|
}: Opts = {}) {
|
||||||
|
bundler = validate_bundler(bundler);
|
||||||
|
|
||||||
|
cwd = path.resolve(cwd);
|
||||||
|
src = path.resolve(cwd, src);
|
||||||
|
dest = path.resolve(cwd, dest);
|
||||||
|
routes = path.resolve(cwd, routes);
|
||||||
|
output = path.resolve(cwd, output);
|
||||||
|
static_files = path.resolve(cwd, static_files);
|
||||||
|
|
||||||
|
if (legacy && bundler === 'webpack') {
|
||||||
|
throw new Error(`Legacy builds are not supported for projects using webpack`);
|
||||||
|
}
|
||||||
|
|
||||||
|
rimraf(output);
|
||||||
|
mkdirp(output);
|
||||||
|
copy_runtime(output);
|
||||||
|
|
||||||
|
rimraf(dest);
|
||||||
|
mkdirp(`${dest}/client`);
|
||||||
|
copy_shimport(dest);
|
||||||
|
|
||||||
|
// minify src/template.html
|
||||||
|
// TODO compile this to a function? could be quicker than str.replace(...).replace(...).replace(...)
|
||||||
|
const template = read_template(src);
|
||||||
|
|
||||||
|
// remove this in a future version
|
||||||
|
if (template.indexOf('%sapper.base%') === -1) {
|
||||||
|
const error = new Error(`As of Sapper v0.10, your template.html file must include %sapper.base% in the <head>`);
|
||||||
|
error.code = `missing-sapper-base`;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(`${dest}/template.html`, minify_html(template));
|
||||||
|
|
||||||
|
const manifest_data = create_manifest_data(routes);
|
||||||
|
|
||||||
|
// create src/node_modules/@sapper/app.mjs and server.mjs
|
||||||
|
create_app({
|
||||||
|
bundler,
|
||||||
|
manifest_data,
|
||||||
|
cwd,
|
||||||
|
src,
|
||||||
|
dest,
|
||||||
|
routes,
|
||||||
|
output,
|
||||||
|
dev: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const { client, server, serviceworker } = await create_compilers(bundler, cwd, src, dest, true);
|
||||||
|
|
||||||
|
const client_result = await client.compile();
|
||||||
|
oncompile({
|
||||||
|
type: 'client',
|
||||||
|
result: client_result
|
||||||
|
});
|
||||||
|
|
||||||
|
const build_info = client_result.to_json(manifest_data, { src, routes, dest });
|
||||||
|
|
||||||
|
if (legacy) {
|
||||||
|
process.env.SAPPER_LEGACY_BUILD = 'true';
|
||||||
|
const { client } = await create_compilers(bundler, cwd, src, dest, true);
|
||||||
|
|
||||||
|
const client_result = await client.compile();
|
||||||
|
|
||||||
|
oncompile({
|
||||||
|
type: 'client (legacy)',
|
||||||
|
result: client_result
|
||||||
|
});
|
||||||
|
|
||||||
|
client_result.to_json(manifest_data, { src, routes, dest });
|
||||||
|
build_info.legacy_assets = client_result.assets;
|
||||||
|
delete process.env.SAPPER_LEGACY_BUILD;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(dest, 'build.json'), JSON.stringify(build_info));
|
||||||
|
|
||||||
|
const server_stats = await server.compile();
|
||||||
|
oncompile({
|
||||||
|
type: 'server',
|
||||||
|
result: server_stats
|
||||||
|
});
|
||||||
|
|
||||||
|
let serviceworker_stats;
|
||||||
|
|
||||||
|
if (serviceworker) {
|
||||||
|
|
||||||
|
const client_files = client_result.chunks
|
||||||
|
.filter(chunk => !chunk.file.endsWith('.map')) // SW does not need to cache sourcemap files
|
||||||
|
.map(chunk => `client/${chunk.file}`);
|
||||||
|
|
||||||
|
create_serviceworker_manifest({
|
||||||
|
manifest_data,
|
||||||
|
output,
|
||||||
|
client_files,
|
||||||
|
static_files
|
||||||
|
});
|
||||||
|
|
||||||
|
serviceworker_stats = await serviceworker.compile();
|
||||||
|
|
||||||
|
oncompile({
|
||||||
|
type: 'serviceworker',
|
||||||
|
result: serviceworker_stats
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
513
src/api/dev.ts
Normal file
513
src/api/dev.ts
Normal file
@@ -0,0 +1,513 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as http from 'http';
|
||||||
|
import * as child_process from 'child_process';
|
||||||
|
import * as ports from 'port-authority';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { create_manifest_data, create_app, create_compilers, create_serviceworker_manifest } from '../core';
|
||||||
|
import { Compiler, Compilers } from '../core/create_compilers';
|
||||||
|
import { CompileResult } from '../core/create_compilers/interfaces';
|
||||||
|
import Deferred from './utils/Deferred';
|
||||||
|
import validate_bundler from './utils/validate_bundler';
|
||||||
|
import { copy_shimport } from './utils/copy_shimport';
|
||||||
|
import { ManifestData, FatalEvent, ErrorEvent, ReadyEvent, InvalidEvent } from '../interfaces';
|
||||||
|
import read_template from '../core/read_template';
|
||||||
|
import { noop } from './utils/noop';
|
||||||
|
import { copy_runtime } from './utils/copy_runtime';
|
||||||
|
import { rimraf, mkdirp } from './utils/fs_utils';
|
||||||
|
|
||||||
|
type Opts = {
|
||||||
|
cwd?: string,
|
||||||
|
src?: string,
|
||||||
|
dest?: string,
|
||||||
|
routes?: string,
|
||||||
|
output?: string,
|
||||||
|
static?: string,
|
||||||
|
'dev-port'?: number,
|
||||||
|
live?: boolean,
|
||||||
|
hot?: boolean,
|
||||||
|
'devtools-port'?: number,
|
||||||
|
bundler?: 'rollup' | 'webpack',
|
||||||
|
port?: number
|
||||||
|
};
|
||||||
|
|
||||||
|
export function dev(opts: Opts) {
|
||||||
|
return new Watcher(opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Watcher extends EventEmitter {
|
||||||
|
bundler: 'rollup' | 'webpack';
|
||||||
|
dirs: {
|
||||||
|
cwd: string;
|
||||||
|
src: string;
|
||||||
|
dest: string;
|
||||||
|
routes: string;
|
||||||
|
output: string;
|
||||||
|
static: string;
|
||||||
|
}
|
||||||
|
port: number;
|
||||||
|
closed: boolean;
|
||||||
|
|
||||||
|
dev_port: number;
|
||||||
|
live: boolean;
|
||||||
|
hot: boolean;
|
||||||
|
|
||||||
|
devtools_port: number;
|
||||||
|
|
||||||
|
dev_server: DevServer;
|
||||||
|
proc: child_process.ChildProcess;
|
||||||
|
filewatchers: Array<{ close: () => void }>;
|
||||||
|
deferred: Deferred;
|
||||||
|
|
||||||
|
crashed: boolean;
|
||||||
|
restarting: boolean;
|
||||||
|
current_build: {
|
||||||
|
changed: Set<string>;
|
||||||
|
rebuilding: Set<string>;
|
||||||
|
unique_warnings: Set<string>;
|
||||||
|
unique_errors: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
cwd = '.',
|
||||||
|
src = 'src',
|
||||||
|
routes = 'src/routes',
|
||||||
|
output = 'src/node_modules/@sapper',
|
||||||
|
static: static_files = 'static',
|
||||||
|
dest = '__sapper__/dev',
|
||||||
|
'dev-port': dev_port,
|
||||||
|
live,
|
||||||
|
hot,
|
||||||
|
'devtools-port': devtools_port,
|
||||||
|
bundler,
|
||||||
|
port = +process.env.PORT
|
||||||
|
}: Opts) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
cwd = path.resolve(cwd);
|
||||||
|
|
||||||
|
this.bundler = validate_bundler(bundler);
|
||||||
|
this.dirs = {
|
||||||
|
cwd,
|
||||||
|
src: path.resolve(cwd, src),
|
||||||
|
dest: path.resolve(cwd, dest),
|
||||||
|
routes: path.resolve(cwd, routes),
|
||||||
|
output: path.resolve(cwd, output),
|
||||||
|
static: path.resolve(cwd, static_files)
|
||||||
|
};
|
||||||
|
|
||||||
|
this.port = port;
|
||||||
|
this.closed = false;
|
||||||
|
|
||||||
|
this.dev_port = dev_port;
|
||||||
|
this.live = live;
|
||||||
|
this.hot = hot;
|
||||||
|
|
||||||
|
this.devtools_port = devtools_port;
|
||||||
|
|
||||||
|
this.filewatchers = [];
|
||||||
|
|
||||||
|
this.current_build = {
|
||||||
|
changed: new Set(),
|
||||||
|
rebuilding: new Set(),
|
||||||
|
unique_errors: new Set(),
|
||||||
|
unique_warnings: new Set()
|
||||||
|
};
|
||||||
|
|
||||||
|
// remove this in a future version
|
||||||
|
const template = read_template(src);
|
||||||
|
if (template.indexOf('%sapper.base%') === -1) {
|
||||||
|
const error = new Error(`As of Sapper v0.10, your template.html file must include %sapper.base% in the <head>`);
|
||||||
|
error.code = `missing-sapper-base`;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.env.NODE_ENV = 'development';
|
||||||
|
|
||||||
|
process.on('exit', () => {
|
||||||
|
this.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if (this.port) {
|
||||||
|
if (!await ports.check(this.port)) {
|
||||||
|
this.emit('fatal', <FatalEvent>{
|
||||||
|
message: `Port ${this.port} is unavailable`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.port = await ports.find(3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { cwd, src, dest, routes, output, static: static_files } = this.dirs;
|
||||||
|
|
||||||
|
rimraf(output);
|
||||||
|
mkdirp(output);
|
||||||
|
copy_runtime(output);
|
||||||
|
|
||||||
|
rimraf(dest);
|
||||||
|
mkdirp(`${dest}/client`);
|
||||||
|
if (this.bundler === 'rollup') copy_shimport(dest);
|
||||||
|
|
||||||
|
if (!this.dev_port) this.dev_port = await ports.find(10000);
|
||||||
|
|
||||||
|
// Chrome looks for debugging targets on ports 9222 and 9229 by default
|
||||||
|
if (!this.devtools_port) this.devtools_port = await ports.find(9222);
|
||||||
|
|
||||||
|
let manifest_data: ManifestData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
manifest_data = create_manifest_data(routes);
|
||||||
|
create_app({
|
||||||
|
bundler: this.bundler,
|
||||||
|
manifest_data,
|
||||||
|
dev: true,
|
||||||
|
dev_port: this.dev_port,
|
||||||
|
cwd, src, dest, routes, output
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.emit('fatal', <FatalEvent>{
|
||||||
|
message: err.message
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dev_server = new DevServer(this.dev_port);
|
||||||
|
|
||||||
|
this.filewatchers.push(
|
||||||
|
watch_dir(
|
||||||
|
routes,
|
||||||
|
({ path: file, stats }) => {
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
return path.basename(file)[0] !== '_';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
try {
|
||||||
|
const new_manifest_data = create_manifest_data(routes);
|
||||||
|
create_app({
|
||||||
|
bundler: this.bundler,
|
||||||
|
manifest_data, // TODO is this right? not new_manifest_data?
|
||||||
|
dev: true,
|
||||||
|
dev_port: this.dev_port,
|
||||||
|
cwd, src, dest, routes, output
|
||||||
|
});
|
||||||
|
|
||||||
|
manifest_data = new_manifest_data;
|
||||||
|
} catch (error) {
|
||||||
|
this.emit('error', <ErrorEvent>{
|
||||||
|
type: 'manifest',
|
||||||
|
error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
fs.watch(`${src}/template.html`, () => {
|
||||||
|
this.dev_server.send({
|
||||||
|
action: 'reload'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let deferred = new Deferred();
|
||||||
|
|
||||||
|
// TODO watch the configs themselves?
|
||||||
|
const compilers: Compilers = await create_compilers(this.bundler, cwd, src, dest, false);
|
||||||
|
|
||||||
|
const emitFatal = () => {
|
||||||
|
this.emit('fatal', <FatalEvent>{
|
||||||
|
message: `Server crashed`
|
||||||
|
});
|
||||||
|
|
||||||
|
this.crashed = true;
|
||||||
|
this.proc = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.watch(compilers.server, {
|
||||||
|
name: 'server',
|
||||||
|
|
||||||
|
invalid: filename => {
|
||||||
|
this.restart(filename, 'server');
|
||||||
|
},
|
||||||
|
|
||||||
|
handle_result: (result: CompileResult) => {
|
||||||
|
deferred.promise.then(() => {
|
||||||
|
const restart = () => {
|
||||||
|
this.crashed = false;
|
||||||
|
|
||||||
|
ports.wait(this.port)
|
||||||
|
.then((() => {
|
||||||
|
this.emit('ready', <ReadyEvent>{
|
||||||
|
port: this.port,
|
||||||
|
process: this.proc
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.hot && this.bundler === 'webpack') {
|
||||||
|
this.dev_server.send({
|
||||||
|
status: 'completed'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.dev_server.send({
|
||||||
|
action: 'reload'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.catch(err => {
|
||||||
|
if (this.crashed) return;
|
||||||
|
|
||||||
|
this.emit('fatal', <FatalEvent>{
|
||||||
|
message: `Server is not listening on port ${this.port}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.proc) {
|
||||||
|
this.proc.removeListener('exit', emitFatal);
|
||||||
|
this.proc.kill();
|
||||||
|
this.proc.on('exit', restart);
|
||||||
|
} else {
|
||||||
|
restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// we need to give the child process its own DevTools port,
|
||||||
|
// otherwise Node will try to use the parent's (and fail)
|
||||||
|
const debugArgRegex = /--inspect(?:-brk|-port)?|--debug-port/;
|
||||||
|
const execArgv = process.execArgv.slice();
|
||||||
|
if (execArgv.some((arg: string) => !!arg.match(debugArgRegex))) {
|
||||||
|
execArgv.push(`--inspect-port=${this.devtools_port}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.proc = child_process.fork(`${dest}/server/server.js`, [], {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
env: Object.assign({
|
||||||
|
PORT: this.port
|
||||||
|
}, process.env),
|
||||||
|
stdio: ['ipc'],
|
||||||
|
execArgv
|
||||||
|
});
|
||||||
|
|
||||||
|
this.proc.stdout.on('data', chunk => {
|
||||||
|
this.emit('stdout', chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.proc.stderr.on('data', chunk => {
|
||||||
|
this.emit('stderr', chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.proc.on('message', message => {
|
||||||
|
if (message.__sapper__ && message.event === 'basepath') {
|
||||||
|
this.emit('basepath', {
|
||||||
|
basepath: message.basepath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.proc.on('exit', emitFatal);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.watch(compilers.client, {
|
||||||
|
name: 'client',
|
||||||
|
|
||||||
|
invalid: filename => {
|
||||||
|
this.restart(filename, 'client');
|
||||||
|
deferred = new Deferred();
|
||||||
|
|
||||||
|
// TODO we should delete old assets. due to a webpack bug
|
||||||
|
// i don't even begin to comprehend, this is apparently
|
||||||
|
// quite difficult
|
||||||
|
},
|
||||||
|
|
||||||
|
handle_result: (result: CompileResult) => {
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dest, 'build.json'),
|
||||||
|
|
||||||
|
// TODO should be more explicit that to_json has effects
|
||||||
|
JSON.stringify(result.to_json(manifest_data, this.dirs), null, ' ')
|
||||||
|
);
|
||||||
|
|
||||||
|
const client_files = result.chunks.map(chunk => `client/${chunk.file}`);
|
||||||
|
|
||||||
|
create_serviceworker_manifest({
|
||||||
|
manifest_data,
|
||||||
|
output,
|
||||||
|
client_files,
|
||||||
|
static_files
|
||||||
|
});
|
||||||
|
|
||||||
|
deferred.fulfil();
|
||||||
|
|
||||||
|
// we need to wait a beat before watching the service
|
||||||
|
// worker, because of some webpack nonsense
|
||||||
|
setTimeout(watch_serviceworker, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let watch_serviceworker = compilers.serviceworker
|
||||||
|
? () => {
|
||||||
|
watch_serviceworker = noop;
|
||||||
|
|
||||||
|
this.watch(compilers.serviceworker, {
|
||||||
|
name: 'service worker'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: noop;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (this.closed) return;
|
||||||
|
this.closed = true;
|
||||||
|
|
||||||
|
if (this.dev_server) this.dev_server.close();
|
||||||
|
|
||||||
|
if (this.proc) this.proc.kill();
|
||||||
|
this.filewatchers.forEach(watcher => {
|
||||||
|
watcher.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
restart(filename: string, type: string) {
|
||||||
|
if (this.restarting) {
|
||||||
|
this.current_build.changed.add(filename);
|
||||||
|
this.current_build.rebuilding.add(type);
|
||||||
|
} else {
|
||||||
|
this.restarting = true;
|
||||||
|
|
||||||
|
this.current_build = {
|
||||||
|
changed: new Set([filename]),
|
||||||
|
rebuilding: new Set([type]),
|
||||||
|
unique_warnings: new Set(),
|
||||||
|
unique_errors: new Set()
|
||||||
|
};
|
||||||
|
|
||||||
|
process.nextTick(() => {
|
||||||
|
this.emit('invalid', <InvalidEvent>{
|
||||||
|
changed: Array.from(this.current_build.changed),
|
||||||
|
invalid: {
|
||||||
|
server: this.current_build.rebuilding.has('server'),
|
||||||
|
client: this.current_build.rebuilding.has('client'),
|
||||||
|
serviceworker: this.current_build.rebuilding.has('serviceworker'),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.restarting = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(compiler: Compiler, { name, invalid = noop, handle_result = noop }: {
|
||||||
|
name: string,
|
||||||
|
invalid?: (filename: string) => void;
|
||||||
|
handle_result?: (result: CompileResult) => void;
|
||||||
|
}) {
|
||||||
|
compiler.oninvalid(invalid);
|
||||||
|
|
||||||
|
compiler.watch((error?: Error, result?: CompileResult) => {
|
||||||
|
if (error) {
|
||||||
|
this.emit('error', <ErrorEvent>{
|
||||||
|
type: name,
|
||||||
|
error
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.emit('build', {
|
||||||
|
type: name,
|
||||||
|
|
||||||
|
duration: result.duration,
|
||||||
|
errors: result.errors,
|
||||||
|
warnings: result.warnings
|
||||||
|
});
|
||||||
|
|
||||||
|
handle_result(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const INTERVAL = 10000;
|
||||||
|
|
||||||
|
class DevServer {
|
||||||
|
clients: Set<http.ServerResponse>;
|
||||||
|
interval: NodeJS.Timer;
|
||||||
|
_: http.Server;
|
||||||
|
|
||||||
|
constructor(port: number, interval = 10000) {
|
||||||
|
this.clients = new Set();
|
||||||
|
|
||||||
|
this._ = http.createServer((req, res) => {
|
||||||
|
if (req.url !== '/__sapper__') return;
|
||||||
|
|
||||||
|
req.socket.setKeepAlive(true);
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Headers': 'Cache-Control',
|
||||||
|
'Content-Type': 'text/event-stream;charset=utf-8',
|
||||||
|
'Cache-Control': 'no-cache, no-transform',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
// While behind nginx, event stream should not be buffered:
|
||||||
|
// http://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffering
|
||||||
|
'X-Accel-Buffering': 'no'
|
||||||
|
});
|
||||||
|
|
||||||
|
res.write('\n');
|
||||||
|
|
||||||
|
this.clients.add(res);
|
||||||
|
req.on('close', () => {
|
||||||
|
this.clients.delete(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this._.listen(port);
|
||||||
|
|
||||||
|
this.interval = setInterval(() => {
|
||||||
|
this.send(null);
|
||||||
|
}, INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this._.close();
|
||||||
|
clearInterval(this.interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
send(data: any) {
|
||||||
|
this.clients.forEach(client => {
|
||||||
|
client.write(`data: ${JSON.stringify(data)}\n\n`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function watch_dir(
|
||||||
|
dir: string,
|
||||||
|
filter: ({ path, stats }: { path: string, stats: fs.Stats }) => boolean,
|
||||||
|
callback: () => void
|
||||||
|
) {
|
||||||
|
let watch: any;
|
||||||
|
let closed = false;
|
||||||
|
|
||||||
|
import('cheap-watch').then(({ default: CheapWatch }) => {
|
||||||
|
if (closed) return;
|
||||||
|
|
||||||
|
watch = new CheapWatch({ dir, filter, debounce: 50 });
|
||||||
|
|
||||||
|
watch.on('+', ({ isNew }: { isNew: boolean }) => {
|
||||||
|
if (isNew) callback();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch.on('-', callback);
|
||||||
|
|
||||||
|
watch.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
close: () => {
|
||||||
|
if (watch) watch.close();
|
||||||
|
closed = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
217
src/api/export.ts
Normal file
217
src/api/export.ts
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
import * as child_process from 'child_process';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as url from 'url';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import * as yootils from 'yootils';
|
||||||
|
import * as ports from 'port-authority';
|
||||||
|
import clean_html from './utils/clean_html';
|
||||||
|
import minify_html from './utils/minify_html';
|
||||||
|
import Deferred from './utils/Deferred';
|
||||||
|
import { noop } from './utils/noop';
|
||||||
|
import { parse as parseLinkHeader } from 'http-link-header';
|
||||||
|
import { rimraf, copy, mkdirp } from './utils/fs_utils';
|
||||||
|
|
||||||
|
type Opts = {
|
||||||
|
build_dir?: string,
|
||||||
|
export_dir?: string,
|
||||||
|
cwd?: string,
|
||||||
|
static?: string,
|
||||||
|
basepath?: string,
|
||||||
|
timeout?: number | false,
|
||||||
|
oninfo?: ({ message }: { message: string }) => void;
|
||||||
|
onfile?: ({ file, size, status }: { file: string, size: number, status: number }) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Ref = {
|
||||||
|
uri: string,
|
||||||
|
rel: string,
|
||||||
|
as: string
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolve(from: string, to: string) {
|
||||||
|
return url.parse(url.resolve(from, to));
|
||||||
|
}
|
||||||
|
|
||||||
|
type URL = url.UrlWithStringQuery;
|
||||||
|
|
||||||
|
export { _export as export };
|
||||||
|
|
||||||
|
async function _export({
|
||||||
|
cwd,
|
||||||
|
static: static_files = 'static',
|
||||||
|
build_dir = '__sapper__/build',
|
||||||
|
export_dir = '__sapper__/export',
|
||||||
|
basepath = '',
|
||||||
|
timeout = 5000,
|
||||||
|
oninfo = noop,
|
||||||
|
onfile = noop
|
||||||
|
}: Opts = {}) {
|
||||||
|
basepath = basepath.replace(/^\//, '')
|
||||||
|
|
||||||
|
cwd = path.resolve(cwd);
|
||||||
|
static_files = path.resolve(cwd, static_files);
|
||||||
|
build_dir = path.resolve(cwd, build_dir);
|
||||||
|
export_dir = path.resolve(cwd, export_dir, basepath);
|
||||||
|
|
||||||
|
// Prep output directory
|
||||||
|
rimraf(export_dir);
|
||||||
|
|
||||||
|
copy(static_files, export_dir);
|
||||||
|
copy(path.join(build_dir, 'client'), path.join(export_dir, 'client'));
|
||||||
|
copy(path.join(build_dir, 'service-worker.js'), path.join(export_dir, 'service-worker.js'));
|
||||||
|
copy(path.join(build_dir, 'service-worker.js.map'), path.join(export_dir, 'service-worker.js.map'));
|
||||||
|
|
||||||
|
const defaultPort = process.env.PORT ? parseInt(process.env.PORT) : 3000;
|
||||||
|
const port = await ports.find(defaultPort);
|
||||||
|
|
||||||
|
const protocol = 'http:';
|
||||||
|
const host = `localhost:${port}`;
|
||||||
|
const origin = `${protocol}//${host}`;
|
||||||
|
|
||||||
|
const root = resolve(origin, basepath);
|
||||||
|
if (!root.href.endsWith('/')) root.href += '/';
|
||||||
|
|
||||||
|
oninfo({
|
||||||
|
message: `Crawling ${root.href}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const proc = child_process.fork(path.resolve(`${build_dir}/server/server.js`), [], {
|
||||||
|
cwd,
|
||||||
|
env: Object.assign({
|
||||||
|
PORT: port,
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
SAPPER_EXPORT: 'true'
|
||||||
|
}, process.env)
|
||||||
|
});
|
||||||
|
|
||||||
|
const seen = new Set();
|
||||||
|
const saved = new Set();
|
||||||
|
|
||||||
|
function save(url: string, status: number, type: string, body: string) {
|
||||||
|
const { pathname } = resolve(origin, url);
|
||||||
|
let file = decodeURIComponent(pathname.slice(1));
|
||||||
|
|
||||||
|
if (saved.has(file)) return;
|
||||||
|
saved.add(file);
|
||||||
|
|
||||||
|
const is_html = type === 'text/html';
|
||||||
|
|
||||||
|
if (is_html) {
|
||||||
|
if (pathname !== '/service-worker-index.html') {
|
||||||
|
file = file === '' ? 'index.html' : `${file}/index.html`;
|
||||||
|
}
|
||||||
|
body = minify_html(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
onfile({
|
||||||
|
file,
|
||||||
|
size: body.length,
|
||||||
|
status
|
||||||
|
});
|
||||||
|
|
||||||
|
const export_file = path.join(export_dir, file);
|
||||||
|
mkdirp(path.dirname(export_file));
|
||||||
|
fs.writeFileSync(export_file, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
proc.on('message', message => {
|
||||||
|
if (!message.__sapper__ || message.event !== 'file') return;
|
||||||
|
save(message.url, message.status, message.type, message.body);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handle(url: URL) {
|
||||||
|
let pathname = url.pathname;
|
||||||
|
if (pathname !== '/service-worker-index.html') {
|
||||||
|
pathname = pathname.replace(root.pathname, '') || '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seen.has(pathname)) return;
|
||||||
|
seen.add(pathname);
|
||||||
|
|
||||||
|
const timeout_deferred = new Deferred();
|
||||||
|
const the_timeout = setTimeout(() => {
|
||||||
|
timeout_deferred.reject(new Error(`Timed out waiting for ${url.href}`));
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
const r = await Promise.race([
|
||||||
|
fetch(url.href, {
|
||||||
|
redirect: 'manual'
|
||||||
|
}),
|
||||||
|
timeout_deferred.promise
|
||||||
|
]);
|
||||||
|
|
||||||
|
clearTimeout(the_timeout); // prevent it hanging at the end
|
||||||
|
|
||||||
|
let type = r.headers.get('Content-Type');
|
||||||
|
|
||||||
|
let body = await r.text();
|
||||||
|
|
||||||
|
const range = ~~(r.status / 100);
|
||||||
|
|
||||||
|
if (range === 2) {
|
||||||
|
if (type === 'text/html') {
|
||||||
|
// parse link rel=preload headers and embed them in the HTML
|
||||||
|
let link = parseLinkHeader(r.headers.get('Link') || '');
|
||||||
|
link.refs.forEach((ref: Ref) => {
|
||||||
|
if (ref.rel === 'preload') {
|
||||||
|
body = body.replace('</head>',
|
||||||
|
`<link rel="preload" as=${JSON.stringify(ref.as)} href=${JSON.stringify(ref.uri)}></head>`)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (pathname !== '/service-worker-index.html') {
|
||||||
|
const cleaned = clean_html(body);
|
||||||
|
|
||||||
|
const q = yootils.queue(8);
|
||||||
|
|
||||||
|
const base_match = /<base ([\s\S]+?)>/m.exec(cleaned);
|
||||||
|
const base_href = base_match && get_href(base_match[1]);
|
||||||
|
const base = resolve(url.href, base_href);
|
||||||
|
|
||||||
|
let match;
|
||||||
|
let pattern = /<a ([\s\S]+?)>/gm;
|
||||||
|
|
||||||
|
while (match = pattern.exec(cleaned)) {
|
||||||
|
const attrs = match[1];
|
||||||
|
const href = get_href(attrs);
|
||||||
|
|
||||||
|
if (href) {
|
||||||
|
const url = resolve(base.href, href);
|
||||||
|
|
||||||
|
if (url.protocol === protocol && url.host === host) {
|
||||||
|
q.add(() => handle(url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await q.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (range === 3) {
|
||||||
|
const location = r.headers.get('Location');
|
||||||
|
|
||||||
|
type = 'text/html';
|
||||||
|
body = `<script>window.location.href = "${location.replace(origin, '')}"</script>`;
|
||||||
|
|
||||||
|
await handle(resolve(root.href, location));
|
||||||
|
}
|
||||||
|
|
||||||
|
save(pathname, r.status, type, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ports.wait(port)
|
||||||
|
.then(() => handle(root))
|
||||||
|
.then(() => handle(resolve(root.href, 'service-worker-index.html')))
|
||||||
|
.then(() => proc.kill())
|
||||||
|
.catch(err => {
|
||||||
|
proc.kill();
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_href(attrs: string) {
|
||||||
|
const match = /href\s*=\s*(?:"(.*?)"|'(.*?)'|([^\s>]*))/.exec(attrs);
|
||||||
|
return match && (match[1] || match[2] || match[3]);
|
||||||
|
}
|
||||||
13
src/api/find_page.ts
Normal file
13
src/api/find_page.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { create_manifest_data } from '../core';
|
||||||
|
|
||||||
|
export function find_page(pathname: string, cwd = 'src/routes') {
|
||||||
|
const { pages } = create_manifest_data(cwd);
|
||||||
|
|
||||||
|
for (let i = 0; i < pages.length; i += 1) {
|
||||||
|
const page = pages[i];
|
||||||
|
|
||||||
|
if (page.pattern.test(pathname)) {
|
||||||
|
return page.parts[page.parts.length - 1].component.file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/api/utils/Deferred.ts
Normal file
12
src/api/utils/Deferred.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export default class Deferred {
|
||||||
|
promise: Promise<any>;
|
||||||
|
fulfil: (value?: any) => void;
|
||||||
|
reject: (error: Error) => void;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.promise = new Promise((fulfil, reject) => {
|
||||||
|
this.fulfil = fulfil;
|
||||||
|
this.reject = reject;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/api/utils/clean_html.ts
Normal file
7
src/api/utils/clean_html.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default function clean_html(html: string) {
|
||||||
|
return html
|
||||||
|
.replace(/<!\[CDATA\[[\s\S]*?\]\]>/gm, '')
|
||||||
|
.replace(/(<script[\s\S]*?>)[\s\S]*?<\/script>/gm, '$1</' + 'script>')
|
||||||
|
.replace(/(<style[\s\S]*?>)[\s\S]*?<\/style>/gm, '$1</' + 'style>')
|
||||||
|
.replace(/<!--[\s\S]*?-->/gm, '');
|
||||||
|
}
|
||||||
21
src/api/utils/copy_runtime.ts
Normal file
21
src/api/utils/copy_runtime.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { mkdirp } from './fs_utils';
|
||||||
|
|
||||||
|
const runtime = [
|
||||||
|
'app.mjs',
|
||||||
|
'server.mjs',
|
||||||
|
'internal/shared.mjs',
|
||||||
|
'internal/layout.svelte',
|
||||||
|
'internal/error.svelte'
|
||||||
|
].map(file => ({
|
||||||
|
file,
|
||||||
|
source: fs.readFileSync(path.join(__dirname, `../runtime/${file}`), 'utf-8')
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function copy_runtime(output: string) {
|
||||||
|
runtime.forEach(({ file, source }) => {
|
||||||
|
mkdirp(path.dirname(`${output}/${file}`));
|
||||||
|
fs.writeFileSync(`${output}/${file}`, source);
|
||||||
|
});
|
||||||
|
}
|
||||||
9
src/api/utils/copy_shimport.ts
Normal file
9
src/api/utils/copy_shimport.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
export function copy_shimport(dest: string) {
|
||||||
|
const shimport_version = require('shimport/package.json').version;
|
||||||
|
fs.writeFileSync(
|
||||||
|
`${dest}/client/shimport@${shimport_version}.js`,
|
||||||
|
fs.readFileSync(require.resolve('shimport/index.js'))
|
||||||
|
);
|
||||||
|
}
|
||||||
46
src/api/utils/fs_utils.ts
Normal file
46
src/api/utils/fs_utils.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export function mkdirp(dir: string) {
|
||||||
|
const parent = path.dirname(dir);
|
||||||
|
if (parent === dir) return;
|
||||||
|
|
||||||
|
mkdirp(parent);
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(dir);
|
||||||
|
} catch (err) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rimraf(thing: string) {
|
||||||
|
if (!fs.existsSync(thing)) return;
|
||||||
|
|
||||||
|
const stats = fs.statSync(thing);
|
||||||
|
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
fs.readdirSync(thing).forEach(file => {
|
||||||
|
rimraf(path.join(thing, file));
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.rmdirSync(thing);
|
||||||
|
} else {
|
||||||
|
fs.unlinkSync(thing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function copy(from: string, to: string) {
|
||||||
|
if (!fs.existsSync(from)) return;
|
||||||
|
|
||||||
|
const stats = fs.statSync(from);
|
||||||
|
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
fs.readdirSync(from).forEach(file => {
|
||||||
|
copy(path.join(from, file), path.join(to, file));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
mkdirp(path.dirname(to));
|
||||||
|
fs.writeFileSync(to, fs.readFileSync(from));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import { minify } from 'html-minifier';
|
import { minify } from 'html-minifier';
|
||||||
|
|
||||||
export function minify_html(html: string) {
|
export default function minify_html(html: string) {
|
||||||
return minify(html, {
|
return minify(html, {
|
||||||
collapseBooleanAttributes: true,
|
collapseBooleanAttributes: true,
|
||||||
collapseWhitespace: true,
|
collapseWhitespace: true,
|
||||||
conservativeCollapse: true,
|
conservativeCollapse: true,
|
||||||
decodeEntities: true,
|
decodeEntities: true,
|
||||||
html5: true,
|
html5: true,
|
||||||
|
ignoreCustomComments: [/^#/],
|
||||||
minifyCSS: true,
|
minifyCSS: true,
|
||||||
minifyJS: true,
|
minifyJS: false,
|
||||||
removeAttributeQuotes: true,
|
removeAttributeQuotes: true,
|
||||||
removeComments: true,
|
removeComments: true,
|
||||||
removeOptionalTags: true,
|
removeOptionalTags: true,
|
||||||
1
src/api/utils/noop.ts
Normal file
1
src/api/utils/noop.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export function noop() {}
|
||||||
38
src/api/utils/validate_bundler.ts
Normal file
38
src/api/utils/validate_bundler.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
export default function validate_bundler(bundler?: 'rollup' | 'webpack') {
|
||||||
|
if (!bundler) {
|
||||||
|
bundler = (
|
||||||
|
fs.existsSync('rollup.config.js') ? 'rollup' :
|
||||||
|
fs.existsSync('webpack.config.js') ? 'webpack' :
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!bundler) {
|
||||||
|
// TODO remove in a future version
|
||||||
|
deprecate_dir('rollup');
|
||||||
|
deprecate_dir('webpack');
|
||||||
|
|
||||||
|
throw new Error(`Could not find rollup.config.js or webpack.config.js`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bundler !== 'rollup' && bundler !== 'webpack') {
|
||||||
|
throw new Error(`'${bundler}' is not a valid option for --bundler — must be either 'rollup' or 'webpack'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bundler;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deprecate_dir(bundler: 'rollup' | 'webpack') {
|
||||||
|
try {
|
||||||
|
const stats = fs.statSync(bundler);
|
||||||
|
if (!stats.isDirectory()) return;
|
||||||
|
} catch (err) {
|
||||||
|
// do nothing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO link to docs, once those docs exist
|
||||||
|
throw new Error(`As of Sapper 0.21, build configuration should be placed in a single ${bundler}.config.js file`);
|
||||||
|
}
|
||||||
312
src/cli.ts
312
src/cli.ts
@@ -1,79 +1,295 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as child_process from 'child_process';
|
|
||||||
import sade from 'sade';
|
import sade from 'sade';
|
||||||
import * as clorox from 'clorox';
|
import colors from 'kleur';
|
||||||
import prettyMs from 'pretty-ms';
|
|
||||||
// import upgrade from './cli/upgrade';
|
|
||||||
import * as ports from 'port-authority';
|
|
||||||
import * as pkg from '../package.json';
|
import * as pkg from '../package.json';
|
||||||
|
import { elapsed, repeat, left_pad, format_milliseconds } from './utils';
|
||||||
|
import { InvalidEvent, ErrorEvent, FatalEvent, BuildEvent, ReadyEvent } from './interfaces';
|
||||||
|
|
||||||
const prog = sade('sapper').version(pkg.version);
|
const prog = sade('sapper').version(pkg.version);
|
||||||
|
|
||||||
|
if (process.argv[2] === 'start') {
|
||||||
|
// remove this in a future version
|
||||||
|
console.error(colors.bold().red(`'sapper start' has been removed`));
|
||||||
|
console.error(`Use 'node [build_dir]' instead`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
prog.command('dev')
|
prog.command('dev')
|
||||||
.describe('Start a development server')
|
.describe('Start a development server')
|
||||||
.option('-p, --port', 'Specify a port')
|
.option('-p, --port', 'Specify a port')
|
||||||
.option('-o, --open', 'Open a browser window')
|
.option('-o, --open', 'Open a browser window')
|
||||||
.action(async (opts: { port: number, open: boolean }) => {
|
.option('--dev-port', 'Specify a port for development server')
|
||||||
const { dev } = await import('./cli/dev');
|
.option('--hot', 'Use hot module replacement (requires webpack)', true)
|
||||||
dev(opts);
|
.option('--live', 'Reload on changes if not using --hot', true)
|
||||||
|
.option('--bundler', 'Specify a bundler (rollup or webpack)')
|
||||||
|
.option('--cwd', 'Current working directory', '.')
|
||||||
|
.option('--src', 'Source directory', 'src')
|
||||||
|
.option('--routes', 'Routes directory', 'src/routes')
|
||||||
|
.option('--static', 'Static files directory', 'static')
|
||||||
|
.option('--output', 'Sapper output directory', 'src/node_modules/@sapper')
|
||||||
|
.option('--build-dir', 'Development build directory', '__sapper__/dev')
|
||||||
|
.action(async (opts: {
|
||||||
|
port: number,
|
||||||
|
open: boolean,
|
||||||
|
'dev-port': number,
|
||||||
|
live: boolean,
|
||||||
|
hot: boolean,
|
||||||
|
bundler?: 'rollup' | 'webpack',
|
||||||
|
cwd: string,
|
||||||
|
src: string,
|
||||||
|
routes: string,
|
||||||
|
static: string,
|
||||||
|
output: string,
|
||||||
|
'build-dir': string
|
||||||
|
}) => {
|
||||||
|
const { dev } = await import('./api/dev');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const watcher = dev({
|
||||||
|
cwd: opts.cwd,
|
||||||
|
src: opts.src,
|
||||||
|
routes: opts.routes,
|
||||||
|
static: opts.static,
|
||||||
|
output: opts.output,
|
||||||
|
dest: opts['build-dir'],
|
||||||
|
port: opts.port,
|
||||||
|
'dev-port': opts['dev-port'],
|
||||||
|
live: opts.live,
|
||||||
|
hot: opts.hot,
|
||||||
|
bundler: opts.bundler
|
||||||
|
});
|
||||||
|
|
||||||
|
let first = true;
|
||||||
|
|
||||||
|
watcher.on('stdout', data => {
|
||||||
|
process.stdout.write(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.on('stderr', data => {
|
||||||
|
process.stderr.write(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.on('ready', async (event: ReadyEvent) => {
|
||||||
|
if (first) {
|
||||||
|
console.log(colors.bold().cyan(`> Listening on http://localhost:${event.port}`));
|
||||||
|
if (opts.open) {
|
||||||
|
const { exec } = await import('child_process');
|
||||||
|
exec(`open http://localhost:${event.port}`);
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.on('invalid', (event: InvalidEvent) => {
|
||||||
|
const changed = event.changed.map(filename => path.relative(process.cwd(), filename)).join(', ');
|
||||||
|
console.log(`\n${colors.bold().cyan(changed)} changed. rebuilding...`);
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.on('error', (event: ErrorEvent) => {
|
||||||
|
const { type, error } = event;
|
||||||
|
|
||||||
|
console.log(colors.bold().red(`✗ ${type}`));
|
||||||
|
|
||||||
|
if (error.loc && error.loc.file) {
|
||||||
|
console.log(colors.bold(`${path.relative(process.cwd(), error.loc.file)} (${error.loc.line}:${error.loc.column})`));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(colors.red(event.error.message));
|
||||||
|
if (error.frame) console.log(error.frame);
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.on('fatal', (event: FatalEvent) => {
|
||||||
|
console.log(colors.bold().red(`> ${event.message}`));
|
||||||
|
if (event.log) console.log(event.log);
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.on('build', (event: BuildEvent) => {
|
||||||
|
if (event.errors.length) {
|
||||||
|
console.log(colors.bold().red(`✗ ${event.type}`));
|
||||||
|
|
||||||
|
event.errors.filter(e => !e.duplicate).forEach(error => {
|
||||||
|
if (error.file) console.log(colors.bold(error.file));
|
||||||
|
console.log(error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
const hidden = event.errors.filter(e => e.duplicate).length;
|
||||||
|
if (hidden > 0) {
|
||||||
|
console.log(`${hidden} duplicate ${hidden === 1 ? 'error' : 'errors'} hidden\n`);
|
||||||
|
}
|
||||||
|
} else if (event.warnings.length) {
|
||||||
|
console.log(colors.bold().yellow(`• ${event.type}`));
|
||||||
|
|
||||||
|
event.warnings.filter(e => !e.duplicate).forEach(warning => {
|
||||||
|
if (warning.file) console.log(colors.bold(warning.file));
|
||||||
|
console.log(warning.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
const hidden = event.warnings.filter(e => e.duplicate).length;
|
||||||
|
if (hidden > 0) {
|
||||||
|
console.log(`${hidden} duplicate ${hidden === 1 ? 'warning' : 'warnings'} hidden\n`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`${colors.bold().green(`✔ ${event.type}`)} ${colors.gray(`(${format_milliseconds(event.duration)})`)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(colors.bold().red(`> ${err.message}`));
|
||||||
|
console.log(colors.gray(err.stack));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
prog.command('build [dest]')
|
prog.command('build [dest]')
|
||||||
.describe('Create a production-ready version of your app')
|
.describe('Create a production-ready version of your app')
|
||||||
.action(async (dest = 'build') => {
|
.option('-p, --port', 'Default of process.env.PORT', '3000')
|
||||||
|
.option('--bundler', 'Specify a bundler (rollup or webpack, blank for auto)')
|
||||||
|
.option('--legacy', 'Create separate legacy build')
|
||||||
|
.option('--cwd', 'Current working directory', '.')
|
||||||
|
.option('--src', 'Source directory', 'src')
|
||||||
|
.option('--routes', 'Routes directory', 'src/routes')
|
||||||
|
.option('--output', 'Sapper output directory', 'src/node_modules/@sapper')
|
||||||
|
.example(`build custom-dir -p 4567`)
|
||||||
|
.action(async (dest = '__sapper__/build', opts: {
|
||||||
|
port: string,
|
||||||
|
legacy: boolean,
|
||||||
|
bundler?: 'rollup' | 'webpack',
|
||||||
|
cwd: string,
|
||||||
|
src: string,
|
||||||
|
routes: string,
|
||||||
|
output: string
|
||||||
|
}) => {
|
||||||
console.log(`> Building...`);
|
console.log(`> Building...`);
|
||||||
|
|
||||||
process.env.NODE_ENV = 'production';
|
|
||||||
process.env.SAPPER_DEST = dest;
|
|
||||||
|
|
||||||
const start = Date.now();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { build } = await import('./cli/build');
|
await _build(opts.bundler, opts.legacy, opts.cwd, opts.src, opts.routes, opts.output, dest);
|
||||||
await build();
|
|
||||||
console.error(`\n> Finished in ${elapsed(start)}. Type ${clorox.bold.cyan(dest === 'build' ? 'npx sapper start' : `npx sapper start ${dest}`)} to run the app.`);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
prog.command('start [dir]')
|
const launcher = path.resolve(dest, 'index.js');
|
||||||
.describe('Start your app')
|
|
||||||
.option('-p, --port', 'Specify a port')
|
fs.writeFileSync(launcher, `
|
||||||
.option('-o, --open', 'Open a browser window')
|
// generated by sapper build at ${new Date().toISOString()}
|
||||||
.action(async (dir = 'build', opts: { port: number, open: boolean }) => {
|
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
|
||||||
const { start } = await import('./cli/start');
|
process.env.PORT = process.env.PORT || ${opts.port || 3000};
|
||||||
start(dir, opts);
|
|
||||||
|
console.log('Starting server on port ' + process.env.PORT);
|
||||||
|
require('./server/server.js');
|
||||||
|
`.replace(/^\t+/gm, '').trim());
|
||||||
|
|
||||||
|
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold().cyan(`node ${dest}`)} to run the app.`);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`${colors.bold().red(`> ${err.message}`)}`);
|
||||||
|
console.log(colors.gray(err.stack));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
prog.command('export [dest]')
|
prog.command('export [dest]')
|
||||||
.describe('Export your app as static files (if possible)')
|
.describe('Export your app as static files (if possible)')
|
||||||
.action(async (dest = 'export') => {
|
.option('--build', '(Re)build app before exporting', true)
|
||||||
console.log(`> Building...`);
|
.option('--basepath', 'Specify a base path')
|
||||||
|
.option('--timeout', 'Milliseconds to wait for a page (--no-timeout to disable)', 5000)
|
||||||
process.env.NODE_ENV = 'production';
|
.option('--legacy', 'Create separate legacy build')
|
||||||
process.env.SAPPER_DEST = '.sapper/.export';
|
.option('--bundler', 'Specify a bundler (rollup or webpack, blank for auto)')
|
||||||
|
.option('--cwd', 'Current working directory', '.')
|
||||||
const start = Date.now();
|
.option('--src', 'Source directory', 'src')
|
||||||
|
.option('--routes', 'Routes directory', 'src/routes')
|
||||||
|
.option('--static', 'Static files directory', 'static')
|
||||||
|
.option('--output', 'Sapper output directory', 'src/node_modules/@sapper')
|
||||||
|
.option('--build-dir', 'Intermediate build directory', '__sapper__/build')
|
||||||
|
.action(async (dest = '__sapper__/export', opts: {
|
||||||
|
build: boolean,
|
||||||
|
legacy: boolean,
|
||||||
|
bundler?: 'rollup' | 'webpack',
|
||||||
|
basepath?: string,
|
||||||
|
timeout: number | false,
|
||||||
|
cwd: string,
|
||||||
|
src: string,
|
||||||
|
routes: string,
|
||||||
|
static: string,
|
||||||
|
output: string,
|
||||||
|
'build-dir': string,
|
||||||
|
}) => {
|
||||||
try {
|
try {
|
||||||
const { build } = await import('./cli/build');
|
if (opts.build) {
|
||||||
await build();
|
console.log(`> Building...`);
|
||||||
console.error(`\n> Built in ${elapsed(start)}. Crawling site...`);
|
await _build(opts.bundler, opts.legacy, opts.cwd, opts.src, opts.routes, opts.output, opts['build-dir']);
|
||||||
|
console.error(`\n> Built in ${elapsed(start)}`);
|
||||||
|
}
|
||||||
|
|
||||||
const { exporter } = await import('./cli/export');
|
const { export: _export } = await import('./api/export');
|
||||||
await exporter(dest);
|
const { default: pb } = await import('pretty-bytes');
|
||||||
console.error(`\n> Finished in ${elapsed(start)}. Type ${clorox.bold.cyan(`npx serve ${dest}`)} to run the app.`);
|
|
||||||
|
await _export({
|
||||||
|
cwd: opts.cwd,
|
||||||
|
static: opts.static,
|
||||||
|
build_dir: opts['build-dir'],
|
||||||
|
export_dir: dest,
|
||||||
|
basepath: opts.basepath,
|
||||||
|
timeout: opts.timeout,
|
||||||
|
|
||||||
|
oninfo: event => {
|
||||||
|
console.log(colors.bold().cyan(`> ${event.message}`));
|
||||||
|
},
|
||||||
|
|
||||||
|
onfile: event => {
|
||||||
|
const size_color = event.size > 150000 ? colors.bold().red : event.size > 50000 ? colors.bold().yellow : colors.bold().gray;
|
||||||
|
const size_label = size_color(left_pad(pb(event.size), 10));
|
||||||
|
|
||||||
|
const file_label = event.status === 200
|
||||||
|
? event.file
|
||||||
|
: colors.bold()[event.status >= 400 ? 'red' : 'yellow'](`(${event.status}) ${event.file}`);
|
||||||
|
|
||||||
|
console.log(`${size_label} ${file_label}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold().cyan(`npx serve ${dest}`)} to run the app.`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
console.error(colors.bold().red(`> ${err.message}`));
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO upgrade
|
|
||||||
|
|
||||||
prog.parse(process.argv);
|
prog.parse(process.argv);
|
||||||
|
|
||||||
function elapsed(start: number) {
|
|
||||||
return prettyMs(Date.now() - start);
|
async function _build(
|
||||||
}
|
bundler: 'rollup' | 'webpack',
|
||||||
|
legacy: boolean,
|
||||||
|
cwd: string,
|
||||||
|
src: string,
|
||||||
|
routes: string,
|
||||||
|
output: string,
|
||||||
|
dest: string
|
||||||
|
) {
|
||||||
|
const { build } = await import('./api/build');
|
||||||
|
|
||||||
|
await build({
|
||||||
|
bundler,
|
||||||
|
legacy,
|
||||||
|
cwd,
|
||||||
|
src,
|
||||||
|
routes,
|
||||||
|
dest,
|
||||||
|
|
||||||
|
oncompile: event => {
|
||||||
|
let banner = `built ${event.type}`;
|
||||||
|
let c = (txt: string) => colors.cyan(txt);
|
||||||
|
|
||||||
|
const { warnings } = event.result;
|
||||||
|
if (warnings.length > 0) {
|
||||||
|
banner += ` with ${warnings.length} ${warnings.length === 1 ? 'warning' : 'warnings'}`;
|
||||||
|
c = (txt: string) => colors.cyan(txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
console.log(c(`┌─${repeat('─', banner.length)}─┐`));
|
||||||
|
console.log(c(`│ ${colors.bold(banner) } │`));
|
||||||
|
console.log(c(`└─${repeat('─', banner.length)}─┘`));
|
||||||
|
|
||||||
|
console.log(event.result.print());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as clorox from 'clorox';
|
|
||||||
import mkdirp from 'mkdirp';
|
|
||||||
import rimraf from 'rimraf';
|
|
||||||
import { minify_html } from './utils/minify_html';
|
|
||||||
import { create_compilers, create_main_manifests, create_routes, create_serviceworker_manifest } from '../core'
|
|
||||||
import { locations } from '../config';
|
|
||||||
|
|
||||||
export async function build() {
|
|
||||||
const output = locations.dest();
|
|
||||||
|
|
||||||
mkdirp.sync(output);
|
|
||||||
rimraf.sync(path.join(output, '**/*'));
|
|
||||||
|
|
||||||
const routes = create_routes();
|
|
||||||
|
|
||||||
// create app/manifest/client.js and app/manifest/server.js
|
|
||||||
create_main_manifests({ routes });
|
|
||||||
|
|
||||||
const { client, server, serviceworker } = create_compilers();
|
|
||||||
|
|
||||||
const client_stats = await compile(client);
|
|
||||||
console.log(`${clorox.inverse(`\nbuilt client`)}`);
|
|
||||||
console.log(client_stats.toString({ colors: true }));
|
|
||||||
fs.writeFileSync(path.join(output, 'client_info.json'), JSON.stringify(client_stats.toJson()));
|
|
||||||
|
|
||||||
const server_stats = await compile(server);
|
|
||||||
console.log(`${clorox.inverse(`\nbuilt server`)}`);
|
|
||||||
console.log(server_stats.toString({ colors: true }));
|
|
||||||
|
|
||||||
let serviceworker_stats;
|
|
||||||
|
|
||||||
if (serviceworker) {
|
|
||||||
create_serviceworker_manifest({
|
|
||||||
routes,
|
|
||||||
client_files: client_stats.toJson().assets.map((chunk: { name: string }) => `/client/${chunk.name}`)
|
|
||||||
});
|
|
||||||
|
|
||||||
serviceworker_stats = await compile(serviceworker);
|
|
||||||
console.log(`${clorox.inverse(`\nbuilt service worker`)}`);
|
|
||||||
console.log(serviceworker_stats.toString({ colors: true }));
|
|
||||||
}
|
|
||||||
|
|
||||||
// minify app/template.html
|
|
||||||
// TODO compile this to a function? could be quicker than str.replace(...).replace(...).replace(...)
|
|
||||||
const template = fs.readFileSync(`${locations.app()}/template.html`, 'utf-8');
|
|
||||||
fs.writeFileSync(`${output}/template.html`, minify_html(template));
|
|
||||||
}
|
|
||||||
|
|
||||||
function compile(compiler: any) {
|
|
||||||
return new Promise((fulfil, reject) => {
|
|
||||||
compiler.run((err: Error, stats: any) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stats.hasErrors()) {
|
|
||||||
console.error(stats.toString({ colors: true }));
|
|
||||||
reject(new Error(`Encountered errors while building app`));
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
fulfil(stats);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
313
src/cli/dev.ts
313
src/cli/dev.ts
@@ -1,313 +0,0 @@
|
|||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as net from 'net';
|
|
||||||
import * as clorox from 'clorox';
|
|
||||||
import * as child_process from 'child_process';
|
|
||||||
import * as http from 'http';
|
|
||||||
import mkdirp from 'mkdirp';
|
|
||||||
import rimraf from 'rimraf';
|
|
||||||
import format_messages from 'webpack-format-messages';
|
|
||||||
import prettyMs from 'pretty-ms';
|
|
||||||
import * as ports from 'port-authority';
|
|
||||||
import { locations } from '../config';
|
|
||||||
import { create_compilers, create_main_manifests, create_routes, create_serviceworker_manifest } from '../core';
|
|
||||||
|
|
||||||
type Deferred = {
|
|
||||||
promise?: Promise<any>;
|
|
||||||
fulfil?: (value?: any) => void;
|
|
||||||
reject?: (err: Error) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function deferred() {
|
|
||||||
const d: Deferred = {};
|
|
||||||
|
|
||||||
d.promise = new Promise((fulfil, reject) => {
|
|
||||||
d.fulfil = fulfil;
|
|
||||||
d.reject = reject;
|
|
||||||
});
|
|
||||||
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
|
|
||||||
function create_hot_update_server(port: number, interval = 10000) {
|
|
||||||
const clients = new Set();
|
|
||||||
|
|
||||||
const server = http.createServer((req, res) => {
|
|
||||||
if (req.url !== '/__sapper__') return;
|
|
||||||
|
|
||||||
req.socket.setKeepAlive(true);
|
|
||||||
res.writeHead(200, {
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
'Access-Control-Allow-Headers': 'Cache-Control',
|
|
||||||
'Content-Type': 'text/event-stream;charset=utf-8',
|
|
||||||
'Cache-Control': 'no-cache, no-transform',
|
|
||||||
'Connection': 'keep-alive',
|
|
||||||
// While behind nginx, event stream should not be buffered:
|
|
||||||
// http://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffering
|
|
||||||
'X-Accel-Buffering': 'no'
|
|
||||||
});
|
|
||||||
|
|
||||||
res.write('\n');
|
|
||||||
|
|
||||||
clients.add(res);
|
|
||||||
req.on('close', () => {
|
|
||||||
clients.delete(res);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen(port);
|
|
||||||
|
|
||||||
function send(data: any) {
|
|
||||||
clients.forEach(client => {
|
|
||||||
client.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
send(null)
|
|
||||||
}, interval);
|
|
||||||
|
|
||||||
return { send };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function dev(opts: { port: number, open: boolean }) {
|
|
||||||
process.env.NODE_ENV = 'development';
|
|
||||||
|
|
||||||
let port = opts.port || +process.env.PORT;
|
|
||||||
|
|
||||||
if (port) {
|
|
||||||
if (!await ports.check(port)) {
|
|
||||||
console.log(`${clorox.bold.red(`> Port ${port} is unavailable`)}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
port = await ports.find(3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dir = locations.dest();
|
|
||||||
rimraf.sync(dir);
|
|
||||||
mkdirp.sync(dir);
|
|
||||||
|
|
||||||
const dev_port = await ports.find(10000);
|
|
||||||
|
|
||||||
const routes = create_routes();
|
|
||||||
create_main_manifests({ routes, dev_port });
|
|
||||||
|
|
||||||
const hot_update_server = create_hot_update_server(dev_port);
|
|
||||||
|
|
||||||
watch_files(`${locations.routes()}/**/*`, ['add', 'unlink'], () => {
|
|
||||||
const routes = create_routes();
|
|
||||||
create_main_manifests({ routes, dev_port });
|
|
||||||
});
|
|
||||||
|
|
||||||
watch_files(`${locations.app()}/template.html`, ['change'], () => {
|
|
||||||
hot_update_server.send({
|
|
||||||
action: 'reload'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let proc: child_process.ChildProcess;
|
|
||||||
|
|
||||||
process.on('exit', () => {
|
|
||||||
// sometimes webpack crashes, so we need to kill our children
|
|
||||||
if (proc) proc.kill();
|
|
||||||
});
|
|
||||||
|
|
||||||
const deferreds = {
|
|
||||||
server: deferred(),
|
|
||||||
client: deferred()
|
|
||||||
};
|
|
||||||
|
|
||||||
let restarting = false;
|
|
||||||
let build = {
|
|
||||||
unique_warnings: new Set(),
|
|
||||||
unique_errors: new Set()
|
|
||||||
};
|
|
||||||
|
|
||||||
function restart_build(filename: string) {
|
|
||||||
if (restarting) return;
|
|
||||||
|
|
||||||
restarting = true;
|
|
||||||
build = {
|
|
||||||
unique_warnings: new Set(),
|
|
||||||
unique_errors: new Set()
|
|
||||||
};
|
|
||||||
|
|
||||||
process.nextTick(() => {
|
|
||||||
restarting = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`\n${clorox.bold.cyan(path.relative(process.cwd(), filename))} changed. rebuilding...`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO watch the configs themselves?
|
|
||||||
const compilers = create_compilers();
|
|
||||||
|
|
||||||
function watch(compiler: any, { name, invalid = noop, error = noop, result }: {
|
|
||||||
name: string,
|
|
||||||
invalid?: (filename: string) => void;
|
|
||||||
error?: (error: Error) => void;
|
|
||||||
result: (stats: any) => void;
|
|
||||||
}) {
|
|
||||||
compiler.hooks.invalid.tap('sapper', (filename: string) => {
|
|
||||||
invalid(filename);
|
|
||||||
});
|
|
||||||
|
|
||||||
compiler.watch({}, (err: Error, stats: any) => {
|
|
||||||
if (err) {
|
|
||||||
console.log(`${clorox.red(`✗ ${name}`)}`);
|
|
||||||
console.log(`${clorox.red(err.message)}`);
|
|
||||||
error(err);
|
|
||||||
} else {
|
|
||||||
const messages = format_messages(stats);
|
|
||||||
const info = stats.toJson();
|
|
||||||
|
|
||||||
if (messages.errors.length > 0) {
|
|
||||||
console.log(`${clorox.bold.red(`✗ ${name}`)}`);
|
|
||||||
|
|
||||||
const filtered = messages.errors.filter((message: string) => {
|
|
||||||
return !build.unique_errors.has(message);
|
|
||||||
});
|
|
||||||
|
|
||||||
filtered.forEach((message: string) => {
|
|
||||||
build.unique_errors.add(message);
|
|
||||||
console.log(message);
|
|
||||||
});
|
|
||||||
|
|
||||||
const hidden = messages.errors.length - filtered.length;
|
|
||||||
if (hidden > 0) {
|
|
||||||
console.log(`${hidden} duplicate ${hidden === 1 ? 'error' : 'errors'} hidden\n`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (messages.warnings.length > 0) {
|
|
||||||
console.log(`${clorox.bold.yellow(`• ${name}`)}`);
|
|
||||||
|
|
||||||
const filtered = messages.warnings.filter((message: string) => {
|
|
||||||
return !build.unique_warnings.has(message);
|
|
||||||
});
|
|
||||||
|
|
||||||
filtered.forEach((message: string) => {
|
|
||||||
build.unique_warnings.add(message);
|
|
||||||
console.log(`${message}\n`);
|
|
||||||
});
|
|
||||||
|
|
||||||
const hidden = messages.warnings.length - filtered.length;
|
|
||||||
if (hidden > 0) {
|
|
||||||
console.log(`${hidden} duplicate ${hidden === 1 ? 'warning' : 'warnings'} hidden\n`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`${clorox.bold.green(`✔ ${name}`)} ${clorox.gray(`(${prettyMs(info.time)})`)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
result(info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(compilers.server, {
|
|
||||||
name: 'server',
|
|
||||||
|
|
||||||
invalid: filename => {
|
|
||||||
restart_build(filename);
|
|
||||||
// TODO print message
|
|
||||||
deferreds.server = deferred();
|
|
||||||
},
|
|
||||||
|
|
||||||
result: info => {
|
|
||||||
// TODO log compile errors/warnings
|
|
||||||
|
|
||||||
fs.writeFileSync(path.join(dir, 'server_info.json'), JSON.stringify(info, null, ' '));
|
|
||||||
|
|
||||||
deferreds.client.promise.then(() => {
|
|
||||||
function restart() {
|
|
||||||
ports.wait(port).then(deferreds.server.fulfil);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (proc) {
|
|
||||||
proc.kill();
|
|
||||||
proc.on('exit', restart);
|
|
||||||
} else {
|
|
||||||
restart();
|
|
||||||
}
|
|
||||||
|
|
||||||
proc = child_process.fork(`${dir}/server.js`, [], {
|
|
||||||
cwd: process.cwd(),
|
|
||||||
env: Object.assign({
|
|
||||||
PORT: port
|
|
||||||
}, process.env)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let first = true;
|
|
||||||
|
|
||||||
watch(compilers.client, {
|
|
||||||
name: 'client',
|
|
||||||
|
|
||||||
invalid: filename => {
|
|
||||||
restart_build(filename);
|
|
||||||
deferreds.client = deferred();
|
|
||||||
|
|
||||||
// TODO we should delete old assets. due to a webpack bug
|
|
||||||
// i don't even begin to comprehend, this is apparently
|
|
||||||
// quite difficult
|
|
||||||
},
|
|
||||||
|
|
||||||
result: info => {
|
|
||||||
fs.writeFileSync(path.join(dir, 'client_info.json'), JSON.stringify(info, null, ' '));
|
|
||||||
deferreds.client.fulfil();
|
|
||||||
|
|
||||||
const client_files = info.assets.map((chunk: { name: string }) => `/client/${chunk.name}`);
|
|
||||||
|
|
||||||
deferreds.server.promise.then(() => {
|
|
||||||
hot_update_server.send({
|
|
||||||
status: 'completed'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (first) {
|
|
||||||
first = false;
|
|
||||||
console.log(`${clorox.bold.cyan(`> Listening on localhost:${port}`)}`);
|
|
||||||
if (opts.open) child_process.exec(`open http://localhost:${port}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
create_serviceworker_manifest({
|
|
||||||
routes: create_routes(),
|
|
||||||
client_files
|
|
||||||
});
|
|
||||||
|
|
||||||
watch_serviceworker();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let watch_serviceworker = compilers.serviceworker
|
|
||||||
? function() {
|
|
||||||
watch_serviceworker = noop;
|
|
||||||
|
|
||||||
watch(compilers.serviceworker, {
|
|
||||||
name: 'service worker',
|
|
||||||
|
|
||||||
result: info => {
|
|
||||||
fs.writeFileSync(path.join(dir, 'serviceworker_info.json'), JSON.stringify(info, null, ' '));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
: noop;
|
|
||||||
}
|
|
||||||
|
|
||||||
function noop() {}
|
|
||||||
|
|
||||||
function watch_files(pattern: string, events: string[], callback: () => void) {
|
|
||||||
const chokidar = require('chokidar');
|
|
||||||
|
|
||||||
const watcher = chokidar.watch(pattern, {
|
|
||||||
persistent: true,
|
|
||||||
ignoreInitial: true
|
|
||||||
});
|
|
||||||
|
|
||||||
events.forEach(event => {
|
|
||||||
watcher.on(event, callback);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import * as child_process from 'child_process';
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as sander from 'sander';
|
|
||||||
import * as clorox from 'clorox';
|
|
||||||
import cheerio from 'cheerio';
|
|
||||||
import URL from 'url-parse';
|
|
||||||
import fetch from 'node-fetch';
|
|
||||||
import * as ports from 'port-authority';
|
|
||||||
import prettyBytes from 'pretty-bytes';
|
|
||||||
import { minify_html } from './utils/minify_html';
|
|
||||||
import { locations } from '../config';
|
|
||||||
|
|
||||||
export async function exporter(export_dir: string) {
|
|
||||||
const build_dir = locations.dest();
|
|
||||||
|
|
||||||
// Prep output directory
|
|
||||||
sander.rimrafSync(export_dir);
|
|
||||||
|
|
||||||
sander.copydirSync('assets').to(export_dir);
|
|
||||||
sander.copydirSync(build_dir, 'client').to(export_dir, 'client');
|
|
||||||
|
|
||||||
if (sander.existsSync(build_dir, 'service-worker.js')) {
|
|
||||||
sander.copyFileSync(build_dir, 'service-worker.js').to(export_dir, 'service-worker.js');
|
|
||||||
}
|
|
||||||
|
|
||||||
const port = await ports.find(3000);
|
|
||||||
|
|
||||||
const origin = `http://localhost:${port}`;
|
|
||||||
|
|
||||||
const proc = child_process.fork(path.resolve(`${build_dir}/server.js`), [], {
|
|
||||||
cwd: process.cwd(),
|
|
||||||
env: {
|
|
||||||
PORT: port,
|
|
||||||
NODE_ENV: 'production',
|
|
||||||
SAPPER_DEST: build_dir,
|
|
||||||
SAPPER_EXPORT: 'true'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const seen = new Set();
|
|
||||||
const saved = new Set();
|
|
||||||
|
|
||||||
proc.on('message', message => {
|
|
||||||
if (!message.__sapper__) return;
|
|
||||||
|
|
||||||
let file = new URL(message.url, origin).pathname.slice(1);
|
|
||||||
let { body } = message;
|
|
||||||
|
|
||||||
if (saved.has(file)) return;
|
|
||||||
saved.add(file);
|
|
||||||
|
|
||||||
const is_html = message.type === 'text/html';
|
|
||||||
|
|
||||||
if (is_html) {
|
|
||||||
file = file === '' ? 'index.html' : `${file}/index.html`;
|
|
||||||
body = minify_html(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`${clorox.bold.cyan(file)} ${clorox.gray(`(${prettyBytes(body.length)})`)}`);
|
|
||||||
|
|
||||||
sander.writeFileSync(`${export_dir}/${file}`, body);
|
|
||||||
});
|
|
||||||
|
|
||||||
function handle(url: URL) {
|
|
||||||
if (url.origin !== origin) return;
|
|
||||||
|
|
||||||
if (seen.has(url.pathname)) return;
|
|
||||||
seen.add(url.pathname);
|
|
||||||
|
|
||||||
return fetch(url.href)
|
|
||||||
.then(r => {
|
|
||||||
if (r.headers.get('Content-Type') === 'text/html') {
|
|
||||||
return r.text().then((body: string) => {
|
|
||||||
const $ = cheerio.load(body);
|
|
||||||
const hrefs: string[] = [];
|
|
||||||
|
|
||||||
$('a[href]').each((i: number, $a) => {
|
|
||||||
hrefs.push($a.attribs.href);
|
|
||||||
});
|
|
||||||
|
|
||||||
return hrefs.reduce((promise, href) => {
|
|
||||||
return promise.then(() => handle(new URL(href, url.href)));
|
|
||||||
}, Promise.resolve());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err: Error) => {
|
|
||||||
console.log(`${clorox.red(`> Error rendering ${url.pathname}: ${err.message}`)}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return ports.wait(port)
|
|
||||||
.then(() => handle(new URL(origin))) // TODO all static routes
|
|
||||||
.then(() => proc.kill());
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as child_process from 'child_process';
|
|
||||||
import * as clorox from 'clorox';
|
|
||||||
import * as ports from 'port-authority';
|
|
||||||
|
|
||||||
export async function start(dir: string, opts: { port: number, open: boolean }) {
|
|
||||||
let port = opts.port || +process.env.PORT;
|
|
||||||
|
|
||||||
const resolved = path.resolve(dir);
|
|
||||||
const server = path.resolve(dir, 'server.js');
|
|
||||||
|
|
||||||
if (!fs.existsSync(server)) {
|
|
||||||
console.log(clorox.bold.red(`> ${dir}/server.js does not exist — type ${clorox.bold.cyan(dir === 'build' ? `npx sapper build` : `npx sapper build ${dir}`)} to create it`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (port) {
|
|
||||||
if (!await ports.check(port)) {
|
|
||||||
console.log(clorox.bold.red(`> Port ${port} is unavailable`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
port = await ports.find(3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
child_process.fork(server, [], {
|
|
||||||
cwd: process.cwd(),
|
|
||||||
env: Object.assign({
|
|
||||||
NODE_ENV: 'production',
|
|
||||||
PORT: port,
|
|
||||||
SAPPER_DEST: dir
|
|
||||||
}, process.env)
|
|
||||||
});
|
|
||||||
|
|
||||||
await ports.wait(port);
|
|
||||||
console.log(`${clorox.bold.cyan(`> Listening on localhost:${port}`)}`);
|
|
||||||
if (opts.open) child_process.exec(`open http://localhost:${port}`);
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import * as fs from 'fs';
|
|
||||||
import * as clorox from 'clorox';
|
|
||||||
|
|
||||||
export default async function upgrade() {
|
|
||||||
const upgraded = [
|
|
||||||
await upgrade_sapper_main()
|
|
||||||
].filter(Boolean);
|
|
||||||
|
|
||||||
if (upgraded.length === 0) {
|
|
||||||
console.log(`No changes!`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function upgrade_sapper_main() {
|
|
||||||
const _2xx = read('templates/2xx.html');
|
|
||||||
const _4xx = read('templates/4xx.html');
|
|
||||||
const _5xx = read('templates/5xx.html');
|
|
||||||
|
|
||||||
const pattern = /<script src='\%sapper\.main\%'><\/script>/;
|
|
||||||
|
|
||||||
let replaced = false;
|
|
||||||
|
|
||||||
['2xx', '4xx', '5xx'].forEach(code => {
|
|
||||||
const file = `templates/${code}.html`
|
|
||||||
const template = read(file);
|
|
||||||
if (!template) return;
|
|
||||||
|
|
||||||
if (/\%sapper\.main\%/.test(template)) {
|
|
||||||
if (!pattern.test(template)) {
|
|
||||||
console.log(`${clorox.red(`Could not replace %sapper.main% in ${file}`)}`);
|
|
||||||
} else {
|
|
||||||
write(file, template.replace(pattern, `%sapper.scripts%`));
|
|
||||||
console.log(`${clorox.green(`Replaced %sapper.main% in ${file}`)}`);
|
|
||||||
replaced = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return replaced;
|
|
||||||
}
|
|
||||||
|
|
||||||
function read(file: string) {
|
|
||||||
try {
|
|
||||||
return fs.readFileSync(file, 'utf-8');
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function write(file: string, data: string) {
|
|
||||||
fs.writeFileSync(file, data);
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
export const dev = () => process.env.NODE_ENV !== 'production';
|
|
||||||
|
|
||||||
export const locations = {
|
|
||||||
base: () => path.resolve(process.env.SAPPER_BASE || ''),
|
|
||||||
app: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_APP || 'app'),
|
|
||||||
routes: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_ROUTES || 'routes'),
|
|
||||||
dest: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_DEST || '.sapper')
|
|
||||||
};
|
|
||||||
7
src/config/env.ts
Normal file
7
src/config/env.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export let dev: boolean;
|
||||||
|
export let src: string;
|
||||||
|
export let dest: string;
|
||||||
|
|
||||||
|
export const set_dev = (_: boolean) => dev = _;
|
||||||
|
export const set_src = (_: string) => src = _;
|
||||||
|
export const set_dest = (_: string) => dest = _;
|
||||||
53
src/config/rollup.ts
Normal file
53
src/config/rollup.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { dev, src, dest } from './env';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
dev,
|
||||||
|
|
||||||
|
client: {
|
||||||
|
input: () => {
|
||||||
|
return `${src}/client.js`
|
||||||
|
},
|
||||||
|
|
||||||
|
output: () => {
|
||||||
|
let dir = `${dest}/client`;
|
||||||
|
if (process.env.SAPPER_LEGACY_BUILD) dir += `/legacy`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
dir,
|
||||||
|
entryFileNames: '[name].[hash].js',
|
||||||
|
chunkFileNames: '[name].[hash].js',
|
||||||
|
format: 'esm',
|
||||||
|
sourcemap: dev
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
server: {
|
||||||
|
input: () => {
|
||||||
|
return {
|
||||||
|
server: `${src}/server.js`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
output: () => {
|
||||||
|
return {
|
||||||
|
dir: `${dest}/server`,
|
||||||
|
format: 'cjs',
|
||||||
|
sourcemap: dev
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
serviceworker: {
|
||||||
|
input: () => {
|
||||||
|
return `${src}/service-worker.js`;
|
||||||
|
},
|
||||||
|
|
||||||
|
output: () => {
|
||||||
|
return {
|
||||||
|
file: `${dest}/service-worker.js`,
|
||||||
|
format: 'iife'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
import { locations, dev } from './config';
|
import { dev, src, dest } from './env';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
dev: dev(),
|
dev,
|
||||||
|
|
||||||
client: {
|
client: {
|
||||||
entry: () => {
|
entry: () => {
|
||||||
return {
|
return {
|
||||||
main: `${locations.app()}/client`
|
main: `${src}/client`
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
output: () => {
|
output: () => {
|
||||||
return {
|
return {
|
||||||
path: `${locations.dest()}/client`,
|
path: `${dest}/client`,
|
||||||
filename: '[hash]/[name].js',
|
filename: '[hash]/[name].js',
|
||||||
chunkFilename: '[hash]/[name].[id].js',
|
chunkFilename: '[hash]/[name].[id].js',
|
||||||
publicPath: '/client/'
|
publicPath: `client/`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -23,13 +23,13 @@ export default {
|
|||||||
server: {
|
server: {
|
||||||
entry: () => {
|
entry: () => {
|
||||||
return {
|
return {
|
||||||
server: `${locations.app()}/server`
|
server: `${src}/server`
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
output: () => {
|
output: () => {
|
||||||
return {
|
return {
|
||||||
path: locations.dest(),
|
path: `${dest}/server`,
|
||||||
filename: '[name].js',
|
filename: '[name].js',
|
||||||
chunkFilename: '[hash]/[name].[id].js',
|
chunkFilename: '[hash]/[name].[id].js',
|
||||||
libraryTarget: 'commonjs2'
|
libraryTarget: 'commonjs2'
|
||||||
@@ -40,13 +40,13 @@ export default {
|
|||||||
serviceworker: {
|
serviceworker: {
|
||||||
entry: () => {
|
entry: () => {
|
||||||
return {
|
return {
|
||||||
'service-worker': `${locations.app()}/service-worker`
|
'service-worker': `${src}/service-worker`
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
output: () => {
|
output: () => {
|
||||||
return {
|
return {
|
||||||
path: locations.dest(),
|
path: dest,
|
||||||
filename: '[name].js',
|
filename: '[name].js',
|
||||||
chunkFilename: '[name].[id].[hash].js'
|
chunkFilename: '[name].[id].[hash].js'
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
export * from './core/create_manifests';
|
export * from './core/create_app';
|
||||||
export { default as create_compilers } from './core/create_compilers';
|
export { default as create_compilers } from './core/create_compilers/index';
|
||||||
export { default as create_routes } from './core/create_routes';
|
export { default as create_manifest_data } from './core/create_manifest_data';
|
||||||
291
src/core/create_app.ts
Normal file
291
src/core/create_app.ts
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { posixify, stringify, walk, write_if_changed } from '../utils';
|
||||||
|
import { Page, PageComponent, ManifestData } from '../interfaces';
|
||||||
|
|
||||||
|
export function create_app({
|
||||||
|
bundler,
|
||||||
|
manifest_data,
|
||||||
|
dev_port,
|
||||||
|
dev,
|
||||||
|
cwd,
|
||||||
|
src,
|
||||||
|
dest,
|
||||||
|
routes,
|
||||||
|
output
|
||||||
|
}: {
|
||||||
|
bundler: string,
|
||||||
|
manifest_data: ManifestData;
|
||||||
|
dev_port?: number;
|
||||||
|
dev: boolean;
|
||||||
|
cwd: string;
|
||||||
|
src: string;
|
||||||
|
dest: string;
|
||||||
|
routes: string;
|
||||||
|
output: string
|
||||||
|
}) {
|
||||||
|
if (!fs.existsSync(output)) fs.mkdirSync(output);
|
||||||
|
|
||||||
|
const path_to_routes = path.relative(`${output}/internal`, routes);
|
||||||
|
|
||||||
|
const client_manifest = generate_client_manifest(manifest_data, path_to_routes, bundler, dev, dev_port);
|
||||||
|
const server_manifest = generate_server_manifest(manifest_data, path_to_routes, cwd, src, dest, dev);
|
||||||
|
|
||||||
|
const app = generate_app(manifest_data, path_to_routes);
|
||||||
|
|
||||||
|
write_if_changed(`${output}/internal/manifest-client.mjs`, client_manifest);
|
||||||
|
write_if_changed(`${output}/internal/manifest-server.mjs`, server_manifest);
|
||||||
|
write_if_changed(`${output}/internal/App.svelte`, app);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function create_serviceworker_manifest({ manifest_data, output, client_files, static_files }: {
|
||||||
|
manifest_data: ManifestData;
|
||||||
|
output: string;
|
||||||
|
client_files: string[];
|
||||||
|
static_files: string;
|
||||||
|
}) {
|
||||||
|
let files: string[] = ['service-worker-index.html'];
|
||||||
|
|
||||||
|
if (fs.existsSync(static_files)) {
|
||||||
|
files = files.concat(walk(static_files));
|
||||||
|
} else {
|
||||||
|
// TODO remove in a future version
|
||||||
|
if (fs.existsSync('assets')) {
|
||||||
|
throw new Error(`As of Sapper 0.21, the assets/ directory should become static/`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = `
|
||||||
|
// This file is generated by Sapper — do not edit it!
|
||||||
|
export const timestamp = ${Date.now()};
|
||||||
|
|
||||||
|
export const files = [\n\t${files.map((x: string) => stringify(x)).join(',\n\t')}\n];
|
||||||
|
export { files as assets }; // legacy
|
||||||
|
|
||||||
|
export const shell = [\n\t${client_files.map((x: string) => stringify(x)).join(',\n\t')}\n];
|
||||||
|
|
||||||
|
export const routes = [\n\t${manifest_data.pages.map((r: Page) => `{ pattern: ${r.pattern} }`).join(',\n\t')}\n];
|
||||||
|
`.replace(/^\t\t/gm, '').trim();
|
||||||
|
|
||||||
|
write_if_changed(`${output}/service-worker.js`, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate_client_manifest(
|
||||||
|
manifest_data: ManifestData,
|
||||||
|
path_to_routes: string,
|
||||||
|
bundler: string,
|
||||||
|
dev: boolean,
|
||||||
|
dev_port?: number
|
||||||
|
) {
|
||||||
|
const page_ids = new Set(manifest_data.pages.map(page =>
|
||||||
|
page.pattern.toString()));
|
||||||
|
|
||||||
|
const server_routes_to_ignore = manifest_data.server_routes.filter(route =>
|
||||||
|
!page_ids.has(route.pattern.toString()));
|
||||||
|
|
||||||
|
const component_indexes: Record<string, number> = {};
|
||||||
|
|
||||||
|
const components = `[
|
||||||
|
${manifest_data.components.map((component, i) => {
|
||||||
|
const annotation = bundler === 'webpack'
|
||||||
|
? `/* webpackChunkName: "${component.name}" */ `
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const source = get_file(path_to_routes, component);
|
||||||
|
|
||||||
|
component_indexes[component.name] = i;
|
||||||
|
|
||||||
|
return `{
|
||||||
|
js: () => import(${annotation}${stringify(source)}),
|
||||||
|
css: "__SAPPER_CSS_PLACEHOLDER:${stringify(component.file, false)}__"
|
||||||
|
}`;
|
||||||
|
}).join(',\n\t\t\t\t')}
|
||||||
|
]`.replace(/^\t/gm, '');
|
||||||
|
|
||||||
|
let needs_decode = false;
|
||||||
|
|
||||||
|
let routes = `[
|
||||||
|
${manifest_data.pages.map(page => `{
|
||||||
|
// ${page.parts[page.parts.length - 1].component.file}
|
||||||
|
pattern: ${page.pattern},
|
||||||
|
parts: [
|
||||||
|
${page.parts.map(part => {
|
||||||
|
if (part === null) return 'null';
|
||||||
|
|
||||||
|
if (part.params.length > 0) {
|
||||||
|
needs_decode = true;
|
||||||
|
const props = part.params.map((param, i) => `${param}: d(match[${i + 1}])`);
|
||||||
|
return `{ i: ${component_indexes[part.component.name]}, params: match => ({ ${props.join(', ')} }) }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `{ i: ${component_indexes[part.component.name]} }`;
|
||||||
|
}).join(',\n\t\t\t\t\t\t')}
|
||||||
|
]
|
||||||
|
}`).join(',\n\n\t\t\t\t')}
|
||||||
|
]`.replace(/^\t/gm, '');
|
||||||
|
|
||||||
|
if (needs_decode) {
|
||||||
|
routes = `(d => ${routes})(decodeURIComponent)`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
// This file is generated by Sapper — do not edit it!
|
||||||
|
export { default as Root } from '${stringify(get_file(path_to_routes, manifest_data.root), false)}';
|
||||||
|
export { preload as root_preload } from '${manifest_data.root.has_preload ? stringify(get_file(path_to_routes, manifest_data.root), false) : './shared'}';
|
||||||
|
export { default as ErrorComponent } from '${stringify(get_file(path_to_routes, manifest_data.error), false)}';
|
||||||
|
|
||||||
|
export const ignore = [${server_routes_to_ignore.map(route => route.pattern).join(', ')}];
|
||||||
|
|
||||||
|
export const components = ${components};
|
||||||
|
|
||||||
|
export const routes = ${routes};
|
||||||
|
|
||||||
|
${dev ? `if (typeof window !== 'undefined') {
|
||||||
|
import(${stringify(posixify(path.resolve(__dirname, '../sapper-dev-client.js')))}).then(client => {
|
||||||
|
client.connect(${dev_port});
|
||||||
|
});
|
||||||
|
}` : ''}
|
||||||
|
`.replace(/^\t{2}/gm, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate_server_manifest(
|
||||||
|
manifest_data: ManifestData,
|
||||||
|
path_to_routes: string,
|
||||||
|
cwd: string,
|
||||||
|
src: string,
|
||||||
|
dest: string,
|
||||||
|
dev: boolean
|
||||||
|
) {
|
||||||
|
const imports = [].concat(
|
||||||
|
manifest_data.server_routes.map((route, i) =>
|
||||||
|
`import * as route_${i} from ${stringify(posixify(`${path_to_routes}/${route.file}`))};`),
|
||||||
|
manifest_data.components.map((component, i) =>
|
||||||
|
`import component_${i}${component.has_preload ? `, { preload as preload_${i} }` : ''} from ${stringify(get_file(path_to_routes, component))};`),
|
||||||
|
`import root${manifest_data.root.has_preload ? `, { preload as root_preload }` : ''} from ${stringify(get_file(path_to_routes, manifest_data.root))};`,
|
||||||
|
`import error from ${stringify(get_file(path_to_routes, manifest_data.error))};`
|
||||||
|
);
|
||||||
|
|
||||||
|
const component_lookup: Record<string, number> = {};
|
||||||
|
manifest_data.components.forEach((component, i) => {
|
||||||
|
component_lookup[component.name] = i;
|
||||||
|
});
|
||||||
|
|
||||||
|
let code = `
|
||||||
|
`.replace(/^\t\t/gm, '').trim();
|
||||||
|
|
||||||
|
const build_dir = posixify(path.relative(cwd, dest));
|
||||||
|
const src_dir = posixify(path.relative(cwd, src));
|
||||||
|
|
||||||
|
return `
|
||||||
|
// This file is generated by Sapper — do not edit it!
|
||||||
|
${imports.join('\n')}
|
||||||
|
|
||||||
|
const d = decodeURIComponent;
|
||||||
|
|
||||||
|
export const manifest = {
|
||||||
|
server_routes: [
|
||||||
|
${manifest_data.server_routes.map((route, i) => `{
|
||||||
|
// ${route.file}
|
||||||
|
pattern: ${route.pattern},
|
||||||
|
handlers: route_${i},
|
||||||
|
params: ${route.params.length > 0
|
||||||
|
? `match => ({ ${route.params.map((param, i) => `${param}: d(match[${i + 1}])`).join(', ')} })`
|
||||||
|
: `() => ({})`}
|
||||||
|
}`).join(',\n\n\t\t\t\t')}
|
||||||
|
],
|
||||||
|
|
||||||
|
pages: [
|
||||||
|
${manifest_data.pages.map(page => `{
|
||||||
|
// ${page.parts[page.parts.length - 1].component.file}
|
||||||
|
pattern: ${page.pattern},
|
||||||
|
parts: [
|
||||||
|
${page.parts.map(part => {
|
||||||
|
if (part === null) return 'null';
|
||||||
|
|
||||||
|
const props = [
|
||||||
|
`name: "${part.component.name}"`,
|
||||||
|
`file: ${stringify(part.component.file)}`,
|
||||||
|
`component: component_${component_lookup[part.component.name]}`,
|
||||||
|
part.component.has_preload && `preload: preload_${component_lookup[part.component.name]}`
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
if (part.params.length > 0) {
|
||||||
|
const params = part.params.map((param, i) => `${param}: d(match[${i + 1}])`);
|
||||||
|
props.push(`params: match => ({ ${params.join(', ')} })`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `{ ${props.join(', ')} }`;
|
||||||
|
}).join(',\n\t\t\t\t\t\t')}
|
||||||
|
]
|
||||||
|
}`).join(',\n\n\t\t\t\t')}
|
||||||
|
],
|
||||||
|
|
||||||
|
root,
|
||||||
|
root_preload${manifest_data.root.has_preload ? '' : `: () => {}`},
|
||||||
|
error
|
||||||
|
};
|
||||||
|
|
||||||
|
export const build_dir = ${JSON.stringify(build_dir)};
|
||||||
|
|
||||||
|
export const src_dir = ${JSON.stringify(src_dir)};
|
||||||
|
|
||||||
|
export const dev = ${dev ? 'true' : 'false'};
|
||||||
|
`.replace(/^\t{2}/gm, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate_app(manifest_data: ManifestData, path_to_routes: string) {
|
||||||
|
// TODO remove default layout altogether
|
||||||
|
|
||||||
|
const max_depth = Math.max(...manifest_data.pages.map(page => page.parts.filter(Boolean).length));
|
||||||
|
|
||||||
|
const levels = [];
|
||||||
|
for (let i = 0; i < max_depth; i += 1) {
|
||||||
|
levels.push(i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let l = max_depth;
|
||||||
|
|
||||||
|
let pyramid = `<svelte:component this={level${l}.component} {...level${l}.props}/>`;
|
||||||
|
|
||||||
|
while (l-- > 1) {
|
||||||
|
pyramid = `
|
||||||
|
<svelte:component this={level${l}.component} segment={segments[${l}]} {...level${l}.props}>
|
||||||
|
{#if level${l + 1}}
|
||||||
|
${pyramid.replace(/\n/g, '\n\t\t\t\t\t')}
|
||||||
|
{/if}
|
||||||
|
</svelte:component>
|
||||||
|
`.replace(/^\t\t\t/gm, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<!-- This file is generated by Sapper — do not edit it! -->
|
||||||
|
<script>
|
||||||
|
import { setContext } from 'svelte';
|
||||||
|
import { CONTEXT_KEY } from './shared';
|
||||||
|
import Layout from '${get_file(path_to_routes, manifest_data.root)}';
|
||||||
|
import Error from '${get_file(path_to_routes, manifest_data.error)}';
|
||||||
|
|
||||||
|
export let session;
|
||||||
|
export let error;
|
||||||
|
export let status;
|
||||||
|
export let segments;
|
||||||
|
export let level0;
|
||||||
|
${levels.map(l => `export let level${l} = null;`).join('\n\t\t\t')}
|
||||||
|
|
||||||
|
setContext(CONTEXT_KEY, session);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Layout segment={segments[0]} {...level0.props}>
|
||||||
|
{#if error}
|
||||||
|
<Error {error} {status}/>
|
||||||
|
{:else}
|
||||||
|
${pyramid.replace(/\n/g, '\n\t\t\t\t')}
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
`.replace(/^\t\t/gm, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_file(path_to_routes: string, component: PageComponent) {
|
||||||
|
if (component.default) return `./${component.type}.svelte`;
|
||||||
|
return posixify(`${path_to_routes}/${component.file}`);
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import * as path from 'path';
|
|
||||||
import relative from 'require-relative';
|
|
||||||
|
|
||||||
export default function create_compilers() {
|
|
||||||
const webpack = relative('webpack', process.cwd());
|
|
||||||
|
|
||||||
const serviceworker_config = try_require(path.resolve('webpack/service-worker.config.js'));
|
|
||||||
|
|
||||||
return {
|
|
||||||
client: webpack(
|
|
||||||
require(path.resolve('webpack/client.config.js'))
|
|
||||||
),
|
|
||||||
|
|
||||||
server: webpack(
|
|
||||||
require(path.resolve('webpack/server.config.js'))
|
|
||||||
),
|
|
||||||
|
|
||||||
serviceworker: serviceworker_config && webpack(serviceworker_config)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function try_require(specifier: string) {
|
|
||||||
try {
|
|
||||||
return require(specifier);
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code === 'MODULE_NOT_FOUND') return null;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
169
src/core/create_compilers/RollupCompiler.ts
Normal file
169
src/core/create_compilers/RollupCompiler.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import relative from 'require-relative';
|
||||||
|
import { CompileResult } from './interfaces';
|
||||||
|
import RollupResult from './RollupResult';
|
||||||
|
|
||||||
|
let rollup: any;
|
||||||
|
|
||||||
|
export default class RollupCompiler {
|
||||||
|
_: Promise<any>;
|
||||||
|
_oninvalid: (filename: string) => void;
|
||||||
|
_start: number;
|
||||||
|
input: string;
|
||||||
|
warnings: any[];
|
||||||
|
errors: any[];
|
||||||
|
chunks: any[];
|
||||||
|
css_files: Array<{ id: string, code: string }>;
|
||||||
|
|
||||||
|
constructor(config: any) {
|
||||||
|
this._ = this.get_config(config);
|
||||||
|
this.input = null;
|
||||||
|
this.warnings = [];
|
||||||
|
this.errors = [];
|
||||||
|
this.chunks = [];
|
||||||
|
this.css_files = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async get_config(mod: any) {
|
||||||
|
// TODO this is hacky, and doesn't need to apply to all three compilers
|
||||||
|
(mod.plugins || (mod.plugins = [])).push({
|
||||||
|
name: 'sapper-internal',
|
||||||
|
options: (opts: any) => {
|
||||||
|
this.input = opts.input;
|
||||||
|
},
|
||||||
|
renderChunk: (code: string, chunk: any) => {
|
||||||
|
this.chunks.push(chunk);
|
||||||
|
},
|
||||||
|
transform: (code: string, id: string) => {
|
||||||
|
if (/\.css$/.test(id)) {
|
||||||
|
this.css_files.push({ id, code });
|
||||||
|
return ``;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onwarn = mod.onwarn || ((warning: any, handler: (warning: any) => void) => {
|
||||||
|
handler(warning);
|
||||||
|
});
|
||||||
|
|
||||||
|
mod.onwarn = (warning: any) => {
|
||||||
|
onwarn(warning, (warning: any) => {
|
||||||
|
this.warnings.push(warning);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return mod;
|
||||||
|
}
|
||||||
|
|
||||||
|
oninvalid(cb: (filename: string) => void) {
|
||||||
|
this._oninvalid = cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
async compile(): Promise<CompileResult> {
|
||||||
|
const config = await this._;
|
||||||
|
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const bundle = await rollup.rollup(config);
|
||||||
|
await bundle.write(config.output);
|
||||||
|
|
||||||
|
return new RollupResult(Date.now() - start, this);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.filename) {
|
||||||
|
// TODO this is a bit messy. Also, can
|
||||||
|
// Rollup emit other kinds of error?
|
||||||
|
err.message = [
|
||||||
|
`Failed to build — error in ${err.filename}: ${err.message}`,
|
||||||
|
err.frame
|
||||||
|
].filter(Boolean).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async watch(cb: (err?: Error, stats?: any) => void) {
|
||||||
|
const config = await this._;
|
||||||
|
|
||||||
|
const watcher = rollup.watch(config);
|
||||||
|
|
||||||
|
watcher.on('change', (id: string) => {
|
||||||
|
this.chunks = [];
|
||||||
|
this.warnings = [];
|
||||||
|
this.errors = [];
|
||||||
|
this._oninvalid(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.on('event', (event: any) => {
|
||||||
|
switch (event.code) {
|
||||||
|
case 'FATAL':
|
||||||
|
// TODO kill the process?
|
||||||
|
if (event.error.filename) {
|
||||||
|
// TODO this is a bit messy. Also, can
|
||||||
|
// Rollup emit other kinds of error?
|
||||||
|
event.error.message = [
|
||||||
|
`Failed to build — error in ${event.error.filename}: ${event.error.message}`,
|
||||||
|
event.error.frame
|
||||||
|
].filter(Boolean).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(event.error);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ERROR':
|
||||||
|
this.errors.push(event.error);
|
||||||
|
cb(null, new RollupResult(Date.now() - this._start, this));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'START':
|
||||||
|
case 'END':
|
||||||
|
// TODO is there anything to do with this info?
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'BUNDLE_START':
|
||||||
|
this._start = Date.now();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'BUNDLE_END':
|
||||||
|
cb(null, new RollupResult(Date.now() - this._start, this));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log(`Unexpected event ${event.code}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async load_config(cwd: string) {
|
||||||
|
if (!rollup) rollup = relative('rollup', cwd);
|
||||||
|
|
||||||
|
const input = path.resolve(cwd, 'rollup.config.js');
|
||||||
|
|
||||||
|
const bundle = await rollup.rollup({
|
||||||
|
input,
|
||||||
|
inlineDynamicImports: true,
|
||||||
|
external: (id: string) => {
|
||||||
|
return (id[0] !== '.' && !path.isAbsolute(id)) || id.slice(-5, id.length) === '.json';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const resp = await bundle.generate({ format: 'cjs' });
|
||||||
|
const { code } = resp.output ? resp.output[0] : resp;
|
||||||
|
|
||||||
|
// temporarily override require
|
||||||
|
const defaultLoader = require.extensions['.js'];
|
||||||
|
require.extensions['.js'] = (module: any, filename: string) => {
|
||||||
|
if (filename === input) {
|
||||||
|
module._compile(code, filename);
|
||||||
|
} else {
|
||||||
|
defaultLoader(module, filename);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const config: any = require(input);
|
||||||
|
delete require.cache[input];
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
119
src/core/create_compilers/RollupResult.ts
Normal file
119
src/core/create_compilers/RollupResult.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import colors from 'kleur';
|
||||||
|
import pb from 'pretty-bytes';
|
||||||
|
import RollupCompiler from './RollupCompiler';
|
||||||
|
import extract_css from './extract_css';
|
||||||
|
import { left_pad } from '../../utils';
|
||||||
|
import { CompileResult, BuildInfo, CompileError, Chunk, CssFile } from './interfaces';
|
||||||
|
import { ManifestData, Dirs } from '../../interfaces';
|
||||||
|
|
||||||
|
export default class RollupResult implements CompileResult {
|
||||||
|
duration: number;
|
||||||
|
errors: CompileError[];
|
||||||
|
warnings: CompileError[];
|
||||||
|
chunks: Chunk[];
|
||||||
|
assets: Record<string, string>;
|
||||||
|
css_files: CssFile[];
|
||||||
|
css: {
|
||||||
|
main: string,
|
||||||
|
chunks: Record<string, string[]>
|
||||||
|
};
|
||||||
|
summary: string;
|
||||||
|
|
||||||
|
constructor(duration: number, compiler: RollupCompiler) {
|
||||||
|
this.duration = duration;
|
||||||
|
|
||||||
|
this.errors = compiler.errors.map(munge_warning_or_error);
|
||||||
|
this.warnings = compiler.warnings.map(munge_warning_or_error); // TODO emit this as they happen
|
||||||
|
|
||||||
|
this.chunks = compiler.chunks.map(chunk => ({
|
||||||
|
file: chunk.fileName,
|
||||||
|
imports: chunk.imports.filter(Boolean),
|
||||||
|
modules: Object.keys(chunk.modules)
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.css_files = compiler.css_files;
|
||||||
|
|
||||||
|
// TODO populate this properly. We don't have named chunks, as in
|
||||||
|
// webpack, but we can have a route -> [chunk] map or something
|
||||||
|
this.assets = {};
|
||||||
|
|
||||||
|
if (typeof compiler.input === 'string') {
|
||||||
|
compiler.chunks.forEach(chunk => {
|
||||||
|
if (compiler.input in chunk.modules) {
|
||||||
|
this.assets.main = chunk.fileName;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
for (const name in compiler.input) {
|
||||||
|
const file = compiler.input[name];
|
||||||
|
const chunk = compiler.chunks.find(chunk => file in chunk.modules);
|
||||||
|
if (chunk) this.assets[name] = chunk.fileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.summary = compiler.chunks.map(chunk => {
|
||||||
|
const size_color = chunk.code.length > 150000 ? colors.bold().red : chunk.code.length > 50000 ? colors.bold().yellow : colors.bold().white;
|
||||||
|
const size_label = left_pad(pb(chunk.code.length), 10);
|
||||||
|
|
||||||
|
const lines = [size_color(`${size_label} ${chunk.fileName}`)];
|
||||||
|
|
||||||
|
const deps = Object.keys(chunk.modules)
|
||||||
|
.map(file => {
|
||||||
|
return {
|
||||||
|
file: path.relative(process.cwd(), file),
|
||||||
|
size: chunk.modules[file].renderedLength
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(dep => dep.size > 0)
|
||||||
|
.sort((a, b) => b.size - a.size);
|
||||||
|
|
||||||
|
const total_unminified = deps.reduce((t, d) => t + d.size, 0);
|
||||||
|
|
||||||
|
deps.forEach((dep, i) => {
|
||||||
|
const c = i === deps.length - 1 ? '└' : '│';
|
||||||
|
let line = ` ${c} ${dep.file}`;
|
||||||
|
|
||||||
|
if (deps.length > 1) {
|
||||||
|
const p = (100 * dep.size / total_unminified).toFixed(1);
|
||||||
|
line += ` (${p}%)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(colors.gray(line));
|
||||||
|
});
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
to_json(manifest_data: ManifestData, dirs: Dirs): BuildInfo {
|
||||||
|
// TODO extract_css has side-effects that don't belong
|
||||||
|
// in a method called to_json
|
||||||
|
return {
|
||||||
|
bundler: 'rollup',
|
||||||
|
shimport: require('shimport/package.json').version,
|
||||||
|
assets: this.assets,
|
||||||
|
css: extract_css(this, manifest_data.components, dirs)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
print() {
|
||||||
|
const blocks: string[] = this.warnings.map(warning => {
|
||||||
|
return warning.file
|
||||||
|
? `> ${colors.bold(warning.file)}\n${warning.message}`
|
||||||
|
: `> ${warning.message}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
blocks.push(this.summary);
|
||||||
|
|
||||||
|
return blocks.join('\n\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function munge_warning_or_error(warning_or_error: any) {
|
||||||
|
return {
|
||||||
|
file: warning_or_error.filename,
|
||||||
|
message: [warning_or_error.message, warning_or_error.frame].filter(Boolean).join('\n')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
46
src/core/create_compilers/WebpackCompiler.ts
Normal file
46
src/core/create_compilers/WebpackCompiler.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import relative from 'require-relative';
|
||||||
|
import { CompileResult } from './interfaces';
|
||||||
|
import WebpackResult from './WebpackResult';
|
||||||
|
|
||||||
|
let webpack: any;
|
||||||
|
|
||||||
|
export class WebpackCompiler {
|
||||||
|
_: any;
|
||||||
|
|
||||||
|
constructor(config: any) {
|
||||||
|
if (!webpack) webpack = relative('webpack', process.cwd());
|
||||||
|
this._ = webpack(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
oninvalid(cb: (filename: string) => void) {
|
||||||
|
this._.hooks.invalid.tap('sapper', cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
compile(): Promise<CompileResult> {
|
||||||
|
return new Promise((fulfil, reject) => {
|
||||||
|
this._.run((err: Error, stats: any) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = new WebpackResult(stats);
|
||||||
|
|
||||||
|
if (result.errors.length) {
|
||||||
|
console.error(stats.toString({ colors: true }));
|
||||||
|
reject(new Error(`Encountered errors while building app`));
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
fulfil(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(cb: (err?: Error, stats?: any) => void) {
|
||||||
|
this._.watch({}, (err?: Error, stats?: any) => {
|
||||||
|
cb(err, stats && new WebpackResult(stats));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/core/create_compilers/WebpackResult.ts
Normal file
87
src/core/create_compilers/WebpackResult.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import format_messages from 'webpack-format-messages';
|
||||||
|
import { CompileResult, BuildInfo, CompileError, Chunk, CssFile } from './interfaces';
|
||||||
|
import { ManifestData, Dirs, PageComponent } from '../../interfaces';
|
||||||
|
|
||||||
|
const locPattern = /\((\d+):(\d+)\)$/;
|
||||||
|
|
||||||
|
function munge_warning_or_error(message: string) {
|
||||||
|
// TODO this is all a bit rube goldberg...
|
||||||
|
const lines = message.split('\n');
|
||||||
|
|
||||||
|
const file = lines.shift()
|
||||||
|
.replace('[7m', '') // careful — there is a special character at the beginning of this string
|
||||||
|
.replace('[27m', '')
|
||||||
|
.replace('./', '');
|
||||||
|
|
||||||
|
let line = null;
|
||||||
|
let column = null;
|
||||||
|
|
||||||
|
const match = locPattern.exec(lines[0]);
|
||||||
|
if (match) {
|
||||||
|
lines[0] = lines[0].replace(locPattern, '');
|
||||||
|
line = +match[1];
|
||||||
|
column = +match[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
file,
|
||||||
|
message: lines.join('\n')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class WebpackResult implements CompileResult {
|
||||||
|
duration: number;
|
||||||
|
errors: CompileError[];
|
||||||
|
warnings: CompileError[];
|
||||||
|
chunks: Chunk[];
|
||||||
|
assets: Record<string, string>;
|
||||||
|
css_files: CssFile[];
|
||||||
|
stats: any;
|
||||||
|
|
||||||
|
constructor(stats: any) {
|
||||||
|
this.stats = stats;
|
||||||
|
|
||||||
|
const info = stats.toJson();
|
||||||
|
|
||||||
|
const messages = format_messages(stats);
|
||||||
|
|
||||||
|
this.errors = messages.errors.map(munge_warning_or_error);
|
||||||
|
this.warnings = messages.warnings.map(munge_warning_or_error);
|
||||||
|
|
||||||
|
this.duration = info.time;
|
||||||
|
|
||||||
|
this.chunks = info.assets.map((chunk: { name: string }) => ({ file: chunk.name }));
|
||||||
|
this.assets = info.assetsByChunkName;
|
||||||
|
}
|
||||||
|
|
||||||
|
to_json(manifest_data: ManifestData, dirs: Dirs): BuildInfo {
|
||||||
|
const extract_css = (assets: string[] | string) => {
|
||||||
|
assets = Array.isArray(assets) ? assets : [assets];
|
||||||
|
return assets.find(asset => /\.css$/.test(asset));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
bundler: 'webpack',
|
||||||
|
shimport: null, // webpack has its own loader
|
||||||
|
assets: this.assets,
|
||||||
|
css: {
|
||||||
|
main: extract_css(this.assets.main),
|
||||||
|
chunks: manifest_data.components
|
||||||
|
.reduce((chunks: Record<string, string[]>, component: PageComponent) => {
|
||||||
|
const css_dependencies = [];
|
||||||
|
const css = extract_css(this.assets[component.name]);
|
||||||
|
|
||||||
|
if (css) css_dependencies.push(css);
|
||||||
|
|
||||||
|
chunks[component.file] = css_dependencies;
|
||||||
|
|
||||||
|
return chunks;
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
print() {
|
||||||
|
return this.stats.toString({ colors: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
248
src/core/create_compilers/extract_css.ts
Normal file
248
src/core/create_compilers/extract_css.ts
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import hash from 'string-hash';
|
||||||
|
import * as codec from 'sourcemap-codec';
|
||||||
|
import { PageComponent, Dirs } from '../../interfaces';
|
||||||
|
import { CompileResult, Chunk } from './interfaces';
|
||||||
|
import { posixify } from '../../utils'
|
||||||
|
|
||||||
|
const inline_sourcemap_header = 'data:application/json;charset=utf-8;base64,';
|
||||||
|
|
||||||
|
function extract_sourcemap(raw: string, id: string) {
|
||||||
|
let raw_map: string;
|
||||||
|
let map = null;
|
||||||
|
|
||||||
|
const code = raw.replace(/\/\*#\s+sourceMappingURL=(.+)\s+\*\//g, (m, url) => {
|
||||||
|
if (raw_map) {
|
||||||
|
// TODO should not happen!
|
||||||
|
throw new Error(`Found multiple sourcemaps in single CSS file (${id})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
raw_map = url;
|
||||||
|
return '';
|
||||||
|
}).trim();
|
||||||
|
|
||||||
|
if (raw_map) {
|
||||||
|
if (raw_map.startsWith(inline_sourcemap_header)) {
|
||||||
|
const json = Buffer.from(raw_map.slice(inline_sourcemap_header.length), 'base64').toString();
|
||||||
|
map = JSON.parse(json);
|
||||||
|
} else {
|
||||||
|
// TODO do we want to handle non-inline sourcemaps? could be a rabbit hole
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
map
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type SourceMap = {
|
||||||
|
version: 3;
|
||||||
|
file: string;
|
||||||
|
sources: string[];
|
||||||
|
sourcesContent: string[];
|
||||||
|
names: string[];
|
||||||
|
mappings: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function get_css_from_modules(modules: string[], css_map: Map<string, string>, dirs: Dirs) {
|
||||||
|
const parts: string[] = [];
|
||||||
|
const mappings: number[][][] = [];
|
||||||
|
|
||||||
|
const combined_map: SourceMap = {
|
||||||
|
version: 3,
|
||||||
|
file: null,
|
||||||
|
sources: [],
|
||||||
|
sourcesContent: [],
|
||||||
|
names: [],
|
||||||
|
mappings: null
|
||||||
|
};
|
||||||
|
|
||||||
|
modules.forEach(module => {
|
||||||
|
if (!/\.css$/.test(module)) return;
|
||||||
|
|
||||||
|
const css = css_map.get(module);
|
||||||
|
|
||||||
|
const { code, map } = extract_sourcemap(css, module);
|
||||||
|
|
||||||
|
parts.push(code);
|
||||||
|
|
||||||
|
if (map) {
|
||||||
|
const lines = codec.decode(map.mappings);
|
||||||
|
|
||||||
|
if (combined_map.sources.length > 0 || combined_map.names.length > 0) {
|
||||||
|
lines.forEach(line => {
|
||||||
|
line.forEach(segment => {
|
||||||
|
// adjust source index
|
||||||
|
segment[1] += combined_map.sources.length;
|
||||||
|
|
||||||
|
// adjust name index
|
||||||
|
if (segment[4]) segment[4] += combined_map.names.length;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
combined_map.sources.push(...map.sources);
|
||||||
|
combined_map.sourcesContent.push(...map.sourcesContent);
|
||||||
|
combined_map.names.push(...map.names);
|
||||||
|
|
||||||
|
mappings.push(...lines);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parts.length > 0) {
|
||||||
|
combined_map.mappings = codec.encode(mappings);
|
||||||
|
|
||||||
|
combined_map.sources = combined_map.sources.map(source => path.relative(`${dirs.dest}/client`, source));
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: parts.join('\n'),
|
||||||
|
map: combined_map
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function extract_css(client_result: CompileResult, components: PageComponent[], dirs: Dirs) {
|
||||||
|
const result: {
|
||||||
|
main: string | null;
|
||||||
|
chunks: Record<string, string[]>
|
||||||
|
} = {
|
||||||
|
main: null,
|
||||||
|
chunks: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!client_result.css_files) return; // Rollup-only for now
|
||||||
|
|
||||||
|
let asset_dir = `${dirs.dest}/client`;
|
||||||
|
if (process.env.SAPPER_LEGACY_BUILD) asset_dir += '/legacy';
|
||||||
|
|
||||||
|
const unclaimed = new Set(client_result.css_files.map(x => x.id));
|
||||||
|
|
||||||
|
const lookup = new Map();
|
||||||
|
client_result.chunks.forEach(chunk => {
|
||||||
|
lookup.set(chunk.file, chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
const css_map = new Map();
|
||||||
|
client_result.css_files.forEach(css_module => {
|
||||||
|
css_map.set(css_module.id, css_module.code);
|
||||||
|
});
|
||||||
|
|
||||||
|
const chunks_with_css = new Set();
|
||||||
|
|
||||||
|
// concatenate and emit CSS
|
||||||
|
client_result.chunks.forEach(chunk => {
|
||||||
|
const css_modules = chunk.modules.filter(m => css_map.has(m));
|
||||||
|
if (!css_modules.length) return;
|
||||||
|
|
||||||
|
const css = get_css_from_modules(css_modules, css_map, dirs);
|
||||||
|
|
||||||
|
const { code, map } = css;
|
||||||
|
|
||||||
|
const output_file_name = chunk.file.replace(/\.js$/, '.css');
|
||||||
|
|
||||||
|
map.file = output_file_name;
|
||||||
|
map.sources = map.sources.map(source => path.relative(`${asset_dir}`, source));
|
||||||
|
|
||||||
|
fs.writeFileSync(`${asset_dir}/${output_file_name}`, `${code}\n/* sourceMappingURL=./${output_file_name}.map */`);
|
||||||
|
fs.writeFileSync(`${asset_dir}/${output_file_name}.map`, JSON.stringify(map, null, ' '));
|
||||||
|
|
||||||
|
chunks_with_css.add(chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
const entry = path.resolve(dirs.src, 'client.js');
|
||||||
|
const entry_chunk = client_result.chunks.find(chunk => chunk.modules.indexOf(entry) !== -1);
|
||||||
|
|
||||||
|
const entry_chunk_dependencies: Set<Chunk> = new Set([entry_chunk]);
|
||||||
|
const entry_css_modules: string[] = [];
|
||||||
|
|
||||||
|
// recursively find the chunks this component depends on
|
||||||
|
entry_chunk_dependencies.forEach(chunk => {
|
||||||
|
if (!chunk) return; // TODO why does this happen?
|
||||||
|
|
||||||
|
chunk.imports.forEach(file => {
|
||||||
|
entry_chunk_dependencies.add(lookup.get(file));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (chunks_with_css.has(chunk)) {
|
||||||
|
chunk.modules.forEach(file => {
|
||||||
|
unclaimed.delete(file);
|
||||||
|
if (css_map.has(file)) {
|
||||||
|
entry_css_modules.push(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// figure out which (css-having) chunks each component depends on
|
||||||
|
components.forEach(component => {
|
||||||
|
const resolved = path.resolve(dirs.routes, component.file);
|
||||||
|
const chunk: Chunk = client_result.chunks.find(chunk => chunk.modules.indexOf(resolved) !== -1);
|
||||||
|
|
||||||
|
if (!chunk) {
|
||||||
|
// this should never happen!
|
||||||
|
return;
|
||||||
|
// throw new Error(`Could not find chunk that owns ${component.file}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunk_dependencies: Set<Chunk> = new Set([chunk]);
|
||||||
|
const css_dependencies: string[] = [];
|
||||||
|
|
||||||
|
// recursively find the chunks this component depends on
|
||||||
|
chunk_dependencies.forEach(chunk => {
|
||||||
|
if (!chunk) return; // TODO why does this happen?
|
||||||
|
|
||||||
|
chunk.imports.forEach(file => {
|
||||||
|
chunk_dependencies.add(lookup.get(file));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (chunks_with_css.has(chunk)) {
|
||||||
|
css_dependencies.push(chunk.file.replace(/\.js$/, '.css'));
|
||||||
|
|
||||||
|
chunk.modules.forEach(file => {
|
||||||
|
unclaimed.delete(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
result.chunks[component.file] = css_dependencies;
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.readdirSync(asset_dir).forEach(file => {
|
||||||
|
if (fs.statSync(`${asset_dir}/${file}`).isDirectory()) return;
|
||||||
|
|
||||||
|
const source = fs.readFileSync(`${asset_dir}/${file}`, 'utf-8');
|
||||||
|
|
||||||
|
const replaced = source.replace(/["']__SAPPER_CSS_PLACEHOLDER:(.+?)__["']/g, (m, route) => {
|
||||||
|
return JSON.stringify(result.chunks[route]);
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(`${asset_dir}/${file}`, replaced);
|
||||||
|
});
|
||||||
|
|
||||||
|
unclaimed.forEach(file => {
|
||||||
|
entry_css_modules.push(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
const leftover = get_css_from_modules(entry_css_modules, css_map, dirs);
|
||||||
|
if (leftover) {
|
||||||
|
const { code, map } = leftover;
|
||||||
|
|
||||||
|
const main_hash = hash(code);
|
||||||
|
|
||||||
|
const output_file_name = `main.${main_hash}.css`;
|
||||||
|
|
||||||
|
map.file = output_file_name;
|
||||||
|
map.sources = map.sources.map(source => path.relative(asset_dir, source));
|
||||||
|
|
||||||
|
fs.writeFileSync(`${asset_dir}/${output_file_name}`, `${code}\n/* sourceMappingURL=client/${output_file_name}.map */`);
|
||||||
|
fs.writeFileSync(`${asset_dir}/${output_file_name}.map`, JSON.stringify(map, null, ' '));
|
||||||
|
|
||||||
|
result.main = output_file_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
72
src/core/create_compilers/index.ts
Normal file
72
src/core/create_compilers/index.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import RollupCompiler from './RollupCompiler';
|
||||||
|
import { WebpackCompiler } from './WebpackCompiler';
|
||||||
|
import { set_dev, set_src, set_dest } from '../../config/env';
|
||||||
|
|
||||||
|
export type Compiler = RollupCompiler | WebpackCompiler;
|
||||||
|
|
||||||
|
export type Compilers = {
|
||||||
|
client: Compiler;
|
||||||
|
server: Compiler;
|
||||||
|
serviceworker?: Compiler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function create_compilers(
|
||||||
|
bundler: 'rollup' | 'webpack',
|
||||||
|
cwd: string,
|
||||||
|
src: string,
|
||||||
|
dest: string,
|
||||||
|
dev: boolean
|
||||||
|
): Promise<Compilers> {
|
||||||
|
set_dev(dev);
|
||||||
|
set_src(src);
|
||||||
|
set_dest(dest);
|
||||||
|
|
||||||
|
if (bundler === 'rollup') {
|
||||||
|
const config = await RollupCompiler.load_config(cwd);
|
||||||
|
validate_config(config, 'rollup');
|
||||||
|
|
||||||
|
normalize_rollup_config(config.client);
|
||||||
|
normalize_rollup_config(config.server);
|
||||||
|
|
||||||
|
if (config.serviceworker) {
|
||||||
|
normalize_rollup_config(config.serviceworker);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
client: new RollupCompiler(config.client),
|
||||||
|
server: new RollupCompiler(config.server),
|
||||||
|
serviceworker: config.serviceworker && new RollupCompiler(config.serviceworker)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bundler === 'webpack') {
|
||||||
|
const config = require(path.resolve(cwd, 'webpack.config.js'));
|
||||||
|
validate_config(config, 'webpack');
|
||||||
|
|
||||||
|
return {
|
||||||
|
client: new WebpackCompiler(config.client),
|
||||||
|
server: new WebpackCompiler(config.server),
|
||||||
|
serviceworker: config.serviceworker && new WebpackCompiler(config.serviceworker)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// this shouldn't be possible...
|
||||||
|
throw new Error(`Invalid bundler option '${bundler}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate_config(config: any, bundler: 'rollup' | 'webpack') {
|
||||||
|
if (!config.client || !config.server) {
|
||||||
|
throw new Error(`${bundler}.config.js must export a { client, server, serviceworker? } object`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalize_rollup_config(config: any) {
|
||||||
|
if (typeof config.input === 'string') {
|
||||||
|
config.input = path.normalize(config.input);
|
||||||
|
} else {
|
||||||
|
for (const name in config.input) {
|
||||||
|
config.input[name] = path.normalize(config.input[name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/core/create_compilers/interfaces.ts
Normal file
39
src/core/create_compilers/interfaces.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { ManifestData, Dirs } from '../../interfaces';
|
||||||
|
|
||||||
|
export type Chunk = {
|
||||||
|
file: string;
|
||||||
|
imports: string[];
|
||||||
|
modules: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CssFile = {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class CompileError {
|
||||||
|
file: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompileResult {
|
||||||
|
duration: number;
|
||||||
|
errors: CompileError[];
|
||||||
|
warnings: CompileError[];
|
||||||
|
chunks: Chunk[];
|
||||||
|
assets: Record<string, string>;
|
||||||
|
css_files: CssFile[];
|
||||||
|
|
||||||
|
to_json: (manifest_data: ManifestData, dirs: Dirs) => BuildInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BuildInfo = {
|
||||||
|
bundler: string;
|
||||||
|
shimport: string;
|
||||||
|
assets: Record<string, string>;
|
||||||
|
legacy_assets?: Record<string, string>;
|
||||||
|
css: {
|
||||||
|
main: string | null,
|
||||||
|
chunks: Record<string, string[]>
|
||||||
|
}
|
||||||
|
}
|
||||||
346
src/core/create_manifest_data.ts
Normal file
346
src/core/create_manifest_data.ts
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import svelte from 'svelte/compiler';
|
||||||
|
import { Page, PageComponent, ServerRoute, ManifestData } from '../interfaces';
|
||||||
|
import { posixify, reserved_words } from '../utils';
|
||||||
|
|
||||||
|
const component_extensions = ['.svelte', '.html']; // TODO make this configurable (to include e.g. .svelte.md?)
|
||||||
|
|
||||||
|
export default function create_manifest_data(cwd: string): ManifestData {
|
||||||
|
// TODO remove in a future version
|
||||||
|
if (!fs.existsSync(cwd)) {
|
||||||
|
throw new Error(`As of Sapper 0.21, the routes/ directory should become src/routes/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function has_preload(file: string) {
|
||||||
|
const source = fs.readFileSync(path.join(cwd, file), 'utf-8');
|
||||||
|
|
||||||
|
if (/preload/.test(source)) {
|
||||||
|
try {
|
||||||
|
const { vars } = svelte.compile(source.replace(/<style\b[^>]*>[^]*?<\/style>/g, ''), { generate: false });
|
||||||
|
return vars.some((variable: any) => variable.module && variable.export_name === 'preload');
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const components: PageComponent[] = [];
|
||||||
|
const pages: Page[] = [];
|
||||||
|
const server_routes: ServerRoute[] = [];
|
||||||
|
|
||||||
|
const default_layout: PageComponent = {
|
||||||
|
default: true,
|
||||||
|
type: 'layout',
|
||||||
|
name: '_default_layout',
|
||||||
|
file: null,
|
||||||
|
has_preload: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const default_error: PageComponent = {
|
||||||
|
default: true,
|
||||||
|
type: 'error',
|
||||||
|
name: '_default_error',
|
||||||
|
file: null,
|
||||||
|
has_preload: false
|
||||||
|
};
|
||||||
|
|
||||||
|
function walk(
|
||||||
|
dir: string,
|
||||||
|
parent_segments: Part[][],
|
||||||
|
parent_params: string[],
|
||||||
|
stack: Array<{
|
||||||
|
component: PageComponent,
|
||||||
|
params: string[]
|
||||||
|
}>
|
||||||
|
) {
|
||||||
|
const items = fs.readdirSync(dir)
|
||||||
|
.map(basename => {
|
||||||
|
const resolved = path.join(dir, basename);
|
||||||
|
const file = path.relative(cwd, resolved);
|
||||||
|
const is_dir = fs.statSync(resolved).isDirectory();
|
||||||
|
|
||||||
|
const ext = path.extname(basename);
|
||||||
|
if (!is_dir && !/^\.[a-z]+$/i.test(ext)) return null; // filter out tmp files etc
|
||||||
|
|
||||||
|
const segment = is_dir
|
||||||
|
? basename
|
||||||
|
: basename.slice(0, -path.extname(basename).length);
|
||||||
|
|
||||||
|
const parts = get_parts(segment);
|
||||||
|
const is_index = is_dir ? false : basename.startsWith('index.');
|
||||||
|
const is_page = component_extensions.indexOf(ext) !== -1;
|
||||||
|
|
||||||
|
parts.forEach(part => {
|
||||||
|
if (/\]\[/.test(part.content)) {
|
||||||
|
throw new Error(`Invalid route ${file} — parameters must be separated`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.qualifier && /[\(\)\?\:]/.test(part.qualifier.slice(1, -1))) {
|
||||||
|
throw new Error(`Invalid route ${file} — cannot use (, ), ? or : in route qualifiers`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
basename,
|
||||||
|
ext,
|
||||||
|
parts,
|
||||||
|
file: posixify(file),
|
||||||
|
is_dir,
|
||||||
|
is_index,
|
||||||
|
is_page
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort(comparator);
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
if (item.basename[0] === '_') return;
|
||||||
|
|
||||||
|
if (item.basename[0] === '.') {
|
||||||
|
if (item.file !== '.well-known') return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = parent_segments.slice();
|
||||||
|
|
||||||
|
if (item.is_index && segments.length > 0) {
|
||||||
|
const last_segment = segments[segments.length - 1].slice();
|
||||||
|
const suffix = item.basename
|
||||||
|
.slice(0, -path.extname(item.basename).length).
|
||||||
|
replace('index', '');
|
||||||
|
|
||||||
|
if (suffix) {
|
||||||
|
const last_part = last_segment[last_segment.length - 1];
|
||||||
|
if (last_part.dynamic) {
|
||||||
|
last_segment.push({ dynamic: false, content: suffix });
|
||||||
|
} else {
|
||||||
|
last_segment[last_segment.length - 1] = {
|
||||||
|
dynamic: false,
|
||||||
|
content: `${last_part.content}${suffix}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
segments[segments.length - 1] = last_segment;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
segments.push(item.parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = parent_params.slice();
|
||||||
|
params.push(...item.parts.filter(p => p.dynamic).map(p => p.content));
|
||||||
|
|
||||||
|
if (item.is_dir) {
|
||||||
|
const ext = component_extensions.find((ext: string) => {
|
||||||
|
const index = path.join(dir, item.basename, `_layout${ext}`);
|
||||||
|
return fs.existsSync(index);
|
||||||
|
});
|
||||||
|
|
||||||
|
const component = ext && {
|
||||||
|
name: `${get_slug(item.file)}__layout`,
|
||||||
|
file: `${item.file}/_layout${ext}`,
|
||||||
|
has_preload: has_preload(`${item.file}/_layout${ext}`)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (component) components.push(component);
|
||||||
|
|
||||||
|
walk(
|
||||||
|
path.join(dir, item.basename),
|
||||||
|
segments,
|
||||||
|
params,
|
||||||
|
component
|
||||||
|
? stack.concat({ component, params })
|
||||||
|
: stack.concat(null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (item.is_page) {
|
||||||
|
const is_index = item.basename === `index${item.ext}`;
|
||||||
|
|
||||||
|
const component = {
|
||||||
|
name: get_slug(item.file),
|
||||||
|
file: item.file,
|
||||||
|
has_preload: has_preload(item.file)
|
||||||
|
};
|
||||||
|
|
||||||
|
components.push(component);
|
||||||
|
|
||||||
|
const parts = (is_index && stack[stack.length - 1] === null)
|
||||||
|
? stack.slice(0, -1).concat({ component, params })
|
||||||
|
: stack.concat({ component, params })
|
||||||
|
|
||||||
|
const page = {
|
||||||
|
pattern: get_pattern(is_index ? parent_segments : segments, true),
|
||||||
|
parts
|
||||||
|
};
|
||||||
|
|
||||||
|
pages.push(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
server_routes.push({
|
||||||
|
name: `route_${get_slug(item.file)}`,
|
||||||
|
pattern: get_pattern(segments, false),
|
||||||
|
file: item.file,
|
||||||
|
params: params
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const root_ext = component_extensions.find(ext => fs.existsSync(path.join(cwd, `_layout${ext}`)));
|
||||||
|
const root = root_ext
|
||||||
|
? {
|
||||||
|
name: 'main',
|
||||||
|
file: `_layout${root_ext}`,
|
||||||
|
has_preload: has_preload(`_layout${root_ext}`)
|
||||||
|
}
|
||||||
|
: default_layout;
|
||||||
|
|
||||||
|
const error_ext = component_extensions.find(ext => fs.existsSync(path.join(cwd, `_error${ext}`)));
|
||||||
|
const error = error_ext
|
||||||
|
? {
|
||||||
|
name: 'error',
|
||||||
|
file: `_error${error_ext}`,
|
||||||
|
has_preload: has_preload(`_error${error_ext}`)
|
||||||
|
}
|
||||||
|
: default_error;
|
||||||
|
|
||||||
|
walk(cwd, [], [], []);
|
||||||
|
|
||||||
|
// check for clashes
|
||||||
|
const seen_pages: Map<string, Page> = new Map();
|
||||||
|
pages.forEach(page => {
|
||||||
|
const pattern = page.pattern.toString();
|
||||||
|
if (seen_pages.has(pattern)) {
|
||||||
|
const file = page.parts.pop().component.file;
|
||||||
|
const other_page = seen_pages.get(pattern);
|
||||||
|
const other_file = other_page.parts.pop().component.file;
|
||||||
|
|
||||||
|
throw new Error(`The ${other_file} and ${file} pages clash`);
|
||||||
|
}
|
||||||
|
|
||||||
|
seen_pages.set(pattern, page);
|
||||||
|
});
|
||||||
|
|
||||||
|
const seen_routes: Map<string, ServerRoute> = new Map();
|
||||||
|
server_routes.forEach(route => {
|
||||||
|
const pattern = route.pattern.toString();
|
||||||
|
if (seen_routes.has(pattern)) {
|
||||||
|
const other_route = seen_routes.get(pattern);
|
||||||
|
throw new Error(`The ${other_route.file} and ${route.file} routes clash`);
|
||||||
|
}
|
||||||
|
|
||||||
|
seen_routes.set(pattern, route);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
root,
|
||||||
|
error,
|
||||||
|
components,
|
||||||
|
pages,
|
||||||
|
server_routes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type Part = {
|
||||||
|
content: string;
|
||||||
|
dynamic: boolean;
|
||||||
|
qualifier?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function comparator(
|
||||||
|
a: { basename: string, parts: Part[], file: string, is_index: boolean },
|
||||||
|
b: { basename: string, parts: Part[], file: string, is_index: boolean }
|
||||||
|
) {
|
||||||
|
if (a.is_index !== b.is_index) return a.is_index ? -1 : 1;
|
||||||
|
|
||||||
|
const max = Math.max(a.parts.length, b.parts.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < max; i += 1) {
|
||||||
|
const a_sub_part = a.parts[i];
|
||||||
|
const b_sub_part = b.parts[i];
|
||||||
|
|
||||||
|
if (!a_sub_part) return 1; // b is more specific, so goes first
|
||||||
|
if (!b_sub_part) return -1;
|
||||||
|
|
||||||
|
if (a_sub_part.dynamic !== b_sub_part.dynamic) {
|
||||||
|
return a_sub_part.dynamic ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!a_sub_part.dynamic && a_sub_part.content !== b_sub_part.content) {
|
||||||
|
return (
|
||||||
|
(b_sub_part.content.length - a_sub_part.content.length) ||
|
||||||
|
(a_sub_part.content < b_sub_part.content ? -1 : 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If both parts dynamic, check for regexp patterns
|
||||||
|
if (a_sub_part.dynamic && b_sub_part.dynamic) {
|
||||||
|
const regexp_pattern = /\((.*?)\)/;
|
||||||
|
const a_match = regexp_pattern.exec(a_sub_part.content);
|
||||||
|
const b_match = regexp_pattern.exec(b_sub_part.content);
|
||||||
|
|
||||||
|
if (!a_match && b_match) {
|
||||||
|
return 1; // No regexp, so less specific than b
|
||||||
|
}
|
||||||
|
if (!b_match && a_match) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a_match && b_match && a_match[1] !== b_match[1]) {
|
||||||
|
return b_match[1].length - a_match[1].length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_parts(part: string): Part[] {
|
||||||
|
return part.split(/\[(.+)\]/)
|
||||||
|
.map((str, i) => {
|
||||||
|
if (!str) return null;
|
||||||
|
const dynamic = i % 2 === 1;
|
||||||
|
|
||||||
|
const [, content, qualifier] = dynamic
|
||||||
|
? /([^(]+)(\(.+\))?$/.exec(str)
|
||||||
|
: [, str, null];
|
||||||
|
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
dynamic,
|
||||||
|
qualifier
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_slug(file: string) {
|
||||||
|
let name = file
|
||||||
|
.replace(/[\\\/]index/, '')
|
||||||
|
.replace(/_default([\/\\index])?\.html$/, 'index')
|
||||||
|
.replace(/[\/\\]/g, '_')
|
||||||
|
.replace(/\.\w+$/, '')
|
||||||
|
.replace(/\[([^(]+)(?:\([^(]+\))?\]/, '$$$1')
|
||||||
|
.replace(/[^a-zA-Z0-9_$]/g, c => {
|
||||||
|
return c === '.' ? '_' : `$${c.charCodeAt(0)}`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (reserved_words.has(name)) name += '_';
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_pattern(segments: Part[][], add_trailing_slash: boolean) {
|
||||||
|
return new RegExp(
|
||||||
|
`^` +
|
||||||
|
segments.map(segment => {
|
||||||
|
return '\\/' + segment.map(part => {
|
||||||
|
return part.dynamic
|
||||||
|
? part.qualifier || '([^\\/]+?)'
|
||||||
|
: encodeURI(part.content.normalize())
|
||||||
|
.replace(/\?/g, '%3F')
|
||||||
|
.replace(/#/g, '%23')
|
||||||
|
.replace(/%5B/g, '[')
|
||||||
|
.replace(/%5D/g, ']');
|
||||||
|
}).join('');
|
||||||
|
}).join('') +
|
||||||
|
(add_trailing_slash ? '\\\/?$' : '$')
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as glob from 'glob';
|
|
||||||
import create_routes from './create_routes';
|
|
||||||
import { posixify, write_if_changed } from './utils';
|
|
||||||
import { dev, locations } from '../config';
|
|
||||||
import { Route } from '../interfaces';
|
|
||||||
|
|
||||||
export function create_main_manifests({ routes, dev_port }: {
|
|
||||||
routes: Route[];
|
|
||||||
dev_port?: number;
|
|
||||||
}) {
|
|
||||||
const path_to_routes = path.relative(`${locations.app()}/manifest`, locations.routes());
|
|
||||||
|
|
||||||
const client_manifest = generate_client(routes, path_to_routes, dev_port);
|
|
||||||
const server_manifest = generate_server(routes, path_to_routes);
|
|
||||||
|
|
||||||
write_if_changed(`${locations.app()}/manifest/client.js`, client_manifest);
|
|
||||||
write_if_changed(`${locations.app()}/manifest/server.js`, server_manifest);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function create_serviceworker_manifest({ routes, client_files }: {
|
|
||||||
routes: Route[];
|
|
||||||
client_files: string[];
|
|
||||||
}) {
|
|
||||||
const assets = glob.sync('**', { cwd: 'assets', nodir: true });
|
|
||||||
|
|
||||||
let code = `
|
|
||||||
// This file is generated by Sapper — do not edit it!
|
|
||||||
export const timestamp = ${Date.now()};
|
|
||||||
|
|
||||||
export const assets = [\n\t${assets.map((x: string) => `"${x}"`).join(',\n\t')}\n];
|
|
||||||
|
|
||||||
export const shell = [\n\t${client_files.map((x: string) => `"${x}"`).join(',\n\t')}\n];
|
|
||||||
|
|
||||||
export const routes = [\n\t${routes.filter((r: Route) => r.type === 'page' && !/^_[45]xx$/.test(r.id)).map((r: Route) => `{ pattern: ${r.pattern} }`).join(',\n\t')}\n];
|
|
||||||
`.replace(/^\t\t/gm, '').trim();
|
|
||||||
|
|
||||||
write_if_changed(`${locations.app()}/manifest/service-worker.js`, code);
|
|
||||||
}
|
|
||||||
|
|
||||||
function generate_client(routes: Route[], path_to_routes: string, dev_port?: number) {
|
|
||||||
let code = `
|
|
||||||
// This file is generated by Sapper — do not edit it!
|
|
||||||
export const routes = [
|
|
||||||
${routes
|
|
||||||
.map(route => {
|
|
||||||
if (route.type !== 'page') {
|
|
||||||
return `{ pattern: ${route.pattern}, ignore: true }`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = posixify(`${path_to_routes}/${route.file}`);
|
|
||||||
|
|
||||||
if (route.id === '_4xx' || route.id === '_5xx') {
|
|
||||||
return `{ error: '${route.id.slice(1)}', load: () => import(/* webpackChunkName: "${route.id}" */ '${file}') }`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = route.params.length === 0
|
|
||||||
? '{}'
|
|
||||||
: `{ ${route.params.map((part, i) => `${part}: match[${i + 1}]`).join(', ')} }`;
|
|
||||||
|
|
||||||
return `{ pattern: ${route.pattern}, params: ${route.params.length > 0 ? `match` : `()`} => (${params}), load: () => import(/* webpackChunkName: "${route.id}" */ '${file}') }`;
|
|
||||||
})
|
|
||||||
.join(',\n\t')}
|
|
||||||
];`.replace(/^\t\t/gm, '').trim();
|
|
||||||
|
|
||||||
if (dev()) {
|
|
||||||
const sapper_dev_client = posixify(
|
|
||||||
path.resolve(__dirname, '../sapper-dev-client.js')
|
|
||||||
);
|
|
||||||
|
|
||||||
code += `
|
|
||||||
|
|
||||||
if (module.hot) {
|
|
||||||
import('${sapper_dev_client}').then(client => {
|
|
||||||
client.connect(${dev_port});
|
|
||||||
});
|
|
||||||
}`.replace(/^\t{3}/gm, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generate_server(routes: Route[], path_to_routes: string) {
|
|
||||||
let code = `
|
|
||||||
// This file is generated by Sapper — do not edit it!
|
|
||||||
${routes
|
|
||||||
.map(route => {
|
|
||||||
const file = posixify(`${path_to_routes}/${route.file}`);
|
|
||||||
return route.type === 'page'
|
|
||||||
? `import ${route.id} from '${file}';`
|
|
||||||
: `import * as ${route.id} from '${file}';`;
|
|
||||||
})
|
|
||||||
.join('\n')}
|
|
||||||
|
|
||||||
export const routes = [
|
|
||||||
${routes
|
|
||||||
.map(route => {
|
|
||||||
const file = posixify(`../../${route.file}`);
|
|
||||||
|
|
||||||
if (route.id === '_4xx' || route.id === '_5xx') {
|
|
||||||
return `{ error: '${route.id.slice(1)}', module: ${route.id} }`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = route.params.length === 0
|
|
||||||
? '{}'
|
|
||||||
: `{ ${route.params.map((part, i) => `${part}: match[${i + 1}]`).join(', ')} }`;
|
|
||||||
|
|
||||||
return `{ id: '${route.id}', type: '${route.type}', pattern: ${route.pattern}, params: ${route.params.length > 0 ? `match` : `()`} => (${params}), module: ${route.id} }`;
|
|
||||||
})
|
|
||||||
.join(',\n\t')
|
|
||||||
}
|
|
||||||
];`.replace(/^\t\t/gm, '').trim();
|
|
||||||
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
import * as path from 'path';
|
|
||||||
import glob from 'glob';
|
|
||||||
import { locations } from '../config';
|
|
||||||
import { Route } from '../interfaces';
|
|
||||||
|
|
||||||
export default function create_routes({ files } = { files: glob.sync('**/*.*', { cwd: locations.routes(), nodir: true }) }) {
|
|
||||||
const routes: Route[] = files
|
|
||||||
.map((file: string) => {
|
|
||||||
if (/(^|\/|\\)_/.test(file)) return;
|
|
||||||
|
|
||||||
if (/]\[/.test(file)) {
|
|
||||||
throw new Error(`Invalid route ${file} — parameters must be separated`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const base = file.replace(/\.[^/.]+$/, '');
|
|
||||||
const parts = base.split('/'); // glob output is always posix-style
|
|
||||||
if (parts[parts.length - 1] === 'index') parts.pop();
|
|
||||||
|
|
||||||
const id = (
|
|
||||||
parts.join('_').replace(/[[\]]/g, '$').replace(/^\d/, '_$&').replace(/[^a-zA-Z0-9_$]/g, '_')
|
|
||||||
) || '_';
|
|
||||||
|
|
||||||
const params: string[] = [];
|
|
||||||
const param_pattern = /\[([^\]]+)\]/g;
|
|
||||||
let match;
|
|
||||||
while (match = param_pattern.exec(base)) {
|
|
||||||
params.push(match[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO can we do all this with sub-parts? or does
|
|
||||||
// nesting make that impossible?
|
|
||||||
let pattern_string = '';
|
|
||||||
let i = parts.length;
|
|
||||||
let nested = true;
|
|
||||||
while (i--) {
|
|
||||||
const part = encodeURIComponent(parts[i].normalize()).replace(/%5B/g, '[').replace(/%5D/g, ']');
|
|
||||||
const dynamic = ~part.indexOf('[');
|
|
||||||
|
|
||||||
if (dynamic) {
|
|
||||||
const matcher = part.replace(param_pattern, `([^\/]+?)`);
|
|
||||||
pattern_string = nested ? `(?:\\/${matcher}${pattern_string})?` : `\\/${matcher}${pattern_string}`;
|
|
||||||
} else {
|
|
||||||
nested = false;
|
|
||||||
pattern_string = `\\/${part}${pattern_string}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pattern = new RegExp(`^${pattern_string}\\/?$`);
|
|
||||||
|
|
||||||
const test = (url: string) => pattern.test(url);
|
|
||||||
|
|
||||||
const exec = (url: string) => {
|
|
||||||
const match = pattern.exec(url);
|
|
||||||
if (!match) return;
|
|
||||||
|
|
||||||
const result: Record<string, string> = {};
|
|
||||||
params.forEach((param, i) => {
|
|
||||||
result[param] = match[i + 1];
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
type: path.extname(file) === '.html' ? 'page' : 'route',
|
|
||||||
file,
|
|
||||||
pattern,
|
|
||||||
test,
|
|
||||||
exec,
|
|
||||||
parts,
|
|
||||||
params
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter(Boolean)
|
|
||||||
.sort((a: Route, b: Route) => {
|
|
||||||
if (a.file === '4xx.html' || a.file === '5xx.html') return -1;
|
|
||||||
if (b.file === '4xx.html' || b.file === '5xx.html') return 1;
|
|
||||||
|
|
||||||
const max = Math.max(a.parts.length, b.parts.length);
|
|
||||||
|
|
||||||
for (let i = 0; i < max; i += 1) {
|
|
||||||
const a_part = a.parts[i];
|
|
||||||
const b_part = b.parts[i];
|
|
||||||
|
|
||||||
if (!a_part) return -1;
|
|
||||||
if (!b_part) return 1;
|
|
||||||
|
|
||||||
const a_sub_parts = get_sub_parts(a_part);
|
|
||||||
const b_sub_parts = get_sub_parts(b_part);
|
|
||||||
const max = Math.max(a_sub_parts.length, b_sub_parts.length);
|
|
||||||
|
|
||||||
for (let i = 0; i < max; i += 1) {
|
|
||||||
const a_sub_part = a_sub_parts[i];
|
|
||||||
const b_sub_part = b_sub_parts[i];
|
|
||||||
|
|
||||||
if (!a_sub_part) return 1; // b is more specific, so goes first
|
|
||||||
if (!b_sub_part) return -1;
|
|
||||||
|
|
||||||
if (a_sub_part.dynamic !== b_sub_part.dynamic) {
|
|
||||||
return a_sub_part.dynamic ? 1 : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!a_sub_part.dynamic && a_sub_part.content !== b_sub_part.content) {
|
|
||||||
return b_sub_part.content.length - a_sub_part.content.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`The ${a.file} and ${b.file} routes clash`);
|
|
||||||
});
|
|
||||||
|
|
||||||
return routes;
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_sub_parts(part: string) {
|
|
||||||
return part.split(/[\[\]]/)
|
|
||||||
.map((content, i) => {
|
|
||||||
if (!content) return null;
|
|
||||||
return {
|
|
||||||
content,
|
|
||||||
dynamic: i % 2 === 1
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
}
|
|
||||||
16
src/core/read_template.ts
Normal file
16
src/core/read_template.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
export default function read_template(dir: string) {
|
||||||
|
try {
|
||||||
|
return fs.readFileSync(`${dir}/template.html`, 'utf-8');
|
||||||
|
} catch (err) {
|
||||||
|
if (fs.existsSync(`app/template.html`)) {
|
||||||
|
throw new Error(`As of Sapper 0.21, the default folder structure has been changed:
|
||||||
|
app/ --> src/
|
||||||
|
routes/ --> src/routes/
|
||||||
|
assets/ --> static/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import * as sander from 'sander';
|
|
||||||
|
|
||||||
const previous_contents = new Map();
|
|
||||||
|
|
||||||
export function write_if_changed(file: string, code: string) {
|
|
||||||
if (code !== previous_contents.get(file)) {
|
|
||||||
previous_contents.set(file, code);
|
|
||||||
sander.writeFileSync(file, code);
|
|
||||||
fudge_mtime(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function posixify(file: string) {
|
|
||||||
return file.replace(/[/\\]/g, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fudge_mtime(file: string) {
|
|
||||||
// need to fudge the mtime so that webpack doesn't go doolally
|
|
||||||
const { atime, mtime } = sander.statSync(file);
|
|
||||||
sander.utimesSync(
|
|
||||||
file,
|
|
||||||
new Date(atime.getTime() - 999999),
|
|
||||||
new Date(mtime.getTime() - 999999)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
|
import * as child_process from 'child_process';
|
||||||
|
import { CompileResult } from './core/create_compilers/interfaces';
|
||||||
|
|
||||||
export type Route = {
|
export type Route = {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'page' | 'route';
|
handlers: {
|
||||||
file: string;
|
type: 'page' | 'route';
|
||||||
|
file: string;
|
||||||
|
}[];
|
||||||
pattern: RegExp;
|
pattern: RegExp;
|
||||||
test: (url: string) => boolean;
|
test: (url: string) => boolean;
|
||||||
exec: (url: string) => Record<string, string>;
|
exec: (url: string) => Record<string, string>;
|
||||||
@@ -12,4 +17,89 @@ export type Route = {
|
|||||||
export type Template = {
|
export type Template = {
|
||||||
render: (data: Record<string, string>) => string;
|
render: (data: Record<string, string>) => string;
|
||||||
stream: (req, res, data: Record<string, string | Promise<string>>) => void;
|
stream: (req, res, data: Record<string, string | Promise<string>>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WritableStore<T> = {
|
||||||
|
set: (value: T) => void;
|
||||||
|
update: (fn: (value: T) => T) => void;
|
||||||
|
subscribe: (fn: (T: any) => void) => () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PageComponent = {
|
||||||
|
default?: boolean;
|
||||||
|
type?: string;
|
||||||
|
name: string;
|
||||||
|
file: string;
|
||||||
|
has_preload: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Page = {
|
||||||
|
pattern: RegExp;
|
||||||
|
parts: Array<{
|
||||||
|
component: PageComponent;
|
||||||
|
params: string[];
|
||||||
|
}>
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ServerRoute = {
|
||||||
|
name: string;
|
||||||
|
pattern: RegExp;
|
||||||
|
file: string;
|
||||||
|
params: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Dirs = {
|
||||||
|
dest: string,
|
||||||
|
src: string,
|
||||||
|
routes: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ManifestData = {
|
||||||
|
root: PageComponent;
|
||||||
|
error: PageComponent;
|
||||||
|
components: PageComponent[];
|
||||||
|
pages: Page[];
|
||||||
|
server_routes: ServerRoute[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReadyEvent = {
|
||||||
|
port: number;
|
||||||
|
process: child_process.ChildProcess;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ErrorEvent = {
|
||||||
|
type: string;
|
||||||
|
error: Error;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FatalEvent = {
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InvalidEvent = {
|
||||||
|
changed: string[];
|
||||||
|
invalid: {
|
||||||
|
client: boolean;
|
||||||
|
server: boolean;
|
||||||
|
serviceworker: boolean;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BuildEvent = {
|
||||||
|
type: string;
|
||||||
|
errors: Array<{ file: string, message: string, duplicate: boolean }>;
|
||||||
|
warnings: Array<{ file: string, message: string, duplicate: boolean }>;
|
||||||
|
duration: number;
|
||||||
|
result: CompileResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FileEvent = {
|
||||||
|
file: string;
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FailureEvent = {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DoneEvent = {};
|
||||||
@@ -1,338 +0,0 @@
|
|||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import { ClientRequest, ServerResponse } from 'http';
|
|
||||||
import mkdirp from 'mkdirp';
|
|
||||||
import rimraf from 'rimraf';
|
|
||||||
import devalue from 'devalue';
|
|
||||||
import { lookup } from './middleware/mime';
|
|
||||||
import { create_routes, create_compilers } from './core';
|
|
||||||
import { locations, dev } from './config';
|
|
||||||
import { Route, Template } from './interfaces';
|
|
||||||
import sourceMapSupport from 'source-map-support';
|
|
||||||
|
|
||||||
sourceMapSupport.install();
|
|
||||||
|
|
||||||
type RouteObject = {
|
|
||||||
id: string;
|
|
||||||
type: 'page' | 'route';
|
|
||||||
pattern: RegExp;
|
|
||||||
params: (match: RegExpMatchArray) => Record<string, string>;
|
|
||||||
module: {
|
|
||||||
render: (data: any) => {
|
|
||||||
head: string;
|
|
||||||
css: { code: string, map: any };
|
|
||||||
html: string
|
|
||||||
},
|
|
||||||
preload: (data: any) => any | Promise<any>
|
|
||||||
};
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Handler = (req: Req, res: ServerResponse, next: () => void) => void;
|
|
||||||
|
|
||||||
interface Req extends ClientRequest {
|
|
||||||
url: string;
|
|
||||||
method: string;
|
|
||||||
pathname: string;
|
|
||||||
params: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function middleware({ routes }: {
|
|
||||||
routes: RouteObject[]
|
|
||||||
}) {
|
|
||||||
const output = locations.dest();
|
|
||||||
|
|
||||||
const client_info = JSON.parse(fs.readFileSync(path.join(output, 'client_info.json'), 'utf-8'));
|
|
||||||
|
|
||||||
const middleware = compose_handlers([
|
|
||||||
(req: Req, res: ServerResponse, next: () => void) => {
|
|
||||||
req.pathname = req.url.replace(/\?.*/, '');
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
|
|
||||||
fs.existsSync(path.join(output, 'index.html')) && serve({
|
|
||||||
pathname: '/index.html',
|
|
||||||
cache_control: 'max-age=600'
|
|
||||||
}),
|
|
||||||
|
|
||||||
fs.existsSync(path.join(output, 'service-worker.js')) && serve({
|
|
||||||
pathname: '/service-worker.js',
|
|
||||||
cache_control: 'max-age=600'
|
|
||||||
}),
|
|
||||||
|
|
||||||
serve({
|
|
||||||
prefix: '/client/',
|
|
||||||
cache_control: 'max-age=31536000'
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_route_handler(client_info.assetsByChunkName, routes)
|
|
||||||
].filter(Boolean));
|
|
||||||
|
|
||||||
return middleware;
|
|
||||||
}
|
|
||||||
|
|
||||||
function serve({ prefix, pathname, cache_control }: {
|
|
||||||
prefix?: string,
|
|
||||||
pathname?: string,
|
|
||||||
cache_control: string
|
|
||||||
}) {
|
|
||||||
const filter = pathname
|
|
||||||
? (req: Req) => req.pathname === pathname
|
|
||||||
: (req: Req) => req.pathname.startsWith(prefix);
|
|
||||||
|
|
||||||
const output = locations.dest();
|
|
||||||
|
|
||||||
const cache: Map<string, Buffer> = new Map();
|
|
||||||
|
|
||||||
const read = dev()
|
|
||||||
? (file: string) => fs.readFileSync(path.resolve(output, file))
|
|
||||||
: (file: string) => (cache.has(file) ? cache : cache.set(file, fs.readFileSync(path.resolve(output, file)))).get(file)
|
|
||||||
|
|
||||||
return (req: Req, res: ServerResponse, next: () => void) => {
|
|
||||||
if (filter(req)) {
|
|
||||||
const type = lookup(req.pathname);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = read(req.pathname.slice(1));
|
|
||||||
|
|
||||||
res.setHeader('Content-Type', type);
|
|
||||||
res.setHeader('Cache-Control', cache_control);
|
|
||||||
res.end(data);
|
|
||||||
} catch (err) {
|
|
||||||
res.statusCode = 404;
|
|
||||||
res.end('not found');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolved = Promise.resolve();
|
|
||||||
|
|
||||||
function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]) {
|
|
||||||
const template = dev()
|
|
||||||
? () => fs.readFileSync(`${locations.app()}/template.html`, 'utf-8')
|
|
||||||
: (str => () => str)(fs.readFileSync(`${locations.dest()}/template.html`, 'utf-8'));
|
|
||||||
|
|
||||||
function handle_route(route: RouteObject, req: Req, res: ServerResponse) {
|
|
||||||
req.params = route.params(route.pattern.exec(req.pathname));
|
|
||||||
|
|
||||||
const mod = route.module;
|
|
||||||
|
|
||||||
if (route.type === 'page') {
|
|
||||||
res.setHeader('Content-Type', 'text/html');
|
|
||||||
|
|
||||||
// preload main.js and current route
|
|
||||||
// TODO detect other stuff we can preload? images, CSS, fonts?
|
|
||||||
const link = []
|
|
||||||
.concat(chunks.main, chunks[route.id])
|
|
||||||
.map(file => `</client/${file}>;rel="preload";as="script"`)
|
|
||||||
.join(', ');
|
|
||||||
|
|
||||||
res.setHeader('Link', link);
|
|
||||||
|
|
||||||
const data = { params: req.params, query: req.query };
|
|
||||||
|
|
||||||
let redirect: { statusCode: number, location: string };
|
|
||||||
let error: { statusCode: number, message: Error | string };
|
|
||||||
|
|
||||||
Promise.resolve(
|
|
||||||
mod.preload ? mod.preload.call({
|
|
||||||
redirect: (statusCode: number, location: string) => {
|
|
||||||
redirect = { statusCode, location };
|
|
||||||
},
|
|
||||||
error: (statusCode: number, message: Error | string) => {
|
|
||||||
error = { statusCode, message };
|
|
||||||
}
|
|
||||||
}, req) : {}
|
|
||||||
).catch(err => {
|
|
||||||
error = { statusCode: 500, message: err };
|
|
||||||
}).then(preloaded => {
|
|
||||||
if (redirect) {
|
|
||||||
res.statusCode = redirect.statusCode;
|
|
||||||
res.setHeader('Location', redirect.location);
|
|
||||||
res.end();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
handle_error(req, res, error.statusCode, error.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const serialized = try_serialize(preloaded); // TODO bail on non-POJOs
|
|
||||||
Object.assign(data, preloaded);
|
|
||||||
|
|
||||||
const { html, head, css } = mod.render(data);
|
|
||||||
|
|
||||||
let scripts = []
|
|
||||||
.concat(chunks.main) // chunks main might be an array. it might not! thanks, webpack
|
|
||||||
.map(file => `<script src='/client/${file}'></script>`)
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
scripts = `<script>__SAPPER__ = { preloaded: ${serialized} };</script>${scripts}`;
|
|
||||||
|
|
||||||
const page = template()
|
|
||||||
.replace('%sapper.scripts%', scripts)
|
|
||||||
.replace('%sapper.html%', html)
|
|
||||||
.replace('%sapper.head%', `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`)
|
|
||||||
.replace('%sapper.styles%', (css && css.code ? `<style>${css.code}</style>` : ''));
|
|
||||||
|
|
||||||
res.end(page);
|
|
||||||
|
|
||||||
if (process.send) {
|
|
||||||
process.send({
|
|
||||||
__sapper__: true,
|
|
||||||
url: req.url,
|
|
||||||
method: req.method,
|
|
||||||
status: 200,
|
|
||||||
type: 'text/html',
|
|
||||||
body: page
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
const method = req.method.toLowerCase();
|
|
||||||
// 'delete' cannot be exported from a module because it is a keyword,
|
|
||||||
// so check for 'del' instead
|
|
||||||
const method_export = method === 'delete' ? 'del' : method;
|
|
||||||
const handler = mod[method_export];
|
|
||||||
if (handler) {
|
|
||||||
if (process.env.SAPPER_EXPORT) {
|
|
||||||
const { write, end, setHeader } = res;
|
|
||||||
const chunks: any[] = [];
|
|
||||||
const headers: Record<string, string> = {};
|
|
||||||
|
|
||||||
// intercept data so that it can be exported
|
|
||||||
res.write = function(chunk: any) {
|
|
||||||
chunks.push(new Buffer(chunk));
|
|
||||||
write.apply(res, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
res.setHeader = function(name: string, value: string) {
|
|
||||||
headers[name.toLowerCase()] = value;
|
|
||||||
setHeader.apply(res, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
res.end = function(chunk?: any) {
|
|
||||||
if (chunk) chunks.push(new Buffer(chunk));
|
|
||||||
end.apply(res, arguments);
|
|
||||||
|
|
||||||
process.send({
|
|
||||||
__sapper__: true,
|
|
||||||
url: req.url,
|
|
||||||
method: req.method,
|
|
||||||
status: res.statusCode,
|
|
||||||
type: headers['content-type'],
|
|
||||||
body: Buffer.concat(chunks).toString()
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const handle_bad_result = (err?: Error) => {
|
|
||||||
if (err) {
|
|
||||||
console.error(err.stack);
|
|
||||||
res.statusCode = 500;
|
|
||||||
res.end(err.message);
|
|
||||||
} else {
|
|
||||||
handle_error(req, res, 404, 'Not found');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
handler(req, res, handle_bad_result);
|
|
||||||
} catch (err) {
|
|
||||||
handle_bad_result(err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// no matching handler for method — 404
|
|
||||||
handle_error(req, res, 404, 'Not found');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const not_found_route = routes.find((route: RouteObject) => route.error === '4xx');
|
|
||||||
const error_route = routes.find((route: RouteObject) => route.error === '5xx');
|
|
||||||
|
|
||||||
function handle_error(req: Req, res: ServerResponse, statusCode: number, message: Error | string) {
|
|
||||||
res.statusCode = statusCode;
|
|
||||||
res.setHeader('Content-Type', 'text/html');
|
|
||||||
|
|
||||||
const error = message instanceof Error ? message : new Error(message);
|
|
||||||
|
|
||||||
const not_found = statusCode >= 400 && statusCode < 500;
|
|
||||||
|
|
||||||
const route = not_found
|
|
||||||
? not_found_route
|
|
||||||
: error_route;
|
|
||||||
|
|
||||||
const title: string = not_found
|
|
||||||
? 'Not found'
|
|
||||||
: `Internal server error: ${error.message}`;
|
|
||||||
|
|
||||||
const rendered = route ? route.module.render({
|
|
||||||
status: statusCode,
|
|
||||||
error
|
|
||||||
}) : { head: '', css: null, html: title };
|
|
||||||
|
|
||||||
const { head, css, html } = rendered;
|
|
||||||
|
|
||||||
const page = template()
|
|
||||||
.replace('%sapper.scripts%', `<script src='/client/${chunks.main}'></script>`)
|
|
||||||
.replace('%sapper.html%', html)
|
|
||||||
.replace('%sapper.head%', `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`)
|
|
||||||
.replace('%sapper.styles%', (css && css.code ? `<style>${css.code}</style>` : ''));
|
|
||||||
|
|
||||||
res.end(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
return function find_route(req: Req, res: ServerResponse) {
|
|
||||||
const url = req.pathname;
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const route of routes) {
|
|
||||||
if (!route.error && route.pattern.test(url)) return handle_route(route, req, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
handle_error(req, res, 404, 'Not found');
|
|
||||||
} catch (error) {
|
|
||||||
handle_error(req, res, 500, error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function compose_handlers(handlers: Handler[]) {
|
|
||||||
return (req: Req, res: ServerResponse, next: () => void) => {
|
|
||||||
let i = 0;
|
|
||||||
function go() {
|
|
||||||
const handler = handlers[i];
|
|
||||||
|
|
||||||
if (handler) {
|
|
||||||
handler(req, res, () => {
|
|
||||||
i += 1;
|
|
||||||
go();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
go();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function read_json(file: string) {
|
|
||||||
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function try_serialize(data: any) {
|
|
||||||
try {
|
|
||||||
return devalue(data);
|
|
||||||
} catch (err) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,287 +0,0 @@
|
|||||||
import { detach, findAnchor, scroll_state, which } from './utils';
|
|
||||||
import { Component, ComponentConstructor, Params, Query, Route, RouteData, ScrollPosition, Target } from './interfaces';
|
|
||||||
|
|
||||||
export let component: Component;
|
|
||||||
let target: Node;
|
|
||||||
let routes: Route[];
|
|
||||||
let errors: { '4xx': Route, '5xx': Route };
|
|
||||||
|
|
||||||
const history = typeof window !== 'undefined' ? window.history : {
|
|
||||||
pushState: (state: any, title: string, href: string) => {},
|
|
||||||
replaceState: (state: any, title: string, href: string) => {},
|
|
||||||
scrollRestoration: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
const scroll_history: Record<string, ScrollPosition> = {};
|
|
||||||
let uid = 1;
|
|
||||||
let cid: number;
|
|
||||||
|
|
||||||
if ('scrollRestoration' in history) {
|
|
||||||
history.scrollRestoration = 'manual';
|
|
||||||
}
|
|
||||||
|
|
||||||
function select_route(url: URL): Target {
|
|
||||||
if (url.origin !== window.location.origin) return null;
|
|
||||||
|
|
||||||
for (const route of routes) {
|
|
||||||
const match = route.pattern.exec(url.pathname);
|
|
||||||
if (match) {
|
|
||||||
if (route.ignore) return null;
|
|
||||||
|
|
||||||
const params = route.params(match);
|
|
||||||
|
|
||||||
const query: Record<string, string | true> = {};
|
|
||||||
if (url.search.length > 0) {
|
|
||||||
url.search.slice(1).split('&').forEach(searchParam => {
|
|
||||||
const [, key, value] = /([^=]+)=(.*)/.exec(searchParam);
|
|
||||||
query[key] = value || true;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return { url, route, data: { params, query } };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let current_token: {};
|
|
||||||
|
|
||||||
function render(Component: ComponentConstructor, data: any, scroll: ScrollPosition, token: {}) {
|
|
||||||
if (current_token !== token) return;
|
|
||||||
|
|
||||||
if (component) {
|
|
||||||
component.destroy();
|
|
||||||
} else {
|
|
||||||
// first load — remove SSR'd <head> contents
|
|
||||||
const start = document.querySelector('#sapper-head-start');
|
|
||||||
const end = document.querySelector('#sapper-head-end');
|
|
||||||
|
|
||||||
if (start && end) {
|
|
||||||
while (start.nextSibling !== end) detach(start.nextSibling);
|
|
||||||
detach(start);
|
|
||||||
detach(end);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
component = new Component({
|
|
||||||
target,
|
|
||||||
data,
|
|
||||||
hydrate: !component
|
|
||||||
});
|
|
||||||
|
|
||||||
if (scroll) {
|
|
||||||
window.scrollTo(scroll.x, scroll.y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepare_route(Component: ComponentConstructor, data: RouteData) {
|
|
||||||
let redirect: { statusCode: number, location: string } = null;
|
|
||||||
let error: { statusCode: number, message: Error | string } = null;
|
|
||||||
|
|
||||||
if (!Component.preload) {
|
|
||||||
return { Component, data, redirect, error };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!component && window.__SAPPER__ && window.__SAPPER__.preloaded) {
|
|
||||||
return { Component, data: Object.assign(data, window.__SAPPER__.preloaded), redirect, error };
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(Component.preload.call({
|
|
||||||
redirect: (statusCode: number, location: string) => {
|
|
||||||
redirect = { statusCode, location };
|
|
||||||
},
|
|
||||||
error: (statusCode: number, message: Error | string) => {
|
|
||||||
error = { statusCode, message };
|
|
||||||
}
|
|
||||||
}, data)).catch(err => {
|
|
||||||
error = { statusCode: 500, message: err };
|
|
||||||
}).then(preloaded => {
|
|
||||||
if (error) {
|
|
||||||
const route = error.statusCode >= 400 && error.statusCode < 500
|
|
||||||
? errors['4xx']
|
|
||||||
: errors['5xx'];
|
|
||||||
|
|
||||||
return route.load().then(({ default: Component }: { default: ComponentConstructor }) => {
|
|
||||||
const err = error.message instanceof Error ? error.message : new Error(error.message);
|
|
||||||
Object.assign(data, { status: error.statusCode, error: err });
|
|
||||||
return { Component, data, redirect: null };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(data, preloaded)
|
|
||||||
return { Component, data, redirect };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigate(target: Target, id: number) {
|
|
||||||
if (id) {
|
|
||||||
// popstate or initial navigation
|
|
||||||
cid = id;
|
|
||||||
} else {
|
|
||||||
// clicked on a link. preserve scroll state
|
|
||||||
scroll_history[cid] = scroll_state();
|
|
||||||
|
|
||||||
id = cid = ++uid;
|
|
||||||
scroll_history[cid] = { x: 0, y: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
cid = id;
|
|
||||||
|
|
||||||
const loaded = prefetching && prefetching.href === target.url.href ?
|
|
||||||
prefetching.promise :
|
|
||||||
target.route.load().then(mod => prepare_route(mod.default, target.data));
|
|
||||||
|
|
||||||
prefetching = null;
|
|
||||||
|
|
||||||
const token = current_token = {};
|
|
||||||
|
|
||||||
return loaded.then(({ Component, data, redirect }) => {
|
|
||||||
if (redirect) {
|
|
||||||
return goto(redirect.location, { replaceState: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
render(Component, data, scroll_history[id], token);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handle_click(event: MouseEvent) {
|
|
||||||
// Adapted from https://github.com/visionmedia/page.js
|
|
||||||
// MIT license https://github.com/visionmedia/page.js#license
|
|
||||||
if (which(event) !== 1) return;
|
|
||||||
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
|
|
||||||
if (event.defaultPrevented) return;
|
|
||||||
|
|
||||||
const a: HTMLAnchorElement | SVGAElement = <HTMLAnchorElement | SVGAElement>findAnchor(<Node>event.target);
|
|
||||||
if (!a) return;
|
|
||||||
|
|
||||||
// check if link is inside an svg
|
|
||||||
// in this case, both href and target are always inside an object
|
|
||||||
const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
|
|
||||||
const href = String(svg ? (<SVGAElement>a).href.baseVal : a.href);
|
|
||||||
|
|
||||||
if (href === window.location.href) {
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore if tag has
|
|
||||||
// 1. 'download' attribute
|
|
||||||
// 2. rel='external' attribute
|
|
||||||
if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
|
|
||||||
|
|
||||||
// Ignore if <a> has a target
|
|
||||||
if (svg ? (<SVGAElement>a).target.baseVal : a.target) return;
|
|
||||||
|
|
||||||
const url = new URL(href);
|
|
||||||
|
|
||||||
// Don't handle hash changes
|
|
||||||
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
|
|
||||||
|
|
||||||
const target = select_route(url);
|
|
||||||
if (target) {
|
|
||||||
navigate(target, null);
|
|
||||||
event.preventDefault();
|
|
||||||
history.pushState({ id: cid }, '', url.href);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handle_popstate(event: PopStateEvent) {
|
|
||||||
scroll_history[cid] = scroll_state();
|
|
||||||
|
|
||||||
if (event.state) {
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
const target = select_route(url);
|
|
||||||
navigate(target, event.state.id);
|
|
||||||
} else {
|
|
||||||
// hashchange
|
|
||||||
cid = ++uid;
|
|
||||||
history.replaceState({ id: cid }, '', window.location.href);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let prefetching: {
|
|
||||||
href: string;
|
|
||||||
promise: Promise<{ Component: ComponentConstructor, data: any }>;
|
|
||||||
} = null;
|
|
||||||
|
|
||||||
export function prefetch(href: string) {
|
|
||||||
const selected = select_route(new URL(href));
|
|
||||||
|
|
||||||
if (selected) {
|
|
||||||
prefetching = {
|
|
||||||
href,
|
|
||||||
promise: selected.route.load().then(mod => prepare_route(mod.default, selected.data))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handle_touchstart_mouseover(event: MouseEvent | TouchEvent) {
|
|
||||||
const a: HTMLAnchorElement = <HTMLAnchorElement>findAnchor(<Node>event.target);
|
|
||||||
if (!a || a.rel !== 'prefetch') return;
|
|
||||||
|
|
||||||
prefetch(a.href);
|
|
||||||
}
|
|
||||||
|
|
||||||
let inited: boolean;
|
|
||||||
|
|
||||||
export function init(_target: Node, _routes: Route[]) {
|
|
||||||
target = _target;
|
|
||||||
routes = _routes.filter(r => !r.error);
|
|
||||||
errors = {
|
|
||||||
'4xx': _routes.find(r => r.error === '4xx'),
|
|
||||||
'5xx': _routes.find(r => r.error === '5xx')
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!inited) { // this check makes HMR possible
|
|
||||||
window.addEventListener('click', handle_click);
|
|
||||||
window.addEventListener('popstate', handle_popstate);
|
|
||||||
|
|
||||||
// prefetch
|
|
||||||
window.addEventListener('touchstart', handle_touchstart_mouseover);
|
|
||||||
window.addEventListener('mouseover', handle_touchstart_mouseover);
|
|
||||||
|
|
||||||
inited = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve().then(() => {
|
|
||||||
const { hash, href } = window.location;
|
|
||||||
|
|
||||||
const deep_linked = hash && document.getElementById(hash.slice(1));
|
|
||||||
scroll_history[uid] = deep_linked ?
|
|
||||||
{ x: 0, y: deep_linked.getBoundingClientRect().top } :
|
|
||||||
scroll_state();
|
|
||||||
|
|
||||||
history.replaceState({ id: uid }, '', href);
|
|
||||||
|
|
||||||
const target = select_route(new URL(window.location.href));
|
|
||||||
return navigate(target, uid);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function goto(href: string, opts = { replaceState: false }) {
|
|
||||||
const target = select_route(new URL(href, window.location.href));
|
|
||||||
if (target) {
|
|
||||||
navigate(target, null);
|
|
||||||
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
|
|
||||||
} else {
|
|
||||||
window.location.href = href;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function prefetchRoutes(pathnames: string[]) {
|
|
||||||
if (!routes) throw new Error(`You must call init() first`);
|
|
||||||
|
|
||||||
return routes
|
|
||||||
.filter(route => {
|
|
||||||
if (!pathnames) return true;
|
|
||||||
return pathnames.some(pathname => {
|
|
||||||
return route.error
|
|
||||||
? route.error === pathname
|
|
||||||
: route.pattern.test(pathname)
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.reduce((promise: Promise<any>, route) => {
|
|
||||||
return promise.then(route.load);
|
|
||||||
}, Promise.resolve());
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove this in 0.9
|
|
||||||
export { prefetchRoutes as preloadRoutes };
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
export type Params = Record<string, string>;
|
|
||||||
export type Query = Record<string, string | true>;
|
|
||||||
export type RouteData = { params: Params, query: Query };
|
|
||||||
|
|
||||||
export interface ComponentConstructor {
|
|
||||||
new (options: { target: Node, data: any, hydrate: boolean }): Component;
|
|
||||||
preload: (data: { params: Params, query: Query }) => Promise<any>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface Component {
|
|
||||||
destroy: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Route = {
|
|
||||||
pattern: RegExp;
|
|
||||||
load: () => Promise<{ default: ComponentConstructor }>;
|
|
||||||
error?: string;
|
|
||||||
params?: (match: RegExpExecArray) => Record<string, string>;
|
|
||||||
ignore?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ScrollPosition = {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Target = {
|
|
||||||
url: URL;
|
|
||||||
route: Route;
|
|
||||||
data: RouteData;
|
|
||||||
};
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
export function detach(node: Node) {
|
|
||||||
node.parentNode.removeChild(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findAnchor(node: Node) {
|
|
||||||
while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG <a> elements have a lowercase name
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function which(event: MouseEvent) {
|
|
||||||
return event.which === null ? event.button : event.which;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function scroll_state() {
|
|
||||||
return {
|
|
||||||
x: window.scrollX,
|
|
||||||
y: window.scrollY
|
|
||||||
};
|
|
||||||
}
|
|
||||||
1
src/types.d.ts
vendored
Normal file
1
src/types.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
declare module 'svelte/compiler';
|
||||||
119
src/utils.ts
Normal file
119
src/utils.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export function left_pad(str: string, len: number) {
|
||||||
|
while (str.length < len) str = ` ${str}`;
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function repeat(str: string, i: number) {
|
||||||
|
let result = '';
|
||||||
|
while (i--) result += str;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function format_milliseconds(ms: number) {
|
||||||
|
if (ms < 1000) return `${ms}ms`;
|
||||||
|
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
||||||
|
|
||||||
|
const minutes = ~~(ms / 60000);
|
||||||
|
const seconds = Math.round((ms % 60000) / 1000);
|
||||||
|
return `${minutes}m${seconds < 10 ? '0' : ''}${seconds}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function elapsed(start: number) {
|
||||||
|
return format_milliseconds(Date.now() - start);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function walk(cwd: string, dir = cwd, files: string[] = []) {
|
||||||
|
fs.readdirSync(dir).forEach(file => {
|
||||||
|
const resolved = path.resolve(dir, file);
|
||||||
|
if (fs.statSync(resolved).isDirectory()) {
|
||||||
|
walk(cwd, resolved, files);
|
||||||
|
} else {
|
||||||
|
files.push(posixify(path.relative(cwd, resolved)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function posixify(str: string) {
|
||||||
|
return str.replace(/\\/g, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
const previous_contents = new Map();
|
||||||
|
|
||||||
|
export function write_if_changed(file: string, code: string) {
|
||||||
|
if (code !== previous_contents.get(file)) {
|
||||||
|
previous_contents.set(file, code);
|
||||||
|
fs.writeFileSync(file, code);
|
||||||
|
fudge_mtime(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stringify(string: string, includeQuotes: boolean = true) {
|
||||||
|
const quoted = JSON.stringify(string);
|
||||||
|
return includeQuotes ? quoted : quoted.slice(1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fudge_mtime(file: string) {
|
||||||
|
// need to fudge the mtime so that webpack doesn't go doolally
|
||||||
|
const { atime, mtime } = fs.statSync(file);
|
||||||
|
fs.utimesSync(
|
||||||
|
file,
|
||||||
|
new Date(atime.getTime() - 999999),
|
||||||
|
new Date(mtime.getTime() - 999999)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reserved_words = new Set([
|
||||||
|
'arguments',
|
||||||
|
'await',
|
||||||
|
'break',
|
||||||
|
'case',
|
||||||
|
'catch',
|
||||||
|
'class',
|
||||||
|
'const',
|
||||||
|
'continue',
|
||||||
|
'debugger',
|
||||||
|
'default',
|
||||||
|
'delete',
|
||||||
|
'do',
|
||||||
|
'else',
|
||||||
|
'enum',
|
||||||
|
'eval',
|
||||||
|
'export',
|
||||||
|
'extends',
|
||||||
|
'false',
|
||||||
|
'finally',
|
||||||
|
'for',
|
||||||
|
'function',
|
||||||
|
'if',
|
||||||
|
'implements',
|
||||||
|
'import',
|
||||||
|
'in',
|
||||||
|
'instanceof',
|
||||||
|
'interface',
|
||||||
|
'let',
|
||||||
|
'new',
|
||||||
|
'null',
|
||||||
|
'package',
|
||||||
|
'private',
|
||||||
|
'protected',
|
||||||
|
'public',
|
||||||
|
'return',
|
||||||
|
'static',
|
||||||
|
'super',
|
||||||
|
'switch',
|
||||||
|
'this',
|
||||||
|
'throw',
|
||||||
|
'true',
|
||||||
|
'try',
|
||||||
|
'typeof',
|
||||||
|
'var',
|
||||||
|
'void',
|
||||||
|
'while',
|
||||||
|
'with',
|
||||||
|
'yield',
|
||||||
|
]);
|
||||||
7
test/app/.gitignore
vendored
7
test/app/.gitignore
vendored
@@ -1,7 +0,0 @@
|
|||||||
.DS_Store
|
|
||||||
node_modules
|
|
||||||
.sapper
|
|
||||||
yarn.lock
|
|
||||||
cypress/screenshots
|
|
||||||
templates/.*
|
|
||||||
dist
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# sapper-template
|
|
||||||
|
|
||||||
The default [Sapper](https://github.com/sveltejs/sapper) template. To clone it and get started:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx degit sveltejs/sapper-template my-app
|
|
||||||
cd my-app
|
|
||||||
npm install # or yarn!
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Open up [localhost:3000](http://localhost:3000) and start clicking around.
|
|
||||||
|
|
||||||
|
|
||||||
## Structure
|
|
||||||
|
|
||||||
Sapper expects to find three directories in the root of your project — `assets`, `routes` and `templates`.
|
|
||||||
|
|
||||||
|
|
||||||
### assets
|
|
||||||
|
|
||||||
The [assets](assets) directory contains any static assets that should be available. These are served using [serve-static](https://github.com/expressjs/serve-static).
|
|
||||||
|
|
||||||
In your [service-worker.js](templates/service-worker.js) file, Sapper makes these files available as `__assets__` so that you can cache them (though you can choose not to, for example if you don't want to cache very large files).
|
|
||||||
|
|
||||||
|
|
||||||
### routes
|
|
||||||
|
|
||||||
This is the heart of your Sapper app. There are two kinds of routes — *pages*, and *server routes*.
|
|
||||||
|
|
||||||
**Pages** are Svelte components written in `.html` files. When a user first visits the application, they will be served a server-rendered version of the route in question, plus some JavaScript that 'hydrates' the page and initialises a client-side router. From that point forward, navigating to other pages is handled entirely on the client for a fast, app-like feel. (Sapper will preload and cache the code for these subsequent pages, so that navigation is instantaneous.)
|
|
||||||
|
|
||||||
**Server routes** are modules written in `.js` files, that export functions corresponding to HTTP methods. Each function receives Express `request` and `response` objects as arguments, plus a `next` function. This is useful for creating a JSON API, for example.
|
|
||||||
|
|
||||||
There are three simple rules for naming the files that define your routes:
|
|
||||||
|
|
||||||
* A file called `routes/about.html` corresponds to the `/about` route. A file called `routes/blog/[slug].html` corresponds to the `/blog/:slug` route, in which case `params.slug` is available to the route
|
|
||||||
* The file `routes/index.html` (or `routes/index.js`) corresponds to the root of your app. `routes/about/index.html` is treated the same as `routes/about.html`.
|
|
||||||
* Files and directories with a leading underscore do *not* create routes. This allows you to colocate helper modules and components with the routes that depend on them — for example you could have a file called `routes/_helpers/datetime.js` and it would *not* create a `/_helpers/datetime` route
|
|
||||||
|
|
||||||
|
|
||||||
### templates
|
|
||||||
|
|
||||||
This directory should contain the following files at a minimum:
|
|
||||||
|
|
||||||
* [2xx.html](templates/2xx.html) — a template for the page to serve for valid requests
|
|
||||||
* [4xx.html](templates/4xx.html) — a template for 4xx-range errors (such as 404 Not Found)
|
|
||||||
* [5xx.html](templates/5xx.html) — a template for 5xx-range errors (such as 500 Internal Server Error)
|
|
||||||
* [main.js](templates/main.js) — this module initialises Sapper
|
|
||||||
* [service-worker.js](templates/service-worker.js) — your app's service worker
|
|
||||||
|
|
||||||
Inside the HTML templates, Sapper will inject various values as indicated by `%sapper.xxxx%` tags. Inside JavaScript files, Sapper will replace strings like `__dev__` with the appropriate value.
|
|
||||||
|
|
||||||
In lieu of documentation (bear with us), consult the files to see what variables are available and how they're used.
|
|
||||||
|
|
||||||
|
|
||||||
## Webpack config
|
|
||||||
|
|
||||||
Sapper uses webpack to provide code-splitting, dynamic imports and hot module reloading, as well as compiling your Svelte components. As long as you don't do anything daft, you can edit the configuration files to add whatever loaders and plugins you'd like.
|
|
||||||
|
|
||||||
|
|
||||||
## Production mode and deployment
|
|
||||||
|
|
||||||
To start a production version of your app, run `npm start`. This will disable hot module replacement, and activate the appropriate webpack plugins.
|
|
||||||
|
|
||||||
You can deploy your application to any environment that supports Node 8 or above. As an example, to deploy to [Now](https://zeit.co/now), run these commands:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install -g now
|
|
||||||
now
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Bugs and feedback
|
|
||||||
|
|
||||||
Sapper is in early development, and may have the odd rough edge here and there. Please be vocal over on the [Sapper issue tracker](https://github.com/sveltejs/sapper/issues).
|
|
||||||
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[LIL](LICENSE)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { init, prefetchRoutes } from '../../../runtime.js';
|
|
||||||
import { routes } from './manifest/client.js';
|
|
||||||
|
|
||||||
window.init = () => {
|
|
||||||
return init(document.querySelector('#sapper'), routes);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.prefetchRoutes = prefetchRoutes;
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import polka from 'polka';
|
|
||||||
import compression from 'compression';
|
|
||||||
import serve from 'serve-static';
|
|
||||||
import sapper from '../../../dist/middleware.ts.js';
|
|
||||||
import { routes } from './manifest/server.js';
|
|
||||||
|
|
||||||
let pending;
|
|
||||||
let ended;
|
|
||||||
|
|
||||||
process.on('message', message => {
|
|
||||||
if (message.action === 'start') {
|
|
||||||
if (pending) {
|
|
||||||
throw new Error(`Already capturing`);
|
|
||||||
}
|
|
||||||
|
|
||||||
pending = new Set();
|
|
||||||
ended = false;
|
|
||||||
process.send({ type: 'ready' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.action === 'end') {
|
|
||||||
ended = true;
|
|
||||||
if (pending.size === 0) {
|
|
||||||
process.send({ type: 'done' });
|
|
||||||
pending = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const app = polka();
|
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
if (pending) pending.add(req.url);
|
|
||||||
|
|
||||||
const { write, end } = res;
|
|
||||||
const chunks = [];
|
|
||||||
|
|
||||||
res.write = function(chunk) {
|
|
||||||
chunks.push(new Buffer(chunk));
|
|
||||||
write.apply(res, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
res.end = function(chunk) {
|
|
||||||
if (chunk) chunks.push(new Buffer(chunk));
|
|
||||||
end.apply(res, arguments);
|
|
||||||
|
|
||||||
if (pending) pending.delete(req.url);
|
|
||||||
|
|
||||||
process.send({
|
|
||||||
method: req.method,
|
|
||||||
url: req.url,
|
|
||||||
status: res.statusCode,
|
|
||||||
headers: res._headers,
|
|
||||||
body: Buffer.concat(chunks).toString()
|
|
||||||
});
|
|
||||||
|
|
||||||
if (pending && pending.size === 0 && ended) {
|
|
||||||
process.send({ type: 'done' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
const { PORT = 3000 } = process.env;
|
|
||||||
|
|
||||||
// this allows us to do e.g. `fetch('/api/blog')` on the server
|
|
||||||
const fetch = require('node-fetch');
|
|
||||||
global.fetch = (url, opts) => {
|
|
||||||
if (url[0] === '/') url = `http://localhost:${PORT}${url}`;
|
|
||||||
return fetch(url, opts);
|
|
||||||
};
|
|
||||||
|
|
||||||
app.use(compression({ threshold: 0 }));
|
|
||||||
|
|
||||||
app.use(serve('assets'));
|
|
||||||
|
|
||||||
app.use(sapper({
|
|
||||||
routes
|
|
||||||
}));
|
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
|
||||||
console.log(`listening on port ${PORT}`);
|
|
||||||
});
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang='en'>
|
|
||||||
<head>
|
|
||||||
<meta charset='utf-8'>
|
|
||||||
<meta name='viewport' content='width=device-width'>
|
|
||||||
<meta name='theme-color' content='#aa1e1e'>
|
|
||||||
|
|
||||||
<link rel='stylesheet' href='/global.css'>
|
|
||||||
<link rel='manifest' href='/manifest.json'>
|
|
||||||
<link rel='icon' type='image/png' href='/favicon.png'>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// if ('serviceWorker' in navigator) {
|
|
||||||
// navigator.serviceWorker.register('/service-worker.js');
|
|
||||||
// }
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Sapper generates a <style> tag containing critical CSS
|
|
||||||
for the current page. CSS for the rest of the app is
|
|
||||||
lazily loaded when it precaches secondary pages -->
|
|
||||||
%sapper.styles%
|
|
||||||
|
|
||||||
<!-- This contains the contents of the <:Head> component, if
|
|
||||||
the current page has one -->
|
|
||||||
%sapper.head%
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- The application will be rendered inside this element,
|
|
||||||
because `templates/main.js` references it -->
|
|
||||||
<div id='sapper'>%sapper.html%</div>
|
|
||||||
|
|
||||||
<!-- Sapper creates a <script> tag containing `templates/main.js`
|
|
||||||
and anything else it needs to hydrate the app and
|
|
||||||
initialise the router -->
|
|
||||||
%sapper.scripts%
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,45 +0,0 @@
|
|||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
position: relative;
|
|
||||||
max-width: 56em;
|
|
||||||
background-color: white;
|
|
||||||
padding: 2em;
|
|
||||||
margin: 0 auto;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
|
||||||
margin: 0 0 0.5em 0;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: menlo, inconsolata, monospace;
|
|
||||||
font-size: calc(1em - 2px);
|
|
||||||
color: #555;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
padding: 0.2em 0.4em;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 400px) {
|
|
||||||
body {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 80 KiB |
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"background_color": "#ffffff",
|
|
||||||
"theme_color": "#aa1e1e",
|
|
||||||
"name": "TODO",
|
|
||||||
"short_name": "TODO",
|
|
||||||
"display": "minimal-ui",
|
|
||||||
"start_url": "/",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "svelte-logo-192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "svelte-logo-512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.3 KiB |
@@ -1,18 +0,0 @@
|
|||||||
<:Head>
|
|
||||||
<title>{{status}}</title>
|
|
||||||
</:Head>
|
|
||||||
|
|
||||||
<Layout page='home'>
|
|
||||||
<h1>Not found</h1>
|
|
||||||
<p>{{error.message}}</p>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Layout from './_components/Layout.html';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Layout
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<:Head>
|
|
||||||
<title>Internal server error</title>
|
|
||||||
</:Head>
|
|
||||||
|
|
||||||
<Layout page='home'>
|
|
||||||
<h1>Internal server error</h1>
|
|
||||||
<p>{{error.message}}</p>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Layout from './_components/Layout.html';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Layout
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<Nav page={{page}}/>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<slot></slot>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Nav from './Nav.html';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Nav
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
<nav>
|
|
||||||
<ul>
|
|
||||||
<li><a href='/'>home</a></li>
|
|
||||||
<li><a href='/about'>about</a></li>
|
|
||||||
<li><a href='/slow-preload'>slow preload</a></li>
|
|
||||||
<li><a href='/redirect-from'>redirect</a></li>
|
|
||||||
<li><a href='/blog/nope'>broken link</a></li>
|
|
||||||
<li><a href='/blog/throw-an-error'>error link</a></li>
|
|
||||||
<li><a rel=prefetch class='{{page === "blog" ? "selected" : ""}}' href='/blog'>blog</a></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
nav {
|
|
||||||
border-bottom: 1px solid rgba(170,30,30,0.1);
|
|
||||||
font-weight: 300;
|
|
||||||
padding: 0 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* clearfix */
|
|
||||||
ul::after {
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: block;
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected::after {
|
|
||||||
position: absolute;
|
|
||||||
content: '';
|
|
||||||
width: calc(100% - 1em);
|
|
||||||
height: 2px;
|
|
||||||
background-color: rgb(170,30,30);
|
|
||||||
display: block;
|
|
||||||
bottom: -1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 1em 0.5em;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<:Head>
|
|
||||||
<title>About</title>
|
|
||||||
</:Head>
|
|
||||||
|
|
||||||
<Layout page='about'>
|
|
||||||
<h1>About this site</h1>
|
|
||||||
|
|
||||||
<p>This is the 'about' page. There's not much here.</p>
|
|
||||||
|
|
||||||
<button class='goto' on:click='goto("/blog/what-is-sapper")'>What is Sapper?</button>
|
|
||||||
<button class='prefetch' on:click='goto("/blog/why-the-name")'>Why the name?</button>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Layout from './_components/Layout.html';
|
|
||||||
import { goto, prefetch } from '../../../runtime.js';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Layout
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
goto,
|
|
||||||
prefetch
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import posts from './blog/_posts.js';
|
|
||||||
|
|
||||||
const contents = JSON.stringify(posts.map(post => {
|
|
||||||
return {
|
|
||||||
title: post.title,
|
|
||||||
slug: post.slug
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
|
|
||||||
export function get(req, res) {
|
|
||||||
res.writeHead(200, {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Cache-Control': `max-age=${30 * 60 * 1e3}` // cache for 30 minutes
|
|
||||||
});
|
|
||||||
|
|
||||||
res.end(contents);
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user