mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-13 19:45:26 +00:00
Compare commits
652 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
b1a9be2dc3 | ||
|
|
c5456d3033 | ||
|
|
9b33dad589 | ||
|
|
4315a46ff2 | ||
|
|
0fb5827968 | ||
|
|
f9bf23dc43 | ||
|
|
611017fd28 | ||
|
|
72b265a35f | ||
|
|
e0d533f2ea | ||
|
|
dba83641e4 | ||
|
|
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 | ||
|
|
2f24cb0429 | ||
|
|
687071902d | ||
|
|
cd3fcfdf3c | ||
|
|
dad48e4abd | ||
|
|
37d3d57694 | ||
|
|
9a5d273590 | ||
|
|
3816fe71ad | ||
|
|
69f5b9cac7 | ||
|
|
ad14320dc3 | ||
|
|
43563bd8e5 | ||
|
|
02d558b97c | ||
|
|
866286c95e | ||
|
|
e1b5e336dc | ||
|
|
1d71b86c0f | ||
|
|
bdc248f09a | ||
|
|
be63ea7c96 | ||
|
|
819ec0b776 | ||
|
|
d22d37fb18 | ||
|
|
8ec433581a | ||
|
|
0d0e4d664e | ||
|
|
4348fad16d | ||
|
|
4314897a78 | ||
|
|
b1c57466c0 | ||
|
|
ef55fc5ddd | ||
|
|
e011fce935 | ||
|
|
ba3d9c85c5 | ||
|
|
cddd7adaad | ||
|
|
d8412f33ba | ||
|
|
254e41b11e | ||
|
|
491c5e3b92 | ||
|
|
4441ceb91d | ||
|
|
77e418cd21 | ||
|
|
4171786953 | ||
|
|
5f7cbadd8d | ||
|
|
9bac32eea4 | ||
|
|
3a19657ad9 | ||
|
|
d1b6d029e9 | ||
|
|
45b1147228 | ||
|
|
c827fda703 | ||
|
|
dd39909371 | ||
|
|
fb24c862f3 | ||
|
|
542115f82e | ||
|
|
61000a4795 | ||
|
|
7f98d50e15 | ||
|
|
c580259c07 | ||
|
|
e8f3aff0da | ||
|
|
c82031a8e5 | ||
|
|
1eed1023aa | ||
|
|
c1a2d93da6 | ||
|
|
504654b58e | ||
|
|
b1067103a4 | ||
|
|
06af8e87da | ||
|
|
8bb0999878 | ||
|
|
b5a8d29c37 | ||
|
|
5925636b16 | ||
|
|
bc232007c3 | ||
|
|
ffaacb4c99 | ||
|
|
47b50f2c0e | ||
|
|
a66ac00d42 | ||
|
|
0f8c04b03d | ||
|
|
d9d93f41c4 | ||
|
|
5289fc11d8 | ||
|
|
dd6c51567a | ||
|
|
01ff84f241 | ||
|
|
329c113723 | ||
|
|
2ad10b380f | ||
|
|
e6314cde96 | ||
|
|
b64e25a177 | ||
|
|
49bc1b00a9 | ||
|
|
24bfcc8d2d | ||
|
|
b405e5878e | ||
|
|
ef0ca58a21 | ||
|
|
854147fa6c | ||
|
|
50ecc5c130 | ||
|
|
7e2f5f8fb6 | ||
|
|
acef0e808f | ||
|
|
248573f510 | ||
|
|
e91955fdad | ||
|
|
368e6d5cb1 | ||
|
|
1984203e87 | ||
|
|
0165c14fd9 | ||
|
|
bdb9d49187 | ||
|
|
4d79cb81ed | ||
|
|
181b0711ec | ||
|
|
1b282e7b0d | ||
|
|
99853c5181 | ||
|
|
ff3b43443e | ||
|
|
2622692f69 | ||
|
|
7625302ec7 | ||
|
|
09422e3c5a | ||
|
|
a96fb93bfb | ||
|
|
17d7ca36f1 | ||
|
|
b73e5eaa8e | ||
|
|
d9cb572271 | ||
|
|
34c28f36cd | ||
|
|
5dd04eb35c | ||
|
|
b1d072d43a | ||
|
|
5ad3f3f1d5 | ||
|
|
58754c6d15 | ||
|
|
c36780fdc8 | ||
|
|
9bebb56bd6 | ||
|
|
f475634d8d | ||
|
|
58c1eb9fa8 | ||
|
|
631afbbfe4 | ||
|
|
1cc9acb4f1 | ||
|
|
19005110f1 | ||
|
|
21ee8ad39d | ||
|
|
906b0c7ad5 | ||
|
|
896fd410d1 | ||
|
|
c0cc877456 | ||
|
|
3ed9ce27a1 | ||
|
|
edba45b809 | ||
|
|
43c1890235 | ||
|
|
605929053c | ||
|
|
2752c73ebb | ||
|
|
2547db39ac | ||
|
|
1285739cc5 | ||
|
|
14d64e854a | ||
|
|
c419c73550 | ||
|
|
835b94175d | ||
|
|
25bdcf9957 | ||
|
|
792ccf5c6a | ||
|
|
4ca8195037 | ||
|
|
cb12231053 | ||
|
|
d55401d45b | ||
|
|
99d4eafb0b | ||
|
|
bff6f550be | ||
|
|
f8ea9ebda1 | ||
|
|
181d7b4a61 | ||
|
|
beb415c65d | ||
|
|
5bbd7ead17 | ||
|
|
e11405d555 | ||
|
|
9fe0ca2c22 | ||
|
|
f2eb95d546 | ||
|
|
ab1ca60363 | ||
|
|
d95f52f8e9 | ||
|
|
b02183af53 | ||
|
|
f9828f9fd2 | ||
|
|
9a760c570f | ||
|
|
0f390920a8 | ||
|
|
9adb6ca7e6 | ||
|
|
24980651c0 | ||
|
|
7c6436a99c | ||
|
|
f6b26f1b07 | ||
|
|
55b60369f9 | ||
|
|
2be9dd1883 | ||
|
|
b29700f725 | ||
|
|
7188ce0d0d | ||
|
|
4f8ce19fe1 | ||
|
|
a85f2921e8 | ||
|
|
7a2ed16884 | ||
|
|
08e575fee0 | ||
|
|
7dbcab74d3 | ||
|
|
9b1b545194 | ||
|
|
7b01242f3e | ||
|
|
15b1fbf8a6 | ||
|
|
8f1d2e0a04 | ||
|
|
dfb8692d78 | ||
|
|
09d3c4d85e | ||
|
|
1e623dde29 | ||
|
|
5104abf329 | ||
|
|
6554fc8616 | ||
|
|
cd01b7e6db | ||
|
|
bfa3da6d3d | ||
|
|
6ee092f8d4 | ||
|
|
ac70004f77 | ||
|
|
3449f1eb37 | ||
|
|
16cb1fccc6 | ||
|
|
b20c1c029f | ||
|
|
7abfb1aab1 | ||
|
|
205c2defe4 | ||
|
|
09a6eec83e | ||
|
|
2cabf61ea7 | ||
|
|
71cfdd2907 | ||
|
|
297f4276de | ||
|
|
422e31e183 | ||
|
|
b53ee061c0 | ||
|
|
8bad37205d | ||
|
|
fd0dd4fe58 | ||
|
|
4940644ae3 | ||
|
|
fb8d952eeb | ||
|
|
fc631c4866 | ||
|
|
03ce2ea998 | ||
|
|
dd8deb2d8a | ||
|
|
7d721abb2a | ||
|
|
39b1fa89ce | ||
|
|
7a3506420f | ||
|
|
72ae4a1c64 | ||
|
|
a09c33d6a5 | ||
|
|
4590aa313c | ||
|
|
d11bd954e0 | ||
|
|
c15959710b | ||
|
|
bb8ff74f68 | ||
|
|
2cbbe91490 | ||
|
|
faeddd8add | ||
|
|
d77722c042 | ||
|
|
61daba7a64 | ||
|
|
54ff8cc2e6 | ||
|
|
e6fcafe09b | ||
|
|
a305d3cea1 | ||
|
|
75e70207b8 | ||
|
|
8a8526d9ed | ||
|
|
9a76229bb6 | ||
|
|
f4e46e6e6c | ||
|
|
90cd347112 | ||
|
|
5adfdd6fe0 | ||
|
|
a6dc61a182 | ||
|
|
96666d05ec | ||
|
|
6390ba692b | ||
|
|
0e131cc81e | ||
|
|
bd3d5713cb | ||
|
|
9ec23c47ad | ||
|
|
b7bb69925e | ||
|
|
25124f6ee7 | ||
|
|
73d491cd19 | ||
|
|
e25fceb4b8 | ||
|
|
3807147c57 | ||
|
|
a523ba58ff | ||
|
|
fe03fd3a52 | ||
|
|
89c430a0cb | ||
|
|
8ef312849c | ||
|
|
4200446684 | ||
|
|
681ed005b8 | ||
|
|
d457af8d51 | ||
|
|
0c158b9e1f | ||
|
|
50011e2077 | ||
|
|
f27b7973e3 | ||
|
|
2af2ab3cb9 | ||
|
|
6a4dc1901c | ||
|
|
fbbc0e9e19 | ||
|
|
1213c3da46 | ||
|
|
4cc2104088 | ||
|
|
d6dda371ca | ||
|
|
304c06085e | ||
|
|
33b6450e34 | ||
|
|
8faa98af6a | ||
|
|
14df138528 | ||
|
|
44285cdb2f | ||
|
|
bd656cfd5b | ||
|
|
c4b4bd587d | ||
|
|
2abfdb03d5 | ||
|
|
a80ac3a8b8 | ||
|
|
887cb09386 | ||
|
|
cfeeafded4 | ||
|
|
2cae674033 | ||
|
|
7c0f32662d | ||
|
|
b4fb1c3268 | ||
|
|
ecd0f673a9 | ||
|
|
40d16852f7 | ||
|
|
133be03791 | ||
|
|
727a76ebb5 | ||
|
|
e3c047831a | ||
|
|
81b5e0d764 | ||
|
|
98e904dcfc | ||
|
|
ca51372150 | ||
|
|
7cef1f1120 | ||
|
|
1b73baabce | ||
|
|
5aa01b922b | ||
|
|
f0bc68be88 | ||
|
|
be7c53becc | ||
|
|
9ea4137b87 | ||
|
|
7588911108 | ||
|
|
fc8280adea | ||
|
|
d08f9eb5a4 | ||
|
|
2b3472b1b1 | ||
|
|
30ddb3dd7e | ||
|
|
0c891ba79e | ||
|
|
ee94f355d5 | ||
|
|
bea9b7965a | ||
|
|
1312aede1f | ||
|
|
50e307e0c0 | ||
|
|
e87ac1f367 | ||
|
|
5da9d0926a | ||
|
|
9538499d51 | ||
|
|
ff1e632057 | ||
|
|
aeeb231477 | ||
|
|
d1940db8c0 | ||
|
|
98f9a64b64 | ||
|
|
b9bef802d3 | ||
|
|
a7024b3806 | ||
|
|
423e02aeae | ||
|
|
12b73ecebf | ||
|
|
e1bc38b5a7 | ||
|
|
b66f624f01 | ||
|
|
502dd547d1 | ||
|
|
4c343490d2 | ||
|
|
b3027c5816 | ||
|
|
c29e8022cc | ||
|
|
e4cd4c9cb0 | ||
|
|
feddad42b2 | ||
|
|
3c4ebcda30 | ||
|
|
75aedf4663 | ||
|
|
c8366dec74 | ||
|
|
9a936669c6 | ||
|
|
0226bd90c6 | ||
|
|
e1926e1bcb | ||
|
|
db1c1f332a | ||
|
|
e8d510b261 | ||
|
|
f8e237b265 | ||
|
|
68c2f2e388 | ||
|
|
0bcb61650b | ||
|
|
43a12a8331 | ||
|
|
f0feab5738 | ||
|
|
e9203b4d71 | ||
|
|
8e79e706e6 | ||
|
|
4b495f44fd | ||
|
|
222a750b7b | ||
|
|
5b214c964c | ||
|
|
95f99fd378 | ||
|
|
1bed4b0670 | ||
|
|
9d4890913a | ||
|
|
f50d3c4262 | ||
|
|
8925e541d5 | ||
|
|
a48afb77d3 | ||
|
|
45e845ee92 | ||
|
|
492f024d2a | ||
|
|
8d40992cf1 | ||
|
|
4232f75b19 | ||
|
|
fefb0d96d7 | ||
|
|
cd91bf2ca4 | ||
|
|
7466e8da82 | ||
|
|
463307db86 | ||
|
|
2a68394dce | ||
|
|
c8fe0679ae |
@@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"root": true,
|
|
||||||
"rules": {
|
|
||||||
"indent": [ 2, "tab", { "SwitchCase": 1 } ],
|
|
||||||
"semi": [ 2, "always" ],
|
|
||||||
"space-before-blocks": [ 2, "always" ],
|
|
||||||
"no-mixed-spaces-and-tabs": [ 2, "smart-tabs" ],
|
|
||||||
"no-cond-assign": 0,
|
|
||||||
"no-unused-vars": 2,
|
|
||||||
"object-shorthand": [ 2, "always" ],
|
|
||||||
"no-const-assign": 2,
|
|
||||||
"no-class-assign": 2,
|
|
||||||
"no-this-before-super": 2,
|
|
||||||
"no-var": 2,
|
|
||||||
"no-unreachable": 2,
|
|
||||||
"valid-typeof": 2,
|
|
||||||
"quote-props": [ 2, "as-needed" ],
|
|
||||||
"one-var": [ 2, "never" ],
|
|
||||||
"prefer-arrow-callback": 2,
|
|
||||||
"prefer-const": [ 2, { "destructuring": "all" } ],
|
|
||||||
"arrow-spacing": 2,
|
|
||||||
"no-inner-declarations": 0
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"es6": true,
|
|
||||||
"browser": true,
|
|
||||||
"node": true,
|
|
||||||
"mocha": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:import/errors",
|
|
||||||
"plugin:import/warnings"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 6,
|
|
||||||
"sourceType": "module"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,4 +1,13 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
yarn.lock
|
||||||
|
yarn-error.log
|
||||||
node_modules
|
node_modules
|
||||||
cypress/screenshots
|
cypress/screenshots
|
||||||
test/app/.sapper
|
test/app/.sapper
|
||||||
|
test/app/app/manifest
|
||||||
|
test/app/export
|
||||||
|
test/app/build
|
||||||
|
sapper
|
||||||
|
runtime.js
|
||||||
|
dist
|
||||||
|
!rollup.config.js
|
||||||
15
.travis.yml
15
.travis.yml
@@ -1,10 +1,21 @@
|
|||||||
sudo: false
|
sudo: false
|
||||||
|
|
||||||
language: node_js
|
language: node_js
|
||||||
|
|
||||||
node_js:
|
node_js:
|
||||||
|
- "6"
|
||||||
- "stable"
|
- "stable"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
- BUILD_TIMEOUT=10000
|
- BUILD_TIMEOUT=10000
|
||||||
|
|
||||||
|
addons:
|
||||||
|
apt:
|
||||||
|
packages:
|
||||||
|
- xvfb
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- npm install
|
- export DISPLAY=':99.0'
|
||||||
- (cd test/app && npm install)
|
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||||
|
- npm ci || npm i
|
||||||
|
|||||||
436
CHANGELOG.md
436
CHANGELOG.md
@@ -1,5 +1,441 @@
|
|||||||
# sapper changelog
|
# sapper changelog
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
* Whoops — `tslib` is a runtime dependency
|
||||||
|
|
||||||
|
## 0.9.5
|
||||||
|
|
||||||
|
* Stringify clorox output ([#197](https://github.com/sveltejs/sapper/pull/197))
|
||||||
|
|
||||||
|
## 0.9.4
|
||||||
|
|
||||||
|
* Add `SAPPER_BASE` and `SAPPER_APP` environment variables ([#181](https://github.com/sveltejs/sapper/issues/181))
|
||||||
|
* Minify template in `sapper build` ([#15](https://github.com/sveltejs/sapper/issues/15))
|
||||||
|
* Minify all HTML files in `sapper export` ([#172](https://github.com/sveltejs/sapper/issues/172))
|
||||||
|
* Log exported files ([#195](https://github.com/sveltejs/sapper/pull/195))
|
||||||
|
* Add `--open`/`-o` flag to `sapper dev` and `sapper start` ([#186](https://github.com/sveltejs/sapper/issues/186))
|
||||||
|
|
||||||
|
## 0.9.3
|
||||||
|
|
||||||
|
* Fix path to `sapper-dev-client`
|
||||||
|
|
||||||
|
## 0.9.2
|
||||||
|
|
||||||
|
* Include `dist` files in package
|
||||||
|
|
||||||
|
## 0.9.1
|
||||||
|
|
||||||
|
* Include `sapper` bin
|
||||||
|
|
||||||
|
## 0.9.0
|
||||||
|
|
||||||
|
* Use `devalue` instead of `serialize-javascript`, allowing `preload` to return non-POJOs and cyclical/repeated references, but *not* functions ([#112](https://github.com/sveltejs/sapper/issues/112))
|
||||||
|
* Kill child process if webpack crashes ([#177](https://github.com/sveltejs/sapper/issues/177))
|
||||||
|
* Support HMR on remote devices ([#165](https://github.com/sveltejs/sapper/issues/165))
|
||||||
|
* Remove hard-coded port (([#169](https://github.com/sveltejs/sapper/issues/169)))
|
||||||
|
* Allow non-JS files, e.g. TypeScript to be used as entry points and server routes ([#57](https://github.com/sveltejs/sapper/issues/57))
|
||||||
|
* Faster startup ([#173](https://github.com/sveltejs/sapper/issues/173))
|
||||||
|
|
||||||
|
## 0.8.4
|
||||||
|
|
||||||
|
* Fix route sorting ([#175](https://github.com/sveltejs/sapper/pull/175))
|
||||||
|
|
||||||
|
## 0.8.3
|
||||||
|
|
||||||
|
* Automatically select available port, or use `--port` flag for `dev` and `start` ([#169](https://github.com/sveltejs/sapper/issues/169))
|
||||||
|
* Show stats after build/export ([#168](https://github.com/sveltejs/sapper/issues/168))
|
||||||
|
* Various CLI improvements ([#170](https://github.com/sveltejs/sapper/pull/170))
|
||||||
|
|
||||||
|
## 0.8.2
|
||||||
|
|
||||||
|
* Rename `preloadRoutes` to `prefetchRoutes` ([#166](https://github.com/sveltejs/sapper/issues/166))
|
||||||
|
|
||||||
|
## 0.8.1
|
||||||
|
|
||||||
|
* Add `sapper start` command, for running an app built with `sapper build` ([#163](https://github.com/sveltejs/sapper/issues/163))
|
||||||
|
|
||||||
|
## 0.8.0
|
||||||
|
|
||||||
|
* Update to webpack 4
|
||||||
|
* Add `preloadRoutes` function — secondary routes are no longer automatically preloaded ([#160](https://github.com/sveltejs/sapper/issues/160))
|
||||||
|
* `sapper build` outputs to `build`, `sapper build custom-dir` outputs to `custom-dir` ([#150](https://github.com/sveltejs/sapper/pull/150))
|
||||||
|
* `sapper export` outputs to `export`, `sapper export custom-dir` outputs to `custom-dir` ([#150](https://github.com/sveltejs/sapper/pull/150))
|
||||||
|
* Improved logging ([#158](https://github.com/sveltejs/sapper/pull/158))
|
||||||
|
* URI-encode routes ([#103](https://github.com/sveltejs/sapper/issues/103))
|
||||||
|
* Various performance and stability improvements ([#152](https://github.com/sveltejs/sapper/pull/152))
|
||||||
|
|
||||||
|
## 0.7.6
|
||||||
|
|
||||||
|
* Prevent client-side navigation to server route ([#145](https://github.com/sveltejs/sapper/issues/145))
|
||||||
|
* Don't serve error page for server route errors ([#138](https://github.com/sveltejs/sapper/issues/138))
|
||||||
|
|
||||||
|
## 0.7.5
|
||||||
|
|
||||||
|
* Allow dynamic parameters inside route parts ([#139](https://github.com/sveltejs/sapper/issues/139))
|
||||||
|
|
||||||
|
## 0.7.4
|
||||||
|
|
||||||
|
* Force `NODE_ENV='production'` when running `build` or `export` ([#141](https://github.com/sveltejs/sapper/issues/141))
|
||||||
|
* Use source-map-support ([#134](https://github.com/sveltejs/sapper/pull/134))
|
||||||
|
|
||||||
|
## 0.7.3
|
||||||
|
|
||||||
|
* Handle webpack assets that are arrays instead of strings ([#131](https://github.com/sveltejs/sapper/pull/131))
|
||||||
|
* Wait for new server to start before broadcasting HMR update ([#129](https://github.com/sveltejs/sapper/pull/129))
|
||||||
|
|
||||||
|
## 0.7.2
|
||||||
|
|
||||||
|
* Add `hmr-client.js` to package
|
||||||
|
* Wait until first successful client build before creating service-worker.js
|
||||||
|
|
||||||
|
## 0.7.1
|
||||||
|
|
||||||
|
* Add missing `tslib` dependency
|
||||||
|
|
||||||
|
## 0.7.0
|
||||||
|
|
||||||
|
* Restructure app layout (see [migration guide](https://sapper.svelte.technology/guide#0-6-to-0-7)) ([#126](https://github.com/sveltejs/sapper/pull/126))
|
||||||
|
* Support `this.redirect(status, location)` and `this.error(status, error)` in `preload` functions ([#127](https://github.com/sveltejs/sapper/pull/127))
|
||||||
|
* Add `sapper dev` command
|
||||||
|
* Add `sapper --help` command
|
||||||
|
|
||||||
|
## 0.6.4
|
||||||
|
|
||||||
|
* Prevent phantom HMR requests in production mode ([#114](https://github.com/sveltejs/sapper/pull/114))
|
||||||
|
|
||||||
|
## 0.6.3
|
||||||
|
|
||||||
|
* Ignore non-HTML responses when crawling during `export`
|
||||||
|
* Build in prod mode for `export`
|
||||||
|
|
||||||
|
## 0.6.2
|
||||||
|
|
||||||
|
* Handle unspecified type in `sapper export`
|
||||||
|
|
||||||
|
## 0.6.1
|
||||||
|
|
||||||
|
* Fix `pkg.files` and `pkg.bin`
|
||||||
|
|
||||||
|
## 0.6.0
|
||||||
|
|
||||||
|
* Hydrate on first load, and only on first load ([#93](https://github.com/sveltejs/sapper/pull/93))
|
||||||
|
* Identify clashes between page and server routes ([#96](https://github.com/sveltejs/sapper/pull/96))
|
||||||
|
* Remove Express-specific utilities, for compatbility with Polka et al ([#94](https://github.com/sveltejs/sapper/issues/94))
|
||||||
|
* Return a promise from `init` when first page has rendered ([#99](https://github.com/sveltejs/sapper/issues/99))
|
||||||
|
* Handle invalid hash links ([#104](https://github.com/sveltejs/sapper/pull/104))
|
||||||
|
* Avoid `URLSearchParams` ([#107](https://github.com/sveltejs/sapper/pull/107))
|
||||||
|
* Don't automatically set `Content-Type` for server routes ([#111](https://github.com/sveltejs/sapper/pull/111))
|
||||||
|
* Handle empty query string routes, e.g. `/?` ([#105](https://github.com/sveltejs/sapper/pull/105))
|
||||||
|
|
||||||
|
## 0.5.1
|
||||||
|
|
||||||
|
* Only write service-worker.js to filesystem in dev mode ([#90](https://github.com/sveltejs/sapper/issues/90))
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
* Experimental support for `sapper export` ([#9](https://github.com/sveltejs/sapper/issues/9))
|
||||||
|
* Lazily load chokidar, for faster startup ([#64](https://github.com/sveltejs/sapper/pull/64))
|
||||||
|
|
||||||
|
## 0.4.0
|
||||||
|
|
||||||
|
* `%sapper.main%` has been replaced with `%sapper.scripts%` ([#86](https://github.com/sveltejs/sapper/issues/86))
|
||||||
|
* Node 6 support ([#67](https://github.com/sveltejs/sapper/pull/67))
|
||||||
|
* Explicitly load css-loader and style-loader ([#72](https://github.com/sveltejs/sapper/pull/72))
|
||||||
|
* DELETE requests are handled with `del` exports ([#77](https://github.com/sveltejs/sapper/issues/77))
|
||||||
|
* Send preloaded data for first route to client, where possible ([#3](https://github.com/sveltejs/sapper/issues/3))
|
||||||
|
|
||||||
|
## 0.3.2
|
||||||
|
|
||||||
|
* Expose `prefetch` function ([#61](https://github.com/sveltejs/sapper/pull/61))
|
||||||
|
|
||||||
|
## 0.3.1
|
||||||
|
|
||||||
|
* Fix missing `runtime.js`
|
||||||
|
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
* Move `sapper/runtime/app.js` to `sapper/runtime.js`
|
||||||
|
* Cancel navigation if overtaken by second navigation ([#48](https://github.com/sveltejs/sapper/issues/48))
|
||||||
|
* Store preloaded data, to avoiding double prefetching ([#49](https://github.com/sveltejs/sapper/issues/49))
|
||||||
|
* Pass server request object to `preload` ([#54](https://github.com/sveltejs/sapper/pull/54))
|
||||||
|
* Nested routes ([#55](https://github.com/sveltejs/sapper/issues/55))
|
||||||
|
|
||||||
|
## 0.2.10
|
||||||
|
|
||||||
|
* Handle deep links correctly ([#44](https://github.com/sveltejs/sapper/issues/44))
|
||||||
|
|
||||||
|
## 0.2.9
|
||||||
|
|
||||||
|
* Don't write files to disk in prod mode
|
||||||
|
|
||||||
|
## 0.2.8
|
||||||
|
|
||||||
|
* Add `goto` function ([#29](https://github.com/sveltejs/sapper/issues/29))
|
||||||
|
* Don't use `/tmp` as destination in Now environments
|
||||||
|
|
||||||
|
## 0.2.7
|
||||||
|
|
||||||
|
* Fix streaming bug
|
||||||
|
|
||||||
|
## 0.2.6
|
||||||
|
|
||||||
|
* Render main.js back to templates, to allow relative imports ([#40](https://github.com/sveltejs/sapper/issues/40))
|
||||||
|
|
||||||
|
## 0.2.5
|
||||||
|
|
||||||
|
* Fix nested routes on Windows ([#39](https://github.com/sveltejs/sapper/pull/39))
|
||||||
|
* Rebundle when routes and main.js change ([#34](https://github.com/sveltejs/sapper/pull/34))
|
||||||
|
* Add `Link...preload` headers for JavaScript assets ([#2](https://github.com/sveltejs/sapper/issues/2))
|
||||||
|
* Stream document up to first dynamic content ([#19](https://github.com/sveltejs/sapper/issues/19))
|
||||||
|
* Error if routes clash ([#33](https://github.com/sveltejs/sapper/issues/33))
|
||||||
|
|
||||||
|
## 0.2.4
|
||||||
|
|
||||||
|
* Posixify path to HMR client
|
||||||
|
|
||||||
|
## 0.2.3
|
||||||
|
|
||||||
|
* Posixify import paths, even on Windows ([#31](https://github.com/sveltejs/sapper/pull/31))
|
||||||
|
* Pass `url` to 404 handler
|
||||||
|
|
||||||
## 0.2.2
|
## 0.2.2
|
||||||
|
|
||||||
* Create destination directory when building, don't assume it's already there from dev mode
|
* Create destination directory when building, don't assume it's already there from dev mode
|
||||||
|
|||||||
9
LICENSE
Normal file
9
LICENSE
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Copyright (c) 2017 [these people](https://github.com/sveltejs/sapper/graphs/contributors).
|
||||||
|
|
||||||
|
Permission is hereby granted by the authors of this software, to any person, to use the software for any purpose, free of charge, including the rights to run, read, copy, change, distribute and sell it, and including usage rights to any patents the authors may hold on it, subject to the following conditions:
|
||||||
|
|
||||||
|
This license, or a link to its text, must be included with all copies of the software and any derivative works.
|
||||||
|
|
||||||
|
Any modification to the software submitted to the authors may be incorporated into the software under the terms of this license.
|
||||||
|
|
||||||
|
The software is provided "as is", without warranty of any kind, including but not limited to the warranties of title, fitness, merchantability and non-infringement. The authors have no obligation to provide support or updates for the software, and may not be held liable for any damages, claims or other liability arising from its use.
|
||||||
177
README.md
177
README.md
@@ -1,150 +1,75 @@
|
|||||||
# sapper
|
# sapper
|
||||||
|
|
||||||
Combat-ready apps, engineered by Svelte.
|
[Military-grade progressive web apps, powered by Svelte.](https://sapper.svelte.technology)
|
||||||
|
|
||||||
## This is not a thing yet
|
|
||||||
|
|
||||||
If you visit this README in a few weeks, hopefully it will have blossomed into the app development framework we deserve. Right now, it's just a set of ideas.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
[Next.js](https://github.com/zeit/next.js/) introduced a beautiful idea — that you should be able to build your app as universal React components in a special `pages` directory, and the framework should take care of routing and rendering on both client and server. What if we did the same thing for Svelte?
|
|
||||||
|
|
||||||
High-level goals:
|
|
||||||
|
|
||||||
* Extreme ease of development
|
|
||||||
* Code-splitting and HMR out of the box (probably via webpack)
|
|
||||||
* Best-in-class performance
|
|
||||||
* As little magic as possible. Anyone should be able to understand how everything fits together, and e.g. make changes to the webpack config
|
|
||||||
* Links are just `<a>` tags, no special `<Link>` components
|
|
||||||
|
|
||||||
|
|
||||||
## Design
|
## What is Sapper?
|
||||||
|
|
||||||
A Sapper app is just an Express app (conventionally, `server.js`) that uses the `sapper` middleware:
|
Sapper is a framework for building high-performance universal web apps. [Read the guide](https://sapper.svelte.technology/guide) or the [introductory blog post](https://svelte.technology/blog/sapper-towards-the-ideal-web-app-framework) to learn more.
|
||||||
|
|
||||||
```js
|
|
||||||
const app = require('express')();
|
|
||||||
const sapper = require('sapper');
|
|
||||||
|
|
||||||
app.use(sapper());
|
## Get started
|
||||||
|
|
||||||
const { PORT = 3000 } = process.env;
|
Clone the [starter project template](https://github.com/sveltejs/sapper-template) with [degit](https://github.com/rich-harris/degit)...
|
||||||
app.listen(PORT, () => {
|
|
||||||
console.log(`listening on port ${PORT}`);
|
```bash
|
||||||
});
|
npx degit sveltejs/sapper-template my-app
|
||||||
```
|
```
|
||||||
|
|
||||||
The middleware serves pages that match files in the `routes` directory, and assets generated by webpack. In development mode, the middleware once activated watches `routes` to keep the app up-to-date.
|
...then install dependencies and start the dev server...
|
||||||
|
|
||||||
|
```bash
|
||||||
## Routing
|
cd my-app
|
||||||
|
npm install
|
||||||
Like Next, routes are defined by the project directory structure, but with some crucial differences:
|
npm run dev
|
||||||
|
|
||||||
* Files with an `.html` extension are treated as Svelte components. The `routes/about.html` (or `routes/about/index.html`) would create the `/about` route.
|
|
||||||
* Files with a `.js` or `.mjs` extension are more generic route handlers. These files should export functions corresponding to the HTTP methods they support (example below).
|
|
||||||
* Instead of route masking, we embed parameters in the filename. For example `post/[id].html` maps to `/post/:id`, and the component will be rendered with the appropriate parameter.
|
|
||||||
* Nested routes (read [this article](https://joshduff.com/2015-06-why-you-need-a-state-router.md)) can be handled by creating a file that matches the subroute — for example, `routes/app/settings/[submenu].html` would match `/app/settings/profile` *and* `app/settings`, but in the latter case the `submenu` parameter would be `null`.
|
|
||||||
|
|
||||||
An example of a generic route:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// routes/api/post/[id].js
|
|
||||||
export async function get(req, res) {
|
|
||||||
try {
|
|
||||||
const data = await getPostFromDatabase(req.params.id);
|
|
||||||
const json = JSON.stringify(data);
|
|
||||||
|
|
||||||
res.set({
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Content-Length': json.length
|
|
||||||
});
|
|
||||||
|
|
||||||
res.send(json);
|
|
||||||
} catch (err) {
|
|
||||||
res.status(500).send(err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Or, if you omit the `res` argument, it can use the return value:
|
...and navigate to [localhost:3000](http://localhost:3000). To build and run in production mode:
|
||||||
|
|
||||||
```js
|
```bash
|
||||||
// routes/api/post/[id].js
|
npm run build
|
||||||
export async function get(req) {
|
npm start
|
||||||
return await getPostFromDatabase(req.params.id);
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
## Client-side app
|
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!
|
||||||
|
|
||||||
Sapper will create (and in development mode, update) a barebones `main.js` file that dynamically imports individual routes and renders them — something like this:
|
To install and work on Sapper locally:
|
||||||
|
|
||||||
```js
|
```bash
|
||||||
window.addEventListener('click', event => {
|
git clone git@github.com:sveltejs/sapper.git
|
||||||
let a = event.target;
|
cd sapper
|
||||||
while (a && a.nodeName !== 'A') a = a.parentNode;
|
npm install
|
||||||
if (!a) return;
|
npm run dev
|
||||||
|
|
||||||
if (navigate(new URL(a.href))) event.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
const target = document.querySelector('#sapper');
|
|
||||||
let component;
|
|
||||||
|
|
||||||
function navigate(url) {
|
|
||||||
if (url.origin !== window.location.origin) return;
|
|
||||||
|
|
||||||
let match;
|
|
||||||
let params = {};
|
|
||||||
const query = {};
|
|
||||||
|
|
||||||
function render(mod) {
|
|
||||||
if (component) {
|
|
||||||
component.destroy();
|
|
||||||
} else {
|
|
||||||
target.innerHTML = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
component = new mod.default({
|
|
||||||
target,
|
|
||||||
data: { query, params },
|
|
||||||
hydrate: !!component
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.pathname === '/about') {
|
|
||||||
import('/about/index.html').then(render);
|
|
||||||
} else if (url.pathname === '/') {
|
|
||||||
import('/index.js').then(render);
|
|
||||||
} else if (match = /^\/post\/([^\/]+)$/.exec(url.pathname)) {
|
|
||||||
params.id = match[1];
|
|
||||||
import('/post/[id].html').then(render);
|
|
||||||
} else if (match = /^\/([^\/]+)$/.exec(url.pathname)) {
|
|
||||||
params.wildcard = match[1];
|
|
||||||
import('/[wildcard].html').then(render);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate(window.location);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
We're glossing over a lot of important stuff here — e.g. handling `popstate` — but you get the idea. Knowledge of all the possible routes means we can generate optimal code, much in the same way that statically analysing Svelte templates allows the compiler to generate optimal code.
|
### 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.
|
||||||
|
|
||||||
## Things to figure out
|
To link Sapper to your project, from the root of your local Sapper git checkout:
|
||||||
|
|
||||||
* How to customise the overall page template
|
```bash
|
||||||
* An equivalent of `getInitialProps`
|
cd sapper
|
||||||
* Critical CSS
|
npm link
|
||||||
* `store` integration
|
```
|
||||||
* Route transitions
|
|
||||||
* Equivalent of `next export`
|
Then, to link from `sapper-template` (or any other given project):
|
||||||
* A good story for realtime/GraphQL stuff
|
|
||||||
* Service worker
|
```bash
|
||||||
* Using `Link...rel=preload` headers to push main.js/[route].js plus styles
|
cd sapper-template
|
||||||
* ...and lots of other things that haven't occurred to me yet.
|
npm link sapper
|
||||||
|
```
|
||||||
|
|
||||||
|
You should be good to test changes locally.
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[LIL](LICENSE)
|
||||||
@@ -10,8 +10,12 @@ build: off
|
|||||||
environment:
|
environment:
|
||||||
matrix:
|
matrix:
|
||||||
# node.js
|
# node.js
|
||||||
- nodejs_version: stable
|
- nodejs_version: 10.5
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- ps: Install-Product node $env:nodejs_version
|
- ps: Install-Product node $env:nodejs_version
|
||||||
- npm install
|
- npm ci
|
||||||
|
|
||||||
|
test_script:
|
||||||
|
- node --version && npm --version
|
||||||
|
- npm test
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const cmd = process.argv[2];
|
|
||||||
|
|
||||||
if (cmd === 'build') {
|
|
||||||
process.env.NODE_ENV = 'production';
|
|
||||||
require('../lib/build.js')();
|
|
||||||
}
|
|
||||||
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');
|
||||||
41
lib/build.js
41
lib/build.js
@@ -1,41 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const glob = require('glob');
|
|
||||||
const mkdirp = require('mkdirp');
|
|
||||||
const { client, server } = require('./utils/compilers.js');
|
|
||||||
const create_app = require('./utils/create_app.js');
|
|
||||||
const generate_asset_cache = require('./utils/generate_asset_cache.js');
|
|
||||||
const { dest } = require('./config.js');
|
|
||||||
|
|
||||||
module.exports = () => {
|
|
||||||
mkdirp(dest);
|
|
||||||
|
|
||||||
// create main.js and server-routes.js
|
|
||||||
create_app();
|
|
||||||
|
|
||||||
function handleErrors(err, stats) {
|
|
||||||
if (err) {
|
|
||||||
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stats.hasErrors()) {
|
|
||||||
console.log(stats.toString({ colors: true }));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
client.run((err, clientStats) => {
|
|
||||||
handleErrors(err, clientStats);
|
|
||||||
const clientInfo = clientStats.toJson();
|
|
||||||
fs.writeFileSync(path.join(dest, 'stats.client.json'), JSON.stringify(clientInfo, null, ' '));
|
|
||||||
|
|
||||||
server.run((err, serverStats) => {
|
|
||||||
handleErrors(err, serverStats);
|
|
||||||
const serverInfo = serverStats.toJson();
|
|
||||||
fs.writeFileSync(path.join(dest, 'stats.server.json'), JSON.stringify(serverInfo, null, ' '));
|
|
||||||
|
|
||||||
generate_asset_cache(clientInfo, serverInfo);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const mkdirp = require('mkdirp');
|
|
||||||
const rimraf = require('rimraf');
|
|
||||||
|
|
||||||
exports.dev = process.env.NODE_ENV !== 'production';
|
|
||||||
|
|
||||||
exports.templates = path.resolve(process.env.SAPPER_TEMPLATES || 'templates');
|
|
||||||
|
|
||||||
exports.src = path.resolve(process.env.SAPPER_ROUTES || 'routes');
|
|
||||||
|
|
||||||
exports.dest = path.resolve(
|
|
||||||
process.env.NOW ? '/tmp' :
|
|
||||||
process.env.SAPPER_DEST || '.sapper'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (exports.dev) {
|
|
||||||
mkdirp(exports.dest);
|
|
||||||
rimraf.sync(path.join(exports.dest, '**/*'));
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.server_routes = path.resolve(exports.dest, 'server-routes.js');
|
|
||||||
208
lib/index.js
208
lib/index.js
@@ -1,208 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const glob = require('glob');
|
|
||||||
const rimraf = require('rimraf');
|
|
||||||
const mkdirp = require('mkdirp');
|
|
||||||
const webpack = require('webpack');
|
|
||||||
const route_manager = require('./route_manager.js');
|
|
||||||
const templates = require('./templates.js');
|
|
||||||
const create_app = require('./utils/create_app.js');
|
|
||||||
const create_watcher = require('./utils/create_watcher.js');
|
|
||||||
const compilers = require('./utils/compilers.js');
|
|
||||||
const generate_asset_cache = require('./utils/generate_asset_cache.js');
|
|
||||||
const escape_html = require('escape-html');
|
|
||||||
const { src, dest, dev } = require('./config.js');
|
|
||||||
|
|
||||||
function connect_dev() {
|
|
||||||
// create main.js and server-routes.js
|
|
||||||
// TODO update on changes
|
|
||||||
create_app();
|
|
||||||
|
|
||||||
const watcher = create_watcher();
|
|
||||||
|
|
||||||
let asset_cache;
|
|
||||||
|
|
||||||
return compose_handlers([
|
|
||||||
require('webpack-hot-middleware')(compilers.client, {
|
|
||||||
reload: true,
|
|
||||||
path: '/__webpack_hmr',
|
|
||||||
heartbeat: 10 * 1000
|
|
||||||
}),
|
|
||||||
|
|
||||||
async (req, res, next) => {
|
|
||||||
asset_cache = await watcher.ready;
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
|
|
||||||
set_req_pathname,
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname === '/index.html',
|
|
||||||
type: 'text/html',
|
|
||||||
cache: 'max-age=600',
|
|
||||||
fn: () => asset_cache.client.index
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname === '/service-worker.js',
|
|
||||||
type: 'application/javascript',
|
|
||||||
cache: 'max-age=600',
|
|
||||||
fn: () => asset_cache.client.service_worker
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname.startsWith('/client/'),
|
|
||||||
type: 'application/javascript',
|
|
||||||
cache: 'max-age=31536000',
|
|
||||||
fn: pathname => asset_cache.client.chunks[pathname]
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_route_handler(() => asset_cache),
|
|
||||||
|
|
||||||
not_found
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function connect_prod() {
|
|
||||||
const asset_cache = generate_asset_cache(
|
|
||||||
read_json(path.join(dest, 'stats.client.json')),
|
|
||||||
read_json(path.join(dest, 'stats.server.json'))
|
|
||||||
);
|
|
||||||
|
|
||||||
return compose_handlers([
|
|
||||||
set_req_pathname,
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname === '/index.html',
|
|
||||||
type: 'text/html',
|
|
||||||
cache: 'max-age=600',
|
|
||||||
fn: () => asset_cache.client.index
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname === '/service-worker.js',
|
|
||||||
type: 'application/javascript',
|
|
||||||
cache: 'max-age=600',
|
|
||||||
fn: () => asset_cache.client.service_worker
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname.startsWith('/client/'),
|
|
||||||
type: 'application/javascript',
|
|
||||||
cache: 'max-age=31536000',
|
|
||||||
fn: pathname => asset_cache.client.chunks[pathname]
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_route_handler(() => asset_cache),
|
|
||||||
|
|
||||||
not_found
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = dev ? connect_dev : connect_prod;
|
|
||||||
|
|
||||||
function set_req_pathname(req, res, next) {
|
|
||||||
req.pathname = req.url.replace(/\?.+/, '');
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_asset_handler(opts) {
|
|
||||||
return (req, res, next) => {
|
|
||||||
if (!opts.filter(req.pathname)) return next();
|
|
||||||
|
|
||||||
res.set({
|
|
||||||
'Content-Type': opts.type,
|
|
||||||
'Cache-Control': opts.cache
|
|
||||||
});
|
|
||||||
res.end(opts.fn(req.pathname));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_route_handler(fn) {
|
|
||||||
return async function handle_route(req, res, next) {
|
|
||||||
const url = req.pathname;
|
|
||||||
|
|
||||||
const { client, server } = fn();
|
|
||||||
|
|
||||||
// whatever happens, we're going to serve some HTML
|
|
||||||
res.set({
|
|
||||||
'Content-Type': 'text/html'
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const route of route_manager.routes) {
|
|
||||||
if (route.test(url)) {
|
|
||||||
req.params = route.exec(url);
|
|
||||||
|
|
||||||
const mod = require(server.entry)[route.id];
|
|
||||||
|
|
||||||
if (route.type === 'page') {
|
|
||||||
let data = { params: req.params, query: req.query };
|
|
||||||
if (mod.preload) data = Object.assign(data, await mod.preload(data));
|
|
||||||
|
|
||||||
const { html, head, css } = mod.render(data);
|
|
||||||
|
|
||||||
const page = templates.render(200, {
|
|
||||||
main: client.main_file,
|
|
||||||
html,
|
|
||||||
head: `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`,
|
|
||||||
styles: (css && css.code ? `<style>${css.code}</style>` : '')
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200);
|
|
||||||
res.end(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
const handler = mod[req.method.toLowerCase()];
|
|
||||||
if (handler) handler(req, res, next);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
} catch(err) {
|
|
||||||
res.status(500).end(templates.render(500, {
|
|
||||||
title: (err && err.name) || 'Internal server error',
|
|
||||||
url,
|
|
||||||
error: escape_html(err && (err.details || err.message || err) || 'Unknown error'),
|
|
||||||
stack: err && err.stack.split('\n').slice(1).join('\n')
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function not_found(req, res) {
|
|
||||||
res.status(404).end(templates.render(404, {
|
|
||||||
title: 'Not found',
|
|
||||||
status: 404,
|
|
||||||
method: req.method,
|
|
||||||
url
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function compose_handlers(handlers) {
|
|
||||||
return (req, res, next) => {
|
|
||||||
let i = 0;
|
|
||||||
function go() {
|
|
||||||
const handler = handlers[i];
|
|
||||||
|
|
||||||
if (handler) {
|
|
||||||
handler(req, res, () => {
|
|
||||||
i += 1;
|
|
||||||
go();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
go();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function read_json(file) {
|
|
||||||
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
const glob = require('glob');
|
|
||||||
const create_routes = require('./utils/create_routes.js');
|
|
||||||
const { src } = require('./config.js');
|
|
||||||
|
|
||||||
const route_manager = {
|
|
||||||
routes: create_routes(
|
|
||||||
glob.sync('**/*.+(html|js|mjs)', { cwd: src })
|
|
||||||
),
|
|
||||||
|
|
||||||
onchange(fn) {
|
|
||||||
// TODO in dev mode, keep this updated, and allow
|
|
||||||
// webpack compiler etc to hook into it
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = route_manager;
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const glob = require('glob');
|
|
||||||
|
|
||||||
const templates = glob.sync('*.html', { cwd: 'templates' })
|
|
||||||
.map(file => {
|
|
||||||
const template = fs.readFileSync(`templates/${file}`, 'utf-8');
|
|
||||||
const status = file.replace('.html', '').toLowerCase();
|
|
||||||
|
|
||||||
if (!/^[0-9x]{3}$/.test(status)) {
|
|
||||||
throw new Error(`Bad template — should be a valid status code like 404.html, or a wildcard like 2xx.html`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const specificity = (
|
|
||||||
(status[0] === 'x' ? 0 : 4) +
|
|
||||||
(status[1] === 'x' ? 0 : 2) +
|
|
||||||
(status[2] === 'x' ? 0 : 1)
|
|
||||||
);
|
|
||||||
|
|
||||||
const pattern = new RegExp(`^${status.split('').map(d => d === 'x' ? '\\d' : d).join('')}$`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
test: status => pattern.test(status),
|
|
||||||
specificity,
|
|
||||||
render(data) {
|
|
||||||
return template.replace(/%sapper\.(\w+)%/g, (match, key) => {
|
|
||||||
return key in data ? data[key] : '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.sort((a, b) => b.specificity - a.specificity);
|
|
||||||
|
|
||||||
exports.render = (status, data) => {
|
|
||||||
const template = templates.find(template => template.test(status));
|
|
||||||
if (template) return template.render(data);
|
|
||||||
|
|
||||||
return `Missing template for status code ${status}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.onchange = fn => {
|
|
||||||
// TODO in dev mode, keep this updated, and allow
|
|
||||||
// webpack compiler etc to hook into it
|
|
||||||
};
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const relative = require('require-relative');
|
|
||||||
const webpack = relative('webpack', process.cwd());
|
|
||||||
|
|
||||||
exports.client = webpack(
|
|
||||||
require(path.resolve('webpack.client.config.js'))
|
|
||||||
);
|
|
||||||
|
|
||||||
exports.server = webpack(
|
|
||||||
require(path.resolve('webpack.server.config.js'))
|
|
||||||
);
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const route_manager = require('../route_manager.js');
|
|
||||||
const { src, dest, server_routes, dev } = require('../config.js');
|
|
||||||
|
|
||||||
module.exports = function create_app() {
|
|
||||||
const { routes } = route_manager;
|
|
||||||
|
|
||||||
function create_client_main() {
|
|
||||||
const template = fs.readFileSync('templates/main.js', 'utf-8');
|
|
||||||
|
|
||||||
const code = `[${
|
|
||||||
routes
|
|
||||||
.filter(route => route.type === 'page')
|
|
||||||
.map(route => {
|
|
||||||
const params = route.dynamic.length === 0 ?
|
|
||||||
'{}' :
|
|
||||||
`{ ${route.dynamic.map((part, i) => `${part}: match[${i + 1}]`).join(', ') } }`;
|
|
||||||
|
|
||||||
return `{ pattern: ${route.pattern}, params: match => (${params}), load: () => import(/* webpackChunkName: "${route.id}" */ '${src}/${route.file}') }`
|
|
||||||
})
|
|
||||||
.join(', ')
|
|
||||||
}]`;
|
|
||||||
|
|
||||||
let main = template
|
|
||||||
.replace(/__app__/g, path.resolve(__dirname, '../../runtime/app.js'))
|
|
||||||
.replace(/__routes__/g, code)
|
|
||||||
.replace(/__dev__/g, String(dev));
|
|
||||||
|
|
||||||
if (dev) {
|
|
||||||
const hmr_client = require.resolve(`webpack-hot-middleware/client`);
|
|
||||||
main += `\n\nimport('${hmr_client}?path=/__webpack_hmr&timeout=20000'); if (module.hot) module.hot.accept();`
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = path.resolve(dest, 'main.js');
|
|
||||||
|
|
||||||
fs.writeFileSync(file, main);
|
|
||||||
|
|
||||||
// need to fudge the mtime, because webpack is soft in the head
|
|
||||||
const { atime, mtime } = fs.statSync(file);
|
|
||||||
fs.utimesSync(file, new Date(atime.getTime() - 999999), new Date(mtime.getTime() - 999999));
|
|
||||||
}
|
|
||||||
|
|
||||||
function create_server_routes() {
|
|
||||||
const imports = routes
|
|
||||||
.map(route => {
|
|
||||||
return route.type === 'page' ?
|
|
||||||
`import ${route.id} from '${src}/${route.file}';` :
|
|
||||||
`import * as ${route.id} from '${src}/${route.file}';`;
|
|
||||||
})
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
const exports = `export { ${routes.map(route => route.id)} };`;
|
|
||||||
|
|
||||||
fs.writeFileSync(server_routes, `${imports}\n\n${exports}`);
|
|
||||||
|
|
||||||
const { atime, mtime } = fs.statSync(server_routes);
|
|
||||||
fs.utimesSync(server_routes, new Date(atime.getTime() - 999999), new Date(mtime.getTime() - 999999));
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO in dev mode, watch files
|
|
||||||
create_client_main();
|
|
||||||
create_server_routes();
|
|
||||||
};
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
|
|
||||||
module.exports = function create_matchers(files) {
|
|
||||||
return files
|
|
||||||
.map(file => {
|
|
||||||
if (/(^|\/|\\)_/.test(file)) return;
|
|
||||||
|
|
||||||
const parts = file.replace(/\.(html|js|mjs)$/, '').split(path.sep);
|
|
||||||
if (parts[parts.length - 1] === 'index') parts.pop();
|
|
||||||
|
|
||||||
const id = (
|
|
||||||
parts.join('_').replace(/[[\]]/g, '$').replace(/^\d/, '_$&').replace(/[^a-zA-Z0-9_$]/g, '_')
|
|
||||||
) || '_';
|
|
||||||
|
|
||||||
const dynamic = parts
|
|
||||||
.filter(part => part[0] === '[')
|
|
||||||
.map(part => part.slice(1, -1));
|
|
||||||
|
|
||||||
const pattern = new RegExp(
|
|
||||||
`^\\/${parts.map(p => p[0] === '[' ? '([^/]+)' : p).join('\\/')}$`
|
|
||||||
);
|
|
||||||
|
|
||||||
const test = url => pattern.test(url);
|
|
||||||
|
|
||||||
const exec = url => {
|
|
||||||
const match = pattern.exec(url);
|
|
||||||
if (!match) return;
|
|
||||||
|
|
||||||
const params = {};
|
|
||||||
dynamic.forEach((param, i) => {
|
|
||||||
params[param] = match[i + 1];
|
|
||||||
});
|
|
||||||
|
|
||||||
return params;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
type: path.extname(file) === '.html' ? 'page' : 'route',
|
|
||||||
file,
|
|
||||||
pattern,
|
|
||||||
test,
|
|
||||||
exec,
|
|
||||||
parts,
|
|
||||||
dynamic
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter(Boolean)
|
|
||||||
.sort((a, b) => {
|
|
||||||
return (
|
|
||||||
(a.dynamic.length - b.dynamic.length) || // match static paths first
|
|
||||||
(b.parts.length - a.parts.length) // match longer paths first
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const chalk = require('chalk');
|
|
||||||
const compilers = require('./compilers.js');
|
|
||||||
const generate_asset_cache = require('./generate_asset_cache.js');
|
|
||||||
|
|
||||||
function deferred() {
|
|
||||||
const d = {};
|
|
||||||
|
|
||||||
d.promise = new Promise((fulfil, reject) => {
|
|
||||||
d.fulfil = fulfil;
|
|
||||||
d.reject = reject;
|
|
||||||
});
|
|
||||||
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function create_watcher() {
|
|
||||||
const deferreds = {
|
|
||||||
client: deferred(),
|
|
||||||
server: deferred()
|
|
||||||
};
|
|
||||||
|
|
||||||
const invalidate = () => Promise.all([
|
|
||||||
deferreds.client.promise,
|
|
||||||
deferreds.server.promise
|
|
||||||
]).then(([client_stats, server_stats]) => {
|
|
||||||
return generate_asset_cache(
|
|
||||||
client_stats.toJson(),
|
|
||||||
server_stats.toJson()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
watcher = {
|
|
||||||
ready: invalidate()
|
|
||||||
};
|
|
||||||
|
|
||||||
function watch_compiler(type) {
|
|
||||||
const compiler = compilers[type];
|
|
||||||
|
|
||||||
compiler.plugin('invalid', filename => {
|
|
||||||
console.log(chalk.red(`${type} bundle invalidated, file changed: ${chalk.bold(filename)}`));
|
|
||||||
deferreds[type] = deferred();
|
|
||||||
watcher.ready = invalidate();
|
|
||||||
});
|
|
||||||
|
|
||||||
compiler.plugin('failed', err => {
|
|
||||||
deferreds[type].reject(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
compiler.watch({}, (err, stats) => {
|
|
||||||
if (stats.hasErrors()) {
|
|
||||||
deferreds[type].reject(stats.toJson().errors[0]);
|
|
||||||
} else {
|
|
||||||
deferreds[type].fulfil(stats);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
watch_compiler('client');
|
|
||||||
watch_compiler('server');
|
|
||||||
|
|
||||||
return watcher;
|
|
||||||
};
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const glob = require('glob');
|
|
||||||
const templates = require('../templates.js');
|
|
||||||
const route_manager = require('../route_manager.js');
|
|
||||||
const { dest } = require('../config.js');
|
|
||||||
|
|
||||||
module.exports = function generate_asset_cache(clientInfo, serverInfo) {
|
|
||||||
const main_file = `/client/${clientInfo.assetsByChunkName.main}`;
|
|
||||||
const chunk_files = clientInfo.assets.map(chunk => `/client/${chunk.name}`);
|
|
||||||
|
|
||||||
const service_worker = generate_service_worker(chunk_files);
|
|
||||||
const index = generate_index(main_file);
|
|
||||||
|
|
||||||
fs.writeFileSync(path.join(dest, 'service-worker.js'), service_worker);
|
|
||||||
fs.writeFileSync(path.join(dest, 'index.html'), index);
|
|
||||||
|
|
||||||
return {
|
|
||||||
client: {
|
|
||||||
main_file,
|
|
||||||
chunk_files,
|
|
||||||
|
|
||||||
main: read(`${dest}${main_file}`),
|
|
||||||
chunks: chunk_files.reduce((lookup, file) => {
|
|
||||||
lookup[file] = read(`${dest}${file}`);
|
|
||||||
return lookup;
|
|
||||||
}, {}),
|
|
||||||
|
|
||||||
index,
|
|
||||||
service_worker
|
|
||||||
},
|
|
||||||
|
|
||||||
server: {
|
|
||||||
entry: path.resolve(dest, 'server', serverInfo.assetsByChunkName.server_routes)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
function generate_service_worker(chunk_files) {
|
|
||||||
const assets = glob.sync('**', { cwd: 'assets', nodir: true });
|
|
||||||
|
|
||||||
const route_code = `[${
|
|
||||||
route_manager.routes
|
|
||||||
.filter(route => route.type === 'page')
|
|
||||||
.map(route => `{ pattern: ${route.pattern} }`)
|
|
||||||
.join(', ')
|
|
||||||
}]`;
|
|
||||||
|
|
||||||
return read('templates/service-worker.js')
|
|
||||||
.replace('__timestamp__', Date.now())
|
|
||||||
.replace('__assets__', JSON.stringify(assets))
|
|
||||||
.replace('__shell__', JSON.stringify(chunk_files.concat('/index.html')))
|
|
||||||
.replace('__routes__', route_code);
|
|
||||||
}
|
|
||||||
|
|
||||||
function generate_index(main_file) {
|
|
||||||
return templates.render(200, {
|
|
||||||
styles: '',
|
|
||||||
head: '',
|
|
||||||
html: '<noscript>Please enable JavaScript!</noscript>',
|
|
||||||
main: main_file
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function read(file) {
|
|
||||||
return fs.readFileSync(file, 'utf-8');
|
|
||||||
}
|
|
||||||
@@ -1,2 +1,5 @@
|
|||||||
|
--require source-map-support/register
|
||||||
|
--require ts-node/register
|
||||||
--recursive
|
--recursive
|
||||||
test/unit/**/*.js
|
test/unit/*/*.ts
|
||||||
|
test/common/test.js
|
||||||
8371
package-lock.json
generated
8371
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
87
package.json
87
package.json
@@ -1,41 +1,82 @@
|
|||||||
{
|
{
|
||||||
"name": "sapper",
|
"name": "sapper",
|
||||||
"version": "0.2.2",
|
"version": "0.20.3",
|
||||||
"description": "Military-grade apps, engineered by Svelte",
|
"description": "Military-grade apps, engineered by Svelte",
|
||||||
"main": "lib/index.js",
|
"main": "dist/middleware.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"sapper": "cli/index.js"
|
"sapper": "./sapper"
|
||||||
},
|
},
|
||||||
|
"files": [
|
||||||
|
"*.js",
|
||||||
|
"runtime",
|
||||||
|
"webpack",
|
||||||
|
"config",
|
||||||
|
"sapper",
|
||||||
|
"components",
|
||||||
|
"dist/*.js"
|
||||||
|
],
|
||||||
"directories": {
|
"directories": {
|
||||||
"test": "test"
|
"test": "test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^2.3.0",
|
"html-minifier": "^3.5.16",
|
||||||
"escape-html": "^1.0.3",
|
"shimport": "0.0.11",
|
||||||
"mkdirp": "^0.5.1",
|
"source-map-support": "^0.5.6",
|
||||||
"relative": "^3.0.2",
|
"sourcemap-codec": "^1.4.1",
|
||||||
"require-relative": "^0.8.7",
|
"string-hash": "^1.1.3",
|
||||||
"rimraf": "^2.6.2",
|
"tslib": "^1.9.1"
|
||||||
"webpack": "^3.10.0",
|
|
||||||
"webpack-hot-middleware": "^2.21.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cypress": "^1.3.0",
|
"@types/glob": "^5.0.34",
|
||||||
|
"@types/mkdirp": "^0.5.2",
|
||||||
|
"@types/mocha": "^5.2.5",
|
||||||
|
"@types/node": "^10.7.1",
|
||||||
|
"@types/rimraf": "^2.0.2",
|
||||||
|
"cheap-watch": "^0.3.0",
|
||||||
|
"compression": "^1.7.1",
|
||||||
|
"cookie": "^0.3.1",
|
||||||
|
"devalue": "^1.0.4",
|
||||||
"eslint": "^4.13.1",
|
"eslint": "^4.13.1",
|
||||||
"mocha": "^4.0.1",
|
"eslint-plugin-import": "^2.12.0",
|
||||||
"npm-run-all": "^4.1.2",
|
"express": "^4.16.3",
|
||||||
"wait-on": "^2.0.2"
|
"kleur": "^2.0.1",
|
||||||
|
"mkdirp": "^0.5.1",
|
||||||
|
"mocha": "^5.2.0",
|
||||||
|
"nightmare": "^3.0.0",
|
||||||
|
"node-fetch": "^2.1.1",
|
||||||
|
"npm-run-all": "^4.1.3",
|
||||||
|
"polka": "^0.4.0",
|
||||||
|
"port-authority": "^1.0.5",
|
||||||
|
"pretty-bytes": "^5.0.0",
|
||||||
|
"pretty-ms": "^3.1.0",
|
||||||
|
"require-relative": "^0.8.7",
|
||||||
|
"rimraf": "^2.6.2",
|
||||||
|
"rollup": "^0.65.0",
|
||||||
|
"rollup-plugin-commonjs": "^9.1.3",
|
||||||
|
"rollup-plugin-json": "^3.0.0",
|
||||||
|
"rollup-plugin-node-resolve": "^3.3.0",
|
||||||
|
"rollup-plugin-string": "^2.0.2",
|
||||||
|
"rollup-plugin-typescript": "^0.8.1",
|
||||||
|
"sade": "^1.4.1",
|
||||||
|
"sander": "^0.6.0",
|
||||||
|
"serve-static": "^1.13.2",
|
||||||
|
"svelte": "^2.6.3",
|
||||||
|
"svelte-loader": "^2.9.0",
|
||||||
|
"tiny-glob": "^0.2.2",
|
||||||
|
"ts-node": "^7.0.1",
|
||||||
|
"typescript": "^2.8.3",
|
||||||
|
"walk-sync": "^0.3.2",
|
||||||
|
"webpack": "^4.8.3",
|
||||||
|
"webpack-format-messages": "^2.0.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"cy:open": "cypress open",
|
"cy:open": "cypress open",
|
||||||
"test": "run-s test:unit test:dev test:prod",
|
"test": "mocha --opts mocha.opts",
|
||||||
"test:unit": "mocha --opts mocha.opts",
|
"pretest": "npm run build",
|
||||||
"test:dev": "run-p --race test:launch:dev cy:run:dev",
|
"build": "rm -rf dist && rollup -c",
|
||||||
"test:launch:dev": "node test/launch.js --dev",
|
"dev": "rollup -cw",
|
||||||
"cy:run:dev": "wait-on http://localhost:3000 && cypress run -s test/cypress/integration/dev.js",
|
"prepublishOnly": "npm test",
|
||||||
"test:prod": "run-p --race test:launch:prod cy:run:prod",
|
"update_mime_types": "curl http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types | grep -e \"^[^#]\" > src/middleware/mime-types.md"
|
||||||
"test:launch:prod": "node test/launch.js --prod",
|
|
||||||
"cy:run:prod": "wait-on http://localhost:3000 && cypress run -s test/cypress/integration/prod.js"
|
|
||||||
},
|
},
|
||||||
"repository": "https://github.com/sveltejs/sapper",
|
"repository": "https://github.com/sveltejs/sapper",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
57
rollup.config.js
Normal file
57
rollup.config.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import typescript from 'rollup-plugin-typescript';
|
||||||
|
import string from 'rollup-plugin-string';
|
||||||
|
import json from 'rollup-plugin-json';
|
||||||
|
import resolve from 'rollup-plugin-node-resolve';
|
||||||
|
import commonjs from 'rollup-plugin-commonjs';
|
||||||
|
import pkg from './package.json';
|
||||||
|
|
||||||
|
const external = [].concat(
|
||||||
|
Object.keys(pkg.dependencies),
|
||||||
|
Object.keys(process.binding('natives')),
|
||||||
|
'sapper/core.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
input: `src/runtime/index.ts`,
|
||||||
|
output: {
|
||||||
|
file: `runtime.js`,
|
||||||
|
format: 'es'
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
typescript({
|
||||||
|
typescript: require('typescript'),
|
||||||
|
target: "ES2017"
|
||||||
|
})
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
input: [
|
||||||
|
`src/api.ts`,
|
||||||
|
`src/cli.ts`,
|
||||||
|
`src/core.ts`,
|
||||||
|
`src/middleware.ts`,
|
||||||
|
`src/rollup.ts`,
|
||||||
|
`src/webpack.ts`
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
dir: 'dist',
|
||||||
|
format: 'cjs',
|
||||||
|
sourcemap: true
|
||||||
|
},
|
||||||
|
external,
|
||||||
|
plugins: [
|
||||||
|
string({
|
||||||
|
include: '**/*.md'
|
||||||
|
}),
|
||||||
|
json(),
|
||||||
|
resolve(),
|
||||||
|
commonjs(),
|
||||||
|
typescript({
|
||||||
|
typescript: require('typescript')
|
||||||
|
})
|
||||||
|
],
|
||||||
|
experimentalCodeSplitting: true
|
||||||
|
}
|
||||||
|
];
|
||||||
1
runtime/README.md
Normal file
1
runtime/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
This directory exists for legacy reasons and should be deleted before releasing version 1.
|
||||||
194
runtime/app.js
194
runtime/app.js
@@ -1,192 +1,2 @@
|
|||||||
const detach = node => {
|
console.error('sapper/runtime/app.js has been deprecated in favour of sapper/runtime.js');
|
||||||
node.parentNode.removeChild(node);
|
export * from '../runtime.js';
|
||||||
};
|
|
||||||
|
|
||||||
export let component;
|
|
||||||
let target;
|
|
||||||
let routes;
|
|
||||||
|
|
||||||
const scroll_history = {};
|
|
||||||
let uid = 1;
|
|
||||||
let cid;
|
|
||||||
|
|
||||||
if ('scrollRestoration' in history) {
|
|
||||||
history.scrollRestoration = 'manual'
|
|
||||||
}
|
|
||||||
|
|
||||||
function select_route(url) {
|
|
||||||
if (url.origin !== window.location.origin) return null;
|
|
||||||
|
|
||||||
for (const route of routes) {
|
|
||||||
const match = route.pattern.exec(url.pathname);
|
|
||||||
if (match) {
|
|
||||||
const params = route.params(match);
|
|
||||||
|
|
||||||
const query = {};
|
|
||||||
for (const [key, value] of url.searchParams) query[key] = value || true;
|
|
||||||
|
|
||||||
return { route, data: { params, query } };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function render(Component, data, scroll) {
|
|
||||||
Promise.resolve(
|
|
||||||
Component.preload ? Component.preload(data) : {}
|
|
||||||
).then(preloaded => {
|
|
||||||
if (component) {
|
|
||||||
component.destroy();
|
|
||||||
} else {
|
|
||||||
// first load — remove SSR'd <head> contents
|
|
||||||
const start = document.querySelector('#sapper-head-start');
|
|
||||||
let end = document.querySelector('#sapper-head-end');
|
|
||||||
|
|
||||||
if (start && end) {
|
|
||||||
while (start.nextSibling !== end) detach(start.nextSibling);
|
|
||||||
detach(start);
|
|
||||||
detach(end);
|
|
||||||
}
|
|
||||||
|
|
||||||
// preload additional routes
|
|
||||||
routes.reduce((promise, route) => promise.then(route.load), Promise.resolve());
|
|
||||||
}
|
|
||||||
|
|
||||||
component = new Component({
|
|
||||||
target,
|
|
||||||
data: Object.assign(data, preloaded),
|
|
||||||
hydrate: !!component
|
|
||||||
});
|
|
||||||
|
|
||||||
if (scroll) {
|
|
||||||
window.scrollTo(scroll.x, scroll.y);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigate(url, id) {
|
|
||||||
const selected = select_route(url);
|
|
||||||
if (selected) {
|
|
||||||
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 };
|
|
||||||
|
|
||||||
history.pushState({ id }, '', url.href);
|
|
||||||
}
|
|
||||||
|
|
||||||
selected.route.load().then(mod => {
|
|
||||||
render(mod.default, selected.data, scroll_history[id]);
|
|
||||||
});
|
|
||||||
|
|
||||||
cid = id;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
const 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
|
|
||||||
const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
|
|
||||||
const href = 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;
|
|
||||||
|
|
||||||
const url = new URL(href);
|
|
||||||
|
|
||||||
// Don't handle hash changes
|
|
||||||
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
|
|
||||||
|
|
||||||
if (navigate(url, null)) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handle_popstate(event) {
|
|
||||||
scroll_history[cid] = scroll_state();
|
|
||||||
|
|
||||||
if (event.state) {
|
|
||||||
navigate(new URL(window.location), event.state.id);
|
|
||||||
} else {
|
|
||||||
// hashchange
|
|
||||||
cid = ++uid;
|
|
||||||
history.replaceState({ id: cid }, '', window.location.href);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function prefetch(event) {
|
|
||||||
const a = findAnchor(event.target);
|
|
||||||
if (!a || a.rel !== 'prefetch') return;
|
|
||||||
|
|
||||||
const selected = select_route(new URL(a.href));
|
|
||||||
|
|
||||||
if (selected) {
|
|
||||||
selected.route.load().then(mod => {
|
|
||||||
if (mod.default.preload) mod.default.preload(selected.data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function findAnchor(node) {
|
|
||||||
while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG <a> elements have a lowercase name
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
let inited;
|
|
||||||
|
|
||||||
export function init(_target, _routes) {
|
|
||||||
target = _target;
|
|
||||||
routes = _routes;
|
|
||||||
|
|
||||||
if (!inited) { // this check makes HMR possible
|
|
||||||
window.addEventListener('click', handle_click);
|
|
||||||
window.addEventListener('popstate', handle_popstate);
|
|
||||||
|
|
||||||
// prefetch
|
|
||||||
window.addEventListener('touchstart', prefetch);
|
|
||||||
window.addEventListener('mouseover', prefetch);
|
|
||||||
|
|
||||||
inited = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scroll = scroll_history[uid] = scroll_state();
|
|
||||||
|
|
||||||
history.replaceState({ id: uid }, '', window.location.href);
|
|
||||||
navigate(new URL(window.location), uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
function which(event) {
|
|
||||||
event = event || window.event;
|
|
||||||
return event.which === null ? event.button : event.which;
|
|
||||||
}
|
|
||||||
|
|
||||||
function scroll_state() {
|
|
||||||
return {
|
|
||||||
x: window.scrollX,
|
|
||||||
y: window.scrollY
|
|
||||||
};
|
|
||||||
}
|
|
||||||
40
sapper-dev-client.js
Normal file
40
sapper-dev-client.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
let source;
|
||||||
|
|
||||||
|
function check() {
|
||||||
|
if (typeof module === 'undefined') return;
|
||||||
|
|
||||||
|
if (module.hot.status() === 'idle') {
|
||||||
|
module.hot.check(true).then(modules => {
|
||||||
|
console.log(`[SAPPER] applied HMR update`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function connect(port) {
|
||||||
|
if (source || !window.EventSource) return;
|
||||||
|
|
||||||
|
source = new EventSource(`http://${window.location.hostname}:${port}/__sapper__`);
|
||||||
|
|
||||||
|
window.source = source;
|
||||||
|
|
||||||
|
source.onopen = function(event) {
|
||||||
|
console.log(`[SAPPER] dev client connected`);
|
||||||
|
};
|
||||||
|
|
||||||
|
source.onerror = function(error) {
|
||||||
|
console.error(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
source.onmessage = function(event) {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (!data) return; // just a heartbeat
|
||||||
|
|
||||||
|
if (data.action === 'reload') {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status === 'completed') {
|
||||||
|
check();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
6
src/api.ts
Normal file
6
src/api.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { dev } from './api/dev';
|
||||||
|
import { build } from './api/build';
|
||||||
|
import { exporter } from './api/export';
|
||||||
|
import { find_page } from './api/find_page';
|
||||||
|
|
||||||
|
export { dev, build, exporter, find_page };
|
||||||
112
src/api/build.ts
Normal file
112
src/api/build.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import mkdirp from 'mkdirp';
|
||||||
|
import rimraf from 'rimraf';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import * as codec from 'sourcemap-codec';
|
||||||
|
import hash from 'string-hash';
|
||||||
|
import minify_html from './utils/minify_html';
|
||||||
|
import { create_compilers, create_main_manifests, create_manifest_data, create_serviceworker_manifest } from '../core';
|
||||||
|
import * as events from './interfaces';
|
||||||
|
import { copy_shimport } from './utils/copy_shimport';
|
||||||
|
import { Dirs, PageComponent } from '../interfaces';
|
||||||
|
import { CompileResult } from '../core/create_compilers/interfaces';
|
||||||
|
|
||||||
|
type Opts = {
|
||||||
|
legacy: boolean;
|
||||||
|
bundler: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function build(opts: Opts, dirs: Dirs) {
|
||||||
|
const emitter = new EventEmitter();
|
||||||
|
|
||||||
|
execute(emitter, opts, dirs).then(
|
||||||
|
() => {
|
||||||
|
emitter.emit('done', <events.DoneEvent>{}); // TODO do we need to pass back any info?
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
emitter.emit('error', <events.ErrorEvent>{
|
||||||
|
error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function execute(emitter: EventEmitter, opts: Opts, dirs: Dirs) {
|
||||||
|
rimraf.sync(path.join(dirs.dest, '**/*'));
|
||||||
|
mkdirp.sync(`${dirs.dest}/client`);
|
||||||
|
copy_shimport(dirs.dest);
|
||||||
|
|
||||||
|
// minify app/template.html
|
||||||
|
// TODO compile this to a function? could be quicker than str.replace(...).replace(...).replace(...)
|
||||||
|
const template = fs.readFileSync(`${dirs.app}/template.html`, 'utf-8');
|
||||||
|
|
||||||
|
// 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(`${dirs.dest}/template.html`, minify_html(template));
|
||||||
|
|
||||||
|
const manifest_data = create_manifest_data();
|
||||||
|
|
||||||
|
// create app/manifest/client.js and app/manifest/server.js
|
||||||
|
create_main_manifests({ bundler: opts.bundler, manifest_data });
|
||||||
|
|
||||||
|
const { client, server, serviceworker } = create_compilers(opts.bundler, dirs);
|
||||||
|
|
||||||
|
const client_result = await client.compile();
|
||||||
|
emitter.emit('build', <events.BuildEvent>{
|
||||||
|
type: 'client',
|
||||||
|
// TODO duration/warnings
|
||||||
|
result: client_result
|
||||||
|
});
|
||||||
|
|
||||||
|
const build_info = client_result.to_json(manifest_data, dirs);
|
||||||
|
|
||||||
|
if (opts.legacy) {
|
||||||
|
process.env.SAPPER_LEGACY_BUILD = 'true';
|
||||||
|
const { client } = create_compilers(opts.bundler, dirs);
|
||||||
|
|
||||||
|
const client_result = await client.compile();
|
||||||
|
|
||||||
|
emitter.emit('build', <events.BuildEvent>{
|
||||||
|
type: 'client (legacy)',
|
||||||
|
// TODO duration/warnings
|
||||||
|
result: client_result
|
||||||
|
});
|
||||||
|
|
||||||
|
build_info.legacy_assets = client_result.assets;
|
||||||
|
delete process.env.SAPPER_LEGACY_BUILD;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(dirs.dest, 'build.json'), JSON.stringify(build_info));
|
||||||
|
|
||||||
|
const server_stats = await server.compile();
|
||||||
|
emitter.emit('build', <events.BuildEvent>{
|
||||||
|
type: 'server',
|
||||||
|
// TODO duration/warnings
|
||||||
|
result: server_stats
|
||||||
|
});
|
||||||
|
|
||||||
|
let serviceworker_stats;
|
||||||
|
|
||||||
|
if (serviceworker) {
|
||||||
|
create_serviceworker_manifest({
|
||||||
|
manifest_data,
|
||||||
|
client_files: client_result.chunks.map(chunk => `client/${chunk.file}`)
|
||||||
|
});
|
||||||
|
|
||||||
|
serviceworker_stats = await serviceworker.compile();
|
||||||
|
|
||||||
|
emitter.emit('build', <events.BuildEvent>{
|
||||||
|
type: 'serviceworker',
|
||||||
|
// TODO duration/warnings
|
||||||
|
result: serviceworker_stats
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
469
src/api/dev.ts
Normal file
469
src/api/dev.ts
Normal file
@@ -0,0 +1,469 @@
|
|||||||
|
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 mkdirp from 'mkdirp';
|
||||||
|
import rimraf from 'rimraf';
|
||||||
|
import { locations } from '../config';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { create_manifest_data, create_main_manifests, create_compilers, create_serviceworker_manifest } from '../core';
|
||||||
|
import { Compiler, Compilers } from '../core/create_compilers';
|
||||||
|
import { CompileResult, CompileError } from '../core/create_compilers/interfaces';
|
||||||
|
import Deferred from './utils/Deferred';
|
||||||
|
import * as events from './interfaces';
|
||||||
|
import validate_bundler from '../cli/utils/validate_bundler';
|
||||||
|
import { copy_shimport } from './utils/copy_shimport';
|
||||||
|
import { ManifestData } from '../interfaces';
|
||||||
|
|
||||||
|
export function dev(opts) {
|
||||||
|
return new Watcher(opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Watcher extends EventEmitter {
|
||||||
|
bundler: string;
|
||||||
|
dirs: {
|
||||||
|
app: string;
|
||||||
|
dest: string;
|
||||||
|
routes: string;
|
||||||
|
rollup: string;
|
||||||
|
webpack: string;
|
||||||
|
}
|
||||||
|
port: number;
|
||||||
|
closed: boolean;
|
||||||
|
|
||||||
|
dev_port: number;
|
||||||
|
live: boolean;
|
||||||
|
hot: boolean;
|
||||||
|
|
||||||
|
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({
|
||||||
|
app = locations.app(),
|
||||||
|
dest = locations.dest(),
|
||||||
|
routes = locations.routes(),
|
||||||
|
'dev-port': dev_port,
|
||||||
|
live,
|
||||||
|
hot,
|
||||||
|
bundler,
|
||||||
|
webpack = 'webpack',
|
||||||
|
rollup = 'rollup',
|
||||||
|
port = +process.env.PORT
|
||||||
|
}: {
|
||||||
|
app: string,
|
||||||
|
dest: string,
|
||||||
|
routes: string,
|
||||||
|
'dev-port': number,
|
||||||
|
live: boolean,
|
||||||
|
hot: boolean,
|
||||||
|
bundler?: string,
|
||||||
|
webpack: string,
|
||||||
|
rollup: string,
|
||||||
|
port: number
|
||||||
|
}) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.bundler = validate_bundler(bundler);
|
||||||
|
this.dirs = { app, dest, routes, webpack, rollup };
|
||||||
|
this.port = port;
|
||||||
|
this.closed = false;
|
||||||
|
|
||||||
|
this.dev_port = dev_port;
|
||||||
|
this.live = live;
|
||||||
|
this.hot = hot;
|
||||||
|
|
||||||
|
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 = fs.readFileSync(path.join(app, 'template.html'), 'utf-8');
|
||||||
|
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', <events.FatalEvent>{
|
||||||
|
message: `Port ${this.port} is unavailable`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.port = await ports.find(3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { dest } = this.dirs;
|
||||||
|
rimraf.sync(dest);
|
||||||
|
mkdirp.sync(`${dest}/client`);
|
||||||
|
if (this.bundler === 'rollup') copy_shimport(dest);
|
||||||
|
|
||||||
|
if (!this.dev_port) this.dev_port = await ports.find(10000);
|
||||||
|
|
||||||
|
let manifest_data: ManifestData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
manifest_data = create_manifest_data();
|
||||||
|
create_main_manifests({ bundler: this.bundler, manifest_data, dev_port: this.dev_port });
|
||||||
|
} catch (err) {
|
||||||
|
this.emit('fatal', <events.FatalEvent>{
|
||||||
|
message: err.message
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dev_server = new DevServer(this.dev_port);
|
||||||
|
|
||||||
|
this.filewatchers.push(
|
||||||
|
watch_dir(
|
||||||
|
locations.routes(),
|
||||||
|
({ path: file, stats }) => {
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
return path.basename(file)[0] !== '_';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
try {
|
||||||
|
const new_manifest_data = create_manifest_data();
|
||||||
|
create_main_manifests({ bundler: this.bundler, manifest_data, dev_port: this.dev_port });
|
||||||
|
|
||||||
|
manifest_data = new_manifest_data;
|
||||||
|
} catch (err) {
|
||||||
|
this.emit('error', <events.ErrorEvent>{
|
||||||
|
message: err.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
fs.watch(`${locations.app()}/template.html`, () => {
|
||||||
|
this.dev_server.send({
|
||||||
|
action: 'reload'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let deferred = new Deferred();
|
||||||
|
|
||||||
|
// TODO watch the configs themselves?
|
||||||
|
const compilers: Compilers = create_compilers(this.bundler, this.dirs);
|
||||||
|
|
||||||
|
let log = '';
|
||||||
|
|
||||||
|
const emitFatal = () => {
|
||||||
|
this.emit('fatal', <events.FatalEvent>{
|
||||||
|
message: `Server crashed`,
|
||||||
|
log
|
||||||
|
});
|
||||||
|
|
||||||
|
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 = () => {
|
||||||
|
log = '';
|
||||||
|
this.crashed = false;
|
||||||
|
|
||||||
|
ports.wait(this.port)
|
||||||
|
.then((() => {
|
||||||
|
this.emit('ready', <events.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', <events.FatalEvent>{
|
||||||
|
message: `Server is not listening on port ${this.port}`,
|
||||||
|
log
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.proc) {
|
||||||
|
this.proc.removeListener('exit', emitFatal);
|
||||||
|
this.proc.kill();
|
||||||
|
this.proc.on('exit', restart);
|
||||||
|
} else {
|
||||||
|
restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.proc = child_process.fork(`${dest}/server.js`, [], {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
env: Object.assign({
|
||||||
|
PORT: this.port
|
||||||
|
}, process.env),
|
||||||
|
stdio: ['ipc']
|
||||||
|
});
|
||||||
|
|
||||||
|
this.proc.stdout.on('data', chunk => {
|
||||||
|
log += chunk;
|
||||||
|
this.emit('stdout', chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.proc.stderr.on('data', chunk => {
|
||||||
|
log += 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,
|
||||||
|
client_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', <events.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((err?: Error, result?: CompileResult) => {
|
||||||
|
if (err) {
|
||||||
|
this.emit('error', <events.ErrorEvent>{
|
||||||
|
type: name,
|
||||||
|
message: err.message
|
||||||
|
});
|
||||||
|
} 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 noop() {}
|
||||||
|
|
||||||
|
function watch_dir(
|
||||||
|
dir: string,
|
||||||
|
filter: ({ path, stats }: { path: string, stats: fs.Stats }) => boolean,
|
||||||
|
callback: () => void
|
||||||
|
) {
|
||||||
|
let watch;
|
||||||
|
let closed = false;
|
||||||
|
|
||||||
|
import('cheap-watch').then(CheapWatch => {
|
||||||
|
if (closed) return;
|
||||||
|
|
||||||
|
watch = new CheapWatch({ dir, filter, debounce: 50 });
|
||||||
|
|
||||||
|
watch.on('+', ({ isNew }) => {
|
||||||
|
if (isNew) callback();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch.on('-', callback);
|
||||||
|
|
||||||
|
watch.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
close: () => {
|
||||||
|
if (watch) watch.close();
|
||||||
|
closed = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
193
src/api/export.ts
Normal file
193
src/api/export.ts
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import * as child_process from 'child_process';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as sander from 'sander';
|
||||||
|
import * as url from 'url';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import * as ports from 'port-authority';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import clean_html from './utils/clean_html';
|
||||||
|
import minify_html from './utils/minify_html';
|
||||||
|
import Deferred from './utils/Deferred';
|
||||||
|
import * as events from './interfaces';
|
||||||
|
|
||||||
|
type Opts = {
|
||||||
|
build: string,
|
||||||
|
dest: string,
|
||||||
|
basepath?: string,
|
||||||
|
timeout: number | false
|
||||||
|
};
|
||||||
|
|
||||||
|
export function exporter(opts: Opts) {
|
||||||
|
const emitter = new EventEmitter();
|
||||||
|
|
||||||
|
execute(emitter, opts).then(
|
||||||
|
() => {
|
||||||
|
emitter.emit('done', <events.DoneEvent>{}); // TODO do we need to pass back any info?
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
emitter.emit('error', <events.ErrorEvent>{
|
||||||
|
error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolve(from: string, to: string) {
|
||||||
|
return url.parse(url.resolve(from, to));
|
||||||
|
}
|
||||||
|
|
||||||
|
type URL = url.UrlWithStringQuery;
|
||||||
|
|
||||||
|
async function execute(emitter: EventEmitter, opts: Opts) {
|
||||||
|
const export_dir = path.join(opts.dest, opts.basepath);
|
||||||
|
|
||||||
|
// Prep output directory
|
||||||
|
sander.rimrafSync(export_dir);
|
||||||
|
|
||||||
|
sander.copydirSync('assets').to(export_dir);
|
||||||
|
sander.copydirSync(opts.build, 'client').to(export_dir, 'client');
|
||||||
|
|
||||||
|
if (sander.existsSync(opts.build, 'service-worker.js')) {
|
||||||
|
sander.copyFileSync(opts.build, 'service-worker.js').to(export_dir, 'service-worker.js');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sander.existsSync(opts.build, 'service-worker.js.map')) {
|
||||||
|
sander.copyFileSync(opts.build, 'service-worker.js.map').to(export_dir, 'service-worker.js.map');
|
||||||
|
}
|
||||||
|
|
||||||
|
const port = await ports.find(3000);
|
||||||
|
|
||||||
|
const protocol = 'http:';
|
||||||
|
const host = `localhost:${port}`;
|
||||||
|
const origin = `${protocol}//${host}`;
|
||||||
|
|
||||||
|
const root = resolve(origin, opts.basepath || '');
|
||||||
|
if (!root.href.endsWith('/')) root.href += '/';
|
||||||
|
|
||||||
|
emitter.emit('info', {
|
||||||
|
message: `Crawling ${root.href}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const proc = child_process.fork(path.resolve(`${opts.build}/server.js`), [], {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
env: Object.assign({
|
||||||
|
PORT: port,
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
SAPPER_DEST: opts.build,
|
||||||
|
SAPPER_EXPORT: 'true'
|
||||||
|
}, process.env)
|
||||||
|
});
|
||||||
|
|
||||||
|
const seen = new Set();
|
||||||
|
const saved = new Set();
|
||||||
|
|
||||||
|
function save(path: string, status: number, type: string, body: string) {
|
||||||
|
const { pathname } = resolve(origin, path);
|
||||||
|
let file = decodeURIComponent(pathname.slice(1));
|
||||||
|
|
||||||
|
if (saved.has(file)) return;
|
||||||
|
saved.add(file);
|
||||||
|
|
||||||
|
const is_html = type === 'text/html';
|
||||||
|
|
||||||
|
if (is_html) {
|
||||||
|
file = file === '' ? 'index.html' : `${file}/index.html`;
|
||||||
|
body = minify_html(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter.emit('file', <events.FileEvent>{
|
||||||
|
file,
|
||||||
|
size: body.length,
|
||||||
|
status
|
||||||
|
});
|
||||||
|
|
||||||
|
sander.writeFileSync(export_dir, 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) {
|
||||||
|
const pathname = (url.pathname.replace(root.pathname, '') || '/');
|
||||||
|
|
||||||
|
if (seen.has(pathname)) return;
|
||||||
|
seen.add(pathname);
|
||||||
|
|
||||||
|
const timeout_deferred = new Deferred();
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
timeout_deferred.reject(new Error(`Timed out waiting for ${url.href}`));
|
||||||
|
}, opts.timeout);
|
||||||
|
|
||||||
|
const r = await Promise.race([
|
||||||
|
fetch(url.href, {
|
||||||
|
redirect: 'manual'
|
||||||
|
}),
|
||||||
|
timeout_deferred.promise
|
||||||
|
]);
|
||||||
|
|
||||||
|
clearTimeout(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') {
|
||||||
|
const urls: URL[] = [];
|
||||||
|
|
||||||
|
const cleaned = clean_html(body);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
urls.push(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(urls.map(handle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(() => proc.kill())
|
||||||
|
.catch(err => {
|
||||||
|
proc.kill();
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_href(attrs: string) {
|
||||||
|
const match = /href\s*=\s*(?:"(.+?)"|'(.+?)'|([^\s>]+))/.exec(attrs);
|
||||||
|
return match[1] || match[2] || match[3];
|
||||||
|
}
|
||||||
14
src/api/find_page.ts
Normal file
14
src/api/find_page.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { locations } from '../config';
|
||||||
|
import { create_manifest_data } from '../core';
|
||||||
|
|
||||||
|
export function find_page(pathname: string, cwd = locations.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/api/interfaces.ts
Normal file
45
src/api/interfaces.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import * as child_process from 'child_process';
|
||||||
|
import { CompileResult } from '../core/create_compilers';
|
||||||
|
|
||||||
|
export type ReadyEvent = {
|
||||||
|
port: number;
|
||||||
|
process: child_process.ChildProcess;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ErrorEvent = {
|
||||||
|
type: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FatalEvent = {
|
||||||
|
message: string;
|
||||||
|
log?: 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 = {};
|
||||||
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, '');
|
||||||
|
}
|
||||||
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'))
|
||||||
|
);
|
||||||
|
}
|
||||||
21
src/api/utils/minify_html.ts
Normal file
21
src/api/utils/minify_html.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { minify } from 'html-minifier';
|
||||||
|
|
||||||
|
export default function minify_html(html: string) {
|
||||||
|
return minify(html, {
|
||||||
|
collapseBooleanAttributes: true,
|
||||||
|
collapseWhitespace: true,
|
||||||
|
conservativeCollapse: true,
|
||||||
|
decodeEntities: true,
|
||||||
|
html5: true,
|
||||||
|
minifyCSS: true,
|
||||||
|
minifyJS: false,
|
||||||
|
removeAttributeQuotes: true,
|
||||||
|
removeComments: true,
|
||||||
|
removeOptionalTags: true,
|
||||||
|
removeRedundantAttributes: true,
|
||||||
|
removeScriptTypeAttributes: true,
|
||||||
|
removeStyleLinkTypeAttributes: true,
|
||||||
|
sortAttributes: true,
|
||||||
|
sortClassName: true
|
||||||
|
});
|
||||||
|
}
|
||||||
124
src/cli.ts
Executable file
124
src/cli.ts
Executable file
@@ -0,0 +1,124 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import sade from 'sade';
|
||||||
|
import colors from 'kleur';
|
||||||
|
import prettyMs from 'pretty-ms';
|
||||||
|
import * as pkg from '../package.json';
|
||||||
|
|
||||||
|
const prog = sade('sapper').version(pkg.version);
|
||||||
|
|
||||||
|
prog.command('dev')
|
||||||
|
.describe('Start a development server')
|
||||||
|
.option('-p, --port', 'Specify a port')
|
||||||
|
.option('-o, --open', 'Open a browser window')
|
||||||
|
.option('--dev-port', 'Specify a port for development server')
|
||||||
|
.option('--hot', 'Use hot module replacement (requires webpack)', true)
|
||||||
|
.option('--live', 'Reload on changes if not using --hot', true)
|
||||||
|
.option('--bundler', 'Specify a bundler (rollup or webpack)')
|
||||||
|
.action(async (opts: {
|
||||||
|
port: number,
|
||||||
|
open: boolean,
|
||||||
|
'dev-port': number,
|
||||||
|
live: boolean,
|
||||||
|
hot: boolean,
|
||||||
|
bundler?: string
|
||||||
|
}) => {
|
||||||
|
const { dev } = await import('./cli/dev');
|
||||||
|
dev(opts);
|
||||||
|
});
|
||||||
|
|
||||||
|
prog.command('build [dest]')
|
||||||
|
.describe('Create a production-ready version of your app')
|
||||||
|
.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')
|
||||||
|
.example(`build custom-dir -p 4567`)
|
||||||
|
.action(async (dest = 'build', opts: {
|
||||||
|
port: string,
|
||||||
|
legacy: boolean,
|
||||||
|
bundler?: string
|
||||||
|
}) => {
|
||||||
|
console.log(`> Building...`);
|
||||||
|
|
||||||
|
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
|
||||||
|
process.env.SAPPER_DEST = dest;
|
||||||
|
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { build } = await import('./cli/build');
|
||||||
|
await build(opts);
|
||||||
|
|
||||||
|
const launcher = path.resolve(dest, 'index.js');
|
||||||
|
|
||||||
|
fs.writeFileSync(launcher, `
|
||||||
|
// generated by sapper build at ${new Date().toISOString()}
|
||||||
|
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
|
||||||
|
process.env.SAPPER_DEST = __dirname;
|
||||||
|
process.env.PORT = process.env.PORT || ${opts.port || 3000};
|
||||||
|
|
||||||
|
console.log('Starting server on port ' + process.env.PORT);
|
||||||
|
require('./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}`)}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
prog.command('start [dir]')
|
||||||
|
.describe('Start your app')
|
||||||
|
.option('-p, --port', 'Specify a port')
|
||||||
|
.option('-o, --open', 'Open a browser window')
|
||||||
|
.action(async (dir = 'build', opts: { port: number, open: boolean }) => {
|
||||||
|
const { start } = await import('./cli/start');
|
||||||
|
start(dir, opts);
|
||||||
|
});
|
||||||
|
|
||||||
|
prog.command('export [dest]')
|
||||||
|
.describe('Export your app as static files (if possible)')
|
||||||
|
.option('--build', '(Re)build app before exporting', true)
|
||||||
|
.option('--build-dir', 'Specify a custom temporary build directory', '.sapper/prod')
|
||||||
|
.option('--basepath', 'Specify a base path')
|
||||||
|
.option('--timeout', 'Milliseconds to wait for a page (--no-timeout to disable)', 5000)
|
||||||
|
.option('--legacy', 'Create separate legacy build')
|
||||||
|
.option('--bundler', 'Specify a bundler (rollup or webpack, blank for auto)')
|
||||||
|
.action(async (dest = 'export', opts: {
|
||||||
|
build: boolean,
|
||||||
|
legacy: boolean,
|
||||||
|
bundler?: string,
|
||||||
|
'build-dir': string,
|
||||||
|
basepath?: string,
|
||||||
|
timeout: number | false
|
||||||
|
}) => {
|
||||||
|
process.env.NODE_ENV = 'production';
|
||||||
|
process.env.SAPPER_DEST = opts['build-dir'];
|
||||||
|
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (opts.build) {
|
||||||
|
console.log(`> Building...`);
|
||||||
|
const { build } = await import('./cli/build');
|
||||||
|
await build(opts);
|
||||||
|
console.error(`\n> Built in ${elapsed(start)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { exporter } = await import('./cli/export');
|
||||||
|
await exporter(dest, opts);
|
||||||
|
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold.cyan(`npx serve ${dest}`)} to run the app.`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(colors.bold.red(`> ${err.message}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO upgrade
|
||||||
|
|
||||||
|
prog.parse(process.argv);
|
||||||
|
|
||||||
|
function elapsed(start: number) {
|
||||||
|
return prettyMs(Date.now() - start);
|
||||||
|
}
|
||||||
56
src/cli/build.ts
Normal file
56
src/cli/build.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { build as _build } from '../api/build';
|
||||||
|
import colors from 'kleur';
|
||||||
|
import { locations } from '../config';
|
||||||
|
import validate_bundler from './utils/validate_bundler';
|
||||||
|
import { repeat } from '../utils';
|
||||||
|
|
||||||
|
export function build(opts: { bundler?: string, legacy?: boolean }) {
|
||||||
|
const bundler = validate_bundler(opts.bundler);
|
||||||
|
|
||||||
|
if (opts.legacy && bundler === 'webpack') {
|
||||||
|
throw new Error(`Legacy builds are not supported for projects using webpack`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((fulfil, reject) => {
|
||||||
|
try {
|
||||||
|
const emitter = _build({
|
||||||
|
legacy: opts.legacy,
|
||||||
|
bundler
|
||||||
|
}, {
|
||||||
|
dest: locations.dest(),
|
||||||
|
app: locations.app(),
|
||||||
|
routes: locations.routes(),
|
||||||
|
webpack: 'webpack',
|
||||||
|
rollup: 'rollup'
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('build', event => {
|
||||||
|
let banner = `built ${event.type}`;
|
||||||
|
let c = colors.cyan;
|
||||||
|
|
||||||
|
const { warnings } = event.result;
|
||||||
|
if (warnings.length > 0) {
|
||||||
|
banner += ` with ${warnings.length} ${warnings.length === 1 ? 'warning' : 'warnings'}`;
|
||||||
|
c = colors.yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('error', event => {
|
||||||
|
reject(event.error);
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('done', event => {
|
||||||
|
fulfil();
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
80
src/cli/dev.ts
Normal file
80
src/cli/dev.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import colors from 'kleur';
|
||||||
|
import * as child_process from 'child_process';
|
||||||
|
import prettyMs from 'pretty-ms';
|
||||||
|
import { dev as _dev } from '../api/dev';
|
||||||
|
import * as events from '../api/interfaces';
|
||||||
|
|
||||||
|
export function dev(opts: { port: number, open: boolean, bundler?: string }) {
|
||||||
|
try {
|
||||||
|
const watcher = _dev(opts);
|
||||||
|
|
||||||
|
let first = true;
|
||||||
|
|
||||||
|
watcher.on('ready', (event: events.ReadyEvent) => {
|
||||||
|
if (first) {
|
||||||
|
console.log(colors.bold.cyan(`> Listening on http://localhost:${event.port}`));
|
||||||
|
if (opts.open) child_process.exec(`open http://localhost:${event.port}`);
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO clear screen?
|
||||||
|
|
||||||
|
event.process.stdout.on('data', data => {
|
||||||
|
process.stdout.write(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
event.process.stderr.on('data', data => {
|
||||||
|
process.stderr.write(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.on('invalid', (event: events.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: events.ErrorEvent) => {
|
||||||
|
console.log(colors.red(`✗ ${event.type}`));
|
||||||
|
console.log(colors.red(event.message));
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.on('fatal', (event: events.FatalEvent) => {
|
||||||
|
console.log(colors.bold.red(`> ${event.message}`));
|
||||||
|
if (event.log) console.log(event.log);
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.on('build', (event: events.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(`(${prettyMs(event.duration)})`)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(colors.bold.red(`> ${err.message}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/cli/export.ts
Normal file
50
src/cli/export.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { exporter as _exporter } from '../api/export';
|
||||||
|
import colors from 'kleur';
|
||||||
|
import pb from 'pretty-bytes';
|
||||||
|
import { locations } from '../config';
|
||||||
|
import { left_pad } from '../utils';
|
||||||
|
|
||||||
|
export function exporter(export_dir: string, {
|
||||||
|
basepath = '',
|
||||||
|
timeout
|
||||||
|
}: {
|
||||||
|
basepath: string,
|
||||||
|
timeout: number | false
|
||||||
|
}) {
|
||||||
|
return new Promise((fulfil, reject) => {
|
||||||
|
try {
|
||||||
|
const emitter = _exporter({
|
||||||
|
build: locations.dest(),
|
||||||
|
dest: export_dir,
|
||||||
|
basepath,
|
||||||
|
timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('file', 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}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('info', event => {
|
||||||
|
console.log(colors.bold.cyan(`> ${event.message}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('error', event => {
|
||||||
|
reject(event.error);
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('done', event => {
|
||||||
|
fulfil();
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`${colors.bold.red(`> ${err.message}`)}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
39
src/cli/start.ts
Normal file
39
src/cli/start.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as child_process from 'child_process';
|
||||||
|
import colors from 'kleur';
|
||||||
|
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(`${colors.bold.red(`> ${dir}/server.js does not exist — type ${colors.bold.cyan(dir === 'build' ? `npx sapper build` : `npx sapper build ${dir}`)} to create it`)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port) {
|
||||||
|
if (!await ports.check(port)) {
|
||||||
|
console.log(`${colors.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(`${colors.bold.cyan(`> Listening on http://localhost:${port}`)}`);
|
||||||
|
if (opts.open) child_process.exec(`open http://localhost:${port}`);
|
||||||
|
}
|
||||||
53
src/cli/upgrade.ts
Normal file
53
src/cli/upgrade.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import colors from 'kleur';
|
||||||
|
|
||||||
|
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(`${colors.red(`Could not replace %sapper.main% in ${file}`)}`);
|
||||||
|
} else {
|
||||||
|
write(file, template.replace(pattern, `%sapper.scripts%`));
|
||||||
|
console.log(`${colors.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);
|
||||||
|
}
|
||||||
21
src/cli/utils/validate_bundler.ts
Normal file
21
src/cli/utils/validate_bundler.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
export default function validate_bundler(bundler?: string) {
|
||||||
|
if (!bundler) {
|
||||||
|
bundler = (
|
||||||
|
fs.existsSync('rollup') ? 'rollup' :
|
||||||
|
fs.existsSync('webpack') ? 'webpack' :
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!bundler) {
|
||||||
|
throw new Error(`Could not find a 'rollup' or 'webpack' directory`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bundler !== 'rollup' && bundler !== 'webpack') {
|
||||||
|
throw new Error(`'${bundler}' is not a valid option for --bundler — must be either 'rollup' or 'webpack'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bundler;
|
||||||
|
}
|
||||||
10
src/config.ts
Normal file
10
src/config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
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/${dev() ? 'dev' : 'prod'}`)
|
||||||
|
};
|
||||||
3
src/core.ts
Normal file
3
src/core.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './core/create_manifests';
|
||||||
|
export { default as create_compilers } from './core/create_compilers/index';
|
||||||
|
export { default as create_manifest_data } from './core/create_manifest_data';
|
||||||
160
src/core/create_compilers/RollupCompiler.ts
Normal file
160
src/core/create_compilers/RollupCompiler.ts
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
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: string) {
|
||||||
|
this._ = this.get_config(path.resolve(config));
|
||||||
|
this.input = null;
|
||||||
|
this.warnings = [];
|
||||||
|
this.errors = [];
|
||||||
|
this.chunks = [];
|
||||||
|
this.css_files = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async get_config(input: string) {
|
||||||
|
if (!rollup) rollup = relative('rollup', process.cwd());
|
||||||
|
|
||||||
|
const bundle = await rollup.rollup({
|
||||||
|
input,
|
||||||
|
external: (id: string) => {
|
||||||
|
return (id[0] !== '.' && !path.isAbsolute(id)) || id.slice(-5, id.length) === '.json';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { code } = await bundle.generate({ format: 'cjs' });
|
||||||
|
|
||||||
|
// 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 mod: any = require(input);
|
||||||
|
delete require.cache[input];
|
||||||
|
|
||||||
|
(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}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/core/create_compilers/RollupResult.ts
Normal file
111
src/core/create_compilers/RollupResult.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
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, PageComponent } 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 = {};
|
||||||
|
|
||||||
|
compiler.chunks.forEach(chunk => {
|
||||||
|
if (compiler.input in chunk.modules) {
|
||||||
|
this.assets.main = 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')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
47
src/core/create_compilers/WebpackCompiler.ts
Normal file
47
src/core/create_compilers/WebpackCompiler.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import relative from 'require-relative';
|
||||||
|
import { CompileResult } from './interfaces';
|
||||||
|
import WebpackResult from './WebpackResult';
|
||||||
|
|
||||||
|
let webpack: any;
|
||||||
|
|
||||||
|
export class WebpackCompiler {
|
||||||
|
_: any;
|
||||||
|
|
||||||
|
constructor(config: string) {
|
||||||
|
if (!webpack) webpack = relative('webpack', process.cwd());
|
||||||
|
this._ = webpack(require(path.resolve(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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/core/create_compilers/WebpackResult.ts
Normal file
73
src/core/create_compilers/WebpackResult.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import format_messages from 'webpack-format-messages';
|
||||||
|
import { CompileResult, BuildInfo, CompileError, Chunk, CssFile } from './interfaces';
|
||||||
|
import { ManifestData, Dirs } 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 {
|
||||||
|
return {
|
||||||
|
bundler: 'webpack',
|
||||||
|
shimport: null, // webpack has its own loader
|
||||||
|
assets: this.assets,
|
||||||
|
css: {
|
||||||
|
// TODO
|
||||||
|
main: null,
|
||||||
|
chunks: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
print() {
|
||||||
|
return this.stats.toString({ colors: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
230
src/core/create_compilers/extract_css.ts
Normal file
230
src/core/create_compilers/extract_css.ts
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
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 } from './interfaces';
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
const unaccounted_for = new Set();
|
||||||
|
|
||||||
|
const css_map = new Map();
|
||||||
|
client_result.css_files.forEach(css => {
|
||||||
|
unaccounted_for.add(css.id);
|
||||||
|
css_map.set(css.id, css.code);
|
||||||
|
});
|
||||||
|
|
||||||
|
const chunk_map = new Map();
|
||||||
|
client_result.chunks.forEach(chunk => {
|
||||||
|
chunk_map.set(chunk.file, chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
const chunks_with_css = new Set();
|
||||||
|
|
||||||
|
// figure out which chunks belong to which components...
|
||||||
|
const component_owners = new Map();
|
||||||
|
client_result.chunks.forEach(chunk => {
|
||||||
|
chunk.modules.forEach(module => {
|
||||||
|
const component = path.relative(dirs.routes, module);
|
||||||
|
component_owners.set(component, chunk);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const chunks_depended_upon_by_component = new Map();
|
||||||
|
|
||||||
|
// ...so we can figure out which chunks don't belong
|
||||||
|
components.forEach(component => {
|
||||||
|
const chunk = component_owners.get(component.file);
|
||||||
|
if (!chunk) {
|
||||||
|
// this should never happen!
|
||||||
|
throw new Error(`Could not find chunk that owns ${component.file}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks = new Set([chunk]);
|
||||||
|
chunks.forEach(chunk => {
|
||||||
|
chunk.imports.forEach((file: string) => {
|
||||||
|
const chunk = chunk_map.get(file);
|
||||||
|
if (chunk) chunks.add(chunk);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
chunks.forEach(chunk => {
|
||||||
|
chunk.modules.forEach((module: string) => {
|
||||||
|
unaccounted_for.delete(module);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
chunks_depended_upon_by_component.set(
|
||||||
|
component,
|
||||||
|
chunks
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function get_css_from_modules(modules: string[]) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const main = client_result.assets.main;
|
||||||
|
const entry = fs.readFileSync(`${dirs.dest}/client/${main}`, 'utf-8');
|
||||||
|
|
||||||
|
const replacements = new Map();
|
||||||
|
|
||||||
|
chunks_depended_upon_by_component.forEach((chunks, component) => {
|
||||||
|
const chunks_with_css = Array.from(chunks).filter(chunk => {
|
||||||
|
const css = get_css_from_modules(chunk.modules);
|
||||||
|
|
||||||
|
if (css) {
|
||||||
|
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(`${dirs.dest}/client`, source));
|
||||||
|
|
||||||
|
fs.writeFileSync(`${dirs.dest}/client/${output_file_name}`, `${code}\n/* sourceMappingURL=client/${output_file_name}.map */`);
|
||||||
|
fs.writeFileSync(`${dirs.dest}/client/${output_file_name}.map`, JSON.stringify(map, null, ' '));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const files = chunks_with_css.map(chunk => chunk.file.replace(/\.js$/, '.css'));
|
||||||
|
|
||||||
|
replacements.set(
|
||||||
|
component.file,
|
||||||
|
files
|
||||||
|
);
|
||||||
|
|
||||||
|
result.chunks[component.file] = files;
|
||||||
|
});
|
||||||
|
|
||||||
|
const replaced = entry.replace(/["']__SAPPER_CSS_PLACEHOLDER:(.+?)__["']/g, (m, route) => {
|
||||||
|
return JSON.stringify(replacements.get(route));
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(`${dirs.dest}/client/${main}`, replaced);
|
||||||
|
|
||||||
|
const leftover = get_css_from_modules(Array.from(unaccounted_for));
|
||||||
|
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(`${dirs.dest}/client`, source));
|
||||||
|
|
||||||
|
fs.writeFileSync(`${dirs.dest}/client/${output_file_name}`, `${code}\n/* sourceMappingURL=client/${output_file_name}.map */`);
|
||||||
|
fs.writeFileSync(`${dirs.dest}/client/${output_file_name}.map`, JSON.stringify(map, null, ' '));
|
||||||
|
|
||||||
|
result.main = output_file_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
37
src/core/create_compilers/index.ts
Normal file
37
src/core/create_compilers/index.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import { Dirs } from '../../interfaces';
|
||||||
|
import RollupCompiler from './RollupCompiler';
|
||||||
|
import { WebpackCompiler } from './WebpackCompiler';
|
||||||
|
|
||||||
|
export type Compiler = RollupCompiler | WebpackCompiler;
|
||||||
|
|
||||||
|
export type Compilers = {
|
||||||
|
client: Compiler;
|
||||||
|
server: Compiler;
|
||||||
|
serviceworker?: Compiler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function create_compilers(bundler: string, dirs: Dirs): Compilers {
|
||||||
|
if (bundler === 'rollup') {
|
||||||
|
const sw = `${dirs.rollup}/service-worker.config.js`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
client: new RollupCompiler(`${dirs.rollup}/client.config.js`),
|
||||||
|
server: new RollupCompiler(`${dirs.rollup}/server.config.js`),
|
||||||
|
serviceworker: fs.existsSync(sw) && new RollupCompiler(sw)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bundler === 'webpack') {
|
||||||
|
const sw = `${dirs.webpack}/service-worker.config.js`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
client: new WebpackCompiler(`${dirs.webpack}/client.config.js`),
|
||||||
|
server: new WebpackCompiler(`${dirs.webpack}/server.config.js`),
|
||||||
|
serviceworker: fs.existsSync(sw) && new WebpackCompiler(sw)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// this shouldn't be possible...
|
||||||
|
throw new Error(`Invalid bundler option '${bundler}'`);
|
||||||
|
}
|
||||||
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[]>
|
||||||
|
}
|
||||||
|
}
|
||||||
302
src/core/create_manifest_data.ts
Normal file
302
src/core/create_manifest_data.ts
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { locations } from '../config';
|
||||||
|
import { Page, PageComponent, ServerRoute, ManifestData } from '../interfaces';
|
||||||
|
import { posixify, reserved_words } from './utils';
|
||||||
|
|
||||||
|
export default function create_manifest_data(cwd = locations.routes()): ManifestData {
|
||||||
|
const components: PageComponent[] = [];
|
||||||
|
const pages: Page[] = [];
|
||||||
|
const server_routes: ServerRoute[] = [];
|
||||||
|
|
||||||
|
const default_layout: PageComponent = {
|
||||||
|
default: true,
|
||||||
|
name: '_default_layout',
|
||||||
|
file: null
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = ext === '.html';
|
||||||
|
|
||||||
|
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,
|
||||||
|
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 index = path.join(dir, item.basename, '_layout.html');
|
||||||
|
|
||||||
|
const component = fs.existsSync(index) && {
|
||||||
|
name: `${get_slug(item.file)}__layout`,
|
||||||
|
file: `${item.file}/_layout.html`
|
||||||
|
};
|
||||||
|
|
||||||
|
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 component = {
|
||||||
|
name: get_slug(item.file),
|
||||||
|
file: item.file
|
||||||
|
};
|
||||||
|
|
||||||
|
const parts = stack.concat({
|
||||||
|
component,
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
components.push(component);
|
||||||
|
if (item.basename === 'index.html') {
|
||||||
|
pages.push({
|
||||||
|
pattern: get_pattern(parent_segments, true),
|
||||||
|
parts
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pages.push({
|
||||||
|
pattern: get_pattern(segments, true),
|
||||||
|
parts
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
server_routes.push({
|
||||||
|
name: `route_${get_slug(item.file)}`,
|
||||||
|
pattern: get_pattern(segments, false),
|
||||||
|
file: item.file,
|
||||||
|
params: params
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const root_file = path.join(cwd, '_layout.html');
|
||||||
|
const root = fs.existsSync(root_file)
|
||||||
|
? {
|
||||||
|
name: 'main',
|
||||||
|
file: '_layout.html'
|
||||||
|
}
|
||||||
|
: default_layout;
|
||||||
|
|
||||||
|
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,
|
||||||
|
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 ? '\\\/?$' : '$')
|
||||||
|
);
|
||||||
|
}
|
||||||
199
src/core/create_manifests.ts
Normal file
199
src/core/create_manifests.ts
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import glob from 'tiny-glob/sync.js';
|
||||||
|
import { posixify, write_if_changed } from './utils';
|
||||||
|
import { dev, locations } from '../config';
|
||||||
|
import { Page, PageComponent, ServerRoute, ManifestData } from '../interfaces';
|
||||||
|
|
||||||
|
export function create_main_manifests({ bundler, manifest_data, dev_port }: {
|
||||||
|
bundler: string,
|
||||||
|
manifest_data: ManifestData;
|
||||||
|
dev_port?: number;
|
||||||
|
}) {
|
||||||
|
const manifest_dir = path.join(locations.app(), 'manifest');
|
||||||
|
if (!fs.existsSync(manifest_dir)) fs.mkdirSync(manifest_dir);
|
||||||
|
|
||||||
|
const path_to_routes = path.relative(manifest_dir, locations.routes());
|
||||||
|
|
||||||
|
const client_manifest = generate_client(manifest_data, path_to_routes, bundler, dev_port);
|
||||||
|
const server_manifest = generate_server(manifest_data, path_to_routes);
|
||||||
|
|
||||||
|
write_if_changed(
|
||||||
|
`${manifest_dir}/default-layout.html`,
|
||||||
|
`<svelte:component this={child.component} {...child.props}/>`
|
||||||
|
);
|
||||||
|
write_if_changed(`${manifest_dir}/client.js`, client_manifest);
|
||||||
|
write_if_changed(`${manifest_dir}/server.js`, server_manifest);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function create_serviceworker_manifest({ manifest_data, client_files }: {
|
||||||
|
manifest_data: ManifestData;
|
||||||
|
client_files: string[];
|
||||||
|
}) {
|
||||||
|
const assets = glob('**', { cwd: 'assets', filesOnly: 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${manifest_data.pages.map((r: Page) => `{ 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(
|
||||||
|
manifest_data: ManifestData,
|
||||||
|
path_to_routes: string,
|
||||||
|
bundler: string,
|
||||||
|
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()));
|
||||||
|
|
||||||
|
let code = `
|
||||||
|
// This file is generated by Sapper — do not edit it!
|
||||||
|
import root from '${get_file(path_to_routes, manifest_data.root)}';
|
||||||
|
import error from '${posixify(`${path_to_routes}/_error.html`)}';
|
||||||
|
|
||||||
|
const d = decodeURIComponent;
|
||||||
|
|
||||||
|
${manifest_data.components.map(component => {
|
||||||
|
const annotation = bundler === 'webpack'
|
||||||
|
? `/* webpackChunkName: "${component.name}" */ `
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const source = get_file(path_to_routes, component);
|
||||||
|
|
||||||
|
return `const ${component.name} = {
|
||||||
|
js: () => import(${annotation}'${source}'),
|
||||||
|
css: "__SAPPER_CSS_PLACEHOLDER:${component.file}__"
|
||||||
|
};`;
|
||||||
|
}).join('\n')}
|
||||||
|
|
||||||
|
export const manifest = {
|
||||||
|
ignore: [${server_routes_to_ignore.map(route => route.pattern).join(', ')}],
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
if (part.params.length > 0) {
|
||||||
|
const props = part.params.map((param, i) => `${param}: d(match[${i + 1}])`);
|
||||||
|
return `{ component: ${part.component.name}, params: match => ({ ${props.join(', ')} }) }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `{ component: ${part.component.name} }`;
|
||||||
|
}).join(',\n\t\t\t\t\t\t')}
|
||||||
|
]
|
||||||
|
}`).join(',\n\n\t\t\t\t')}
|
||||||
|
],
|
||||||
|
|
||||||
|
root,
|
||||||
|
|
||||||
|
error
|
||||||
|
};
|
||||||
|
|
||||||
|
// this is included for legacy reasons
|
||||||
|
export const routes = {};`.replace(/^\t\t/gm, '').trim();
|
||||||
|
|
||||||
|
if (dev()) {
|
||||||
|
const sapper_dev_client = posixify(
|
||||||
|
path.resolve(__dirname, '../sapper-dev-client.js')
|
||||||
|
);
|
||||||
|
|
||||||
|
code += `
|
||||||
|
|
||||||
|
import('${sapper_dev_client}').then(client => {
|
||||||
|
client.connect(${dev_port});
|
||||||
|
});`.replace(/^\t{3}/gm, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate_server(
|
||||||
|
manifest_data: ManifestData,
|
||||||
|
path_to_routes: string
|
||||||
|
) {
|
||||||
|
const imports = [].concat(
|
||||||
|
manifest_data.server_routes.map(route =>
|
||||||
|
`import * as ${route.name} from '${posixify(`${path_to_routes}/${route.file}`)}';`),
|
||||||
|
manifest_data.components.map(component =>
|
||||||
|
`import ${component.name} from '${get_file(path_to_routes, component)}';`),
|
||||||
|
`import root from '${get_file(path_to_routes, manifest_data.root)}';`,
|
||||||
|
`import error from '${posixify(`${path_to_routes}/_error.html`)}';`
|
||||||
|
);
|
||||||
|
|
||||||
|
let code = `
|
||||||
|
// 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 => `{
|
||||||
|
// ${route.file}
|
||||||
|
pattern: ${route.pattern},
|
||||||
|
handlers: ${route.name},
|
||||||
|
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: "${part.component.file}"`,
|
||||||
|
`component: ${part.component.name}`
|
||||||
|
];
|
||||||
|
|
||||||
|
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,
|
||||||
|
|
||||||
|
error
|
||||||
|
};
|
||||||
|
|
||||||
|
// this is included for legacy reasons
|
||||||
|
export const routes = {};`.replace(/^\t\t/gm, '').trim();
|
||||||
|
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_file(path_to_routes: string, component: PageComponent) {
|
||||||
|
if (component.default) {
|
||||||
|
return `./default-layout.html`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return posixify(`${path_to_routes}/${component.file}`);
|
||||||
|
}
|
||||||
76
src/core/utils.ts
Normal file
76
src/core/utils.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
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 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 } = 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',
|
||||||
|
]);
|
||||||
57
src/interfaces.ts
Normal file
57
src/interfaces.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
export type Route = {
|
||||||
|
id: string;
|
||||||
|
handlers: {
|
||||||
|
type: 'page' | 'route';
|
||||||
|
file: string;
|
||||||
|
}[];
|
||||||
|
pattern: RegExp;
|
||||||
|
test: (url: string) => boolean;
|
||||||
|
exec: (url: string) => Record<string, string>;
|
||||||
|
parts: string[];
|
||||||
|
params: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Template = {
|
||||||
|
render: (data: Record<string, string>) => string;
|
||||||
|
stream: (req, res, data: Record<string, string | Promise<string>>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Store = {
|
||||||
|
get: () => any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PageComponent = {
|
||||||
|
default?: boolean;
|
||||||
|
name: string;
|
||||||
|
file: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
app: string,
|
||||||
|
routes: string,
|
||||||
|
webpack: string,
|
||||||
|
rollup: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ManifestData = {
|
||||||
|
root: PageComponent;
|
||||||
|
components: PageComponent[];
|
||||||
|
pages: Page[];
|
||||||
|
server_routes: ServerRoute[];
|
||||||
|
};
|
||||||
605
src/middleware.ts
Normal file
605
src/middleware.ts
Normal file
@@ -0,0 +1,605 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { URL } from 'url';
|
||||||
|
import { ClientRequest, ServerResponse } from 'http';
|
||||||
|
import cookie from 'cookie';
|
||||||
|
import devalue from 'devalue';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import { lookup } from './middleware/mime';
|
||||||
|
import { locations, dev } from './config';
|
||||||
|
import sourceMapSupport from 'source-map-support';
|
||||||
|
|
||||||
|
sourceMapSupport.install();
|
||||||
|
|
||||||
|
type ServerRoute = {
|
||||||
|
pattern: RegExp;
|
||||||
|
handlers: Record<string, Handler>;
|
||||||
|
params: (match: RegExpMatchArray) => Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Page = {
|
||||||
|
pattern: RegExp;
|
||||||
|
parts: Array<{
|
||||||
|
name: string;
|
||||||
|
component: Component;
|
||||||
|
params?: (match: RegExpMatchArray) => Record<string, string>;
|
||||||
|
}>
|
||||||
|
};
|
||||||
|
|
||||||
|
type Manifest = {
|
||||||
|
server_routes: ServerRoute[];
|
||||||
|
pages: Page[];
|
||||||
|
root: Component;
|
||||||
|
error: Component;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Handler = (req: Req, res: ServerResponse, next: () => void) => void;
|
||||||
|
|
||||||
|
type Store = {
|
||||||
|
get: () => any
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
path: string;
|
||||||
|
query: Record<string, string>;
|
||||||
|
params: Record<string, string>;
|
||||||
|
error?: { message: string };
|
||||||
|
status?: number;
|
||||||
|
child: {
|
||||||
|
segment: string;
|
||||||
|
component: Component;
|
||||||
|
props: Props;
|
||||||
|
};
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
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>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Component {
|
||||||
|
render: (data: any, opts: { store: Store }) => {
|
||||||
|
head: string;
|
||||||
|
css: { code: string, map: any };
|
||||||
|
html: string
|
||||||
|
},
|
||||||
|
preload: (data: any) => any | Promise<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
const IGNORE = '__SAPPER__IGNORE__';
|
||||||
|
function toIgnore(uri: string, val: any) {
|
||||||
|
if (Array.isArray(val)) return val.some(x => toIgnore(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 default function middleware(opts: {
|
||||||
|
manifest: Manifest,
|
||||||
|
store: (req: Req, res: ServerResponse) => Store,
|
||||||
|
ignore?: any,
|
||||||
|
routes?: any // legacy
|
||||||
|
}) {
|
||||||
|
if (opts.routes) {
|
||||||
|
throw new Error(`As of Sapper 0.15, opts.routes should be opts.manifest`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = locations.dest();
|
||||||
|
|
||||||
|
const { manifest, store, ignore } = opts;
|
||||||
|
|
||||||
|
let emitted_basepath = false;
|
||||||
|
|
||||||
|
const middleware = compose_handlers([
|
||||||
|
ignore && ((req: Req, res: ServerResponse, next: () => void) => {
|
||||||
|
req[IGNORE] = toIgnore(req.path, ignore);
|
||||||
|
next();
|
||||||
|
}),
|
||||||
|
|
||||||
|
(req: Req, res: ServerResponse, 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(output, 'index.html')) && serve({
|
||||||
|
pathname: '/index.html',
|
||||||
|
cache_control: dev() ? 'no-cache' : 'max-age=600'
|
||||||
|
}),
|
||||||
|
|
||||||
|
fs.existsSync(path.join(output, 'service-worker.js')) && serve({
|
||||||
|
pathname: '/service-worker.js',
|
||||||
|
cache_control: 'no-cache, no-store, must-revalidate'
|
||||||
|
}),
|
||||||
|
|
||||||
|
fs.existsSync(path.join(output, '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, store)
|
||||||
|
].filter(Boolean));
|
||||||
|
|
||||||
|
return middleware;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 (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 get_server_route_handler(routes: ServerRoute[]) {
|
||||||
|
function handle_route(route: ServerRoute, req: Req, res: ServerResponse, 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 {
|
||||||
|
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: ServerResponse, 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();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_page_handler(
|
||||||
|
manifest: Manifest,
|
||||||
|
store_getter: (req: Req, res: ServerResponse) => Store
|
||||||
|
) {
|
||||||
|
const output = locations.dest();
|
||||||
|
|
||||||
|
const get_build_info = dev()
|
||||||
|
? () => JSON.parse(fs.readFileSync(path.join(output, 'build.json'), 'utf-8'))
|
||||||
|
: (assets => () => assets)(JSON.parse(fs.readFileSync(path.join(output, 'build.json'), 'utf-8')));
|
||||||
|
|
||||||
|
const template = dev()
|
||||||
|
? () => fs.readFileSync(`${locations.app()}/template.html`, 'utf-8')
|
||||||
|
: (str => () => str)(fs.readFileSync(`${locations.dest()}/template.html`, 'utf-8'));
|
||||||
|
|
||||||
|
const { server_routes, pages } = manifest;
|
||||||
|
const error_route = manifest.error;
|
||||||
|
|
||||||
|
function handle_error(req: Req, res: ServerResponse, 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'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_page(page: Page, req: Req, res: ServerResponse, status = 200, error: Error | string = null) {
|
||||||
|
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) {
|
||||||
|
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]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = preloaded_chunks
|
||||||
|
.filter(file => file && !file.match(/\.map$/))
|
||||||
|
.map(file => `<${req.baseUrl}/client/${file}>;rel="preload";as="script"`)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
res.setHeader('Link', link);
|
||||||
|
|
||||||
|
const store = store_getter ? store_getter(req, res) : null;
|
||||||
|
|
||||||
|
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, `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) {
|
||||||
|
if (!opts.headers) opts.headers = {};
|
||||||
|
|
||||||
|
const str = []
|
||||||
|
.concat(
|
||||||
|
cookie.parse(req.headers.cookie || ''),
|
||||||
|
cookie.parse(opts.headers.cookie || ''),
|
||||||
|
cookie.parse(res.getHeader('Set-Cookie') || '')
|
||||||
|
)
|
||||||
|
.map(cookie => {
|
||||||
|
return Object.keys(cookie)
|
||||||
|
.map(name => `${name}=${encodeURIComponent(cookie[name])}`)
|
||||||
|
.join('; ');
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
opts.headers.cookie = str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(parsed.href, opts);
|
||||||
|
},
|
||||||
|
store
|
||||||
|
};
|
||||||
|
|
||||||
|
const root_preloaded = manifest.root.preload
|
||||||
|
? manifest.root.preload.call(preload_context, {
|
||||||
|
path: req.path,
|
||||||
|
query: req.query,
|
||||||
|
params: {}
|
||||||
|
})
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const match = error ? null : page.pattern.exec(req.path);
|
||||||
|
|
||||||
|
Promise.all([root_preloaded].concat(page.parts.map(part => {
|
||||||
|
if (!part) return null;
|
||||||
|
|
||||||
|
return part.component.preload
|
||||||
|
? part.component.preload.call(preload_context, {
|
||||||
|
path: req.path,
|
||||||
|
query: req.query,
|
||||||
|
params: part.params ? part.params(match) : {}
|
||||||
|
})
|
||||||
|
: {};
|
||||||
|
}))).catch(err => {
|
||||||
|
preload_error = { statusCode: 500, message: err };
|
||||||
|
return []; // appease TypeScript
|
||||||
|
}).then(preloaded => {
|
||||||
|
if (redirect) {
|
||||||
|
const location = `${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 serialized = {
|
||||||
|
preloaded: `[${preloaded.map(data => try_serialize(data)).join(',')}]`,
|
||||||
|
store: store && try_serialize(store.get())
|
||||||
|
};
|
||||||
|
|
||||||
|
const segments = req.path.split('/').filter(Boolean);
|
||||||
|
|
||||||
|
const props: Props = {
|
||||||
|
path: req.path,
|
||||||
|
query: req.query,
|
||||||
|
params: {},
|
||||||
|
child: null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
props.error = error instanceof Error ? error : { message: error };
|
||||||
|
props.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = Object.assign({}, props, preloaded[0], {
|
||||||
|
params: {},
|
||||||
|
child: {
|
||||||
|
segment: segments[0]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let level = data.child;
|
||||||
|
for (let i = 0; i < page.parts.length; i += 1) {
|
||||||
|
const part = page.parts[i];
|
||||||
|
if (!part) continue;
|
||||||
|
|
||||||
|
const get_params = part.params || (() => ({}));
|
||||||
|
|
||||||
|
Object.assign(level, {
|
||||||
|
component: part.component,
|
||||||
|
props: Object.assign({}, props, {
|
||||||
|
params: get_params(match)
|
||||||
|
}, preloaded[i + 1])
|
||||||
|
});
|
||||||
|
|
||||||
|
level.props.child = <Props["child"]>{
|
||||||
|
segment: segments[i + 1]
|
||||||
|
};
|
||||||
|
level = level.props.child;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { html, head, css } = manifest.root.render(data, {
|
||||||
|
store
|
||||||
|
});
|
||||||
|
|
||||||
|
let script = `__SAPPER__={${[
|
||||||
|
error && `error:1`,
|
||||||
|
`baseUrl:"${req.baseUrl}"`,
|
||||||
|
serialized.preloaded && `preloaded:${serialized.preloaded}`,
|
||||||
|
serialized.store && `store:${serialized.store}`
|
||||||
|
].filter(Boolean).join(',')}};`;
|
||||||
|
|
||||||
|
const has_service_worker = fs.existsSync(path.join(locations.dest(), 'service-worker.js'));
|
||||||
|
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}"};try{new Function("import('"+main+"')")();}catch(e){var s=document.createElement("script");s.src="${req.baseUrl}/client/shimport@${build_info.shimport}.js";s.setAttribute("data-main",main);document.head.appendChild(s);}}());`;
|
||||||
|
} else {
|
||||||
|
script += `try{new Function("import('${main}')")();}catch(e){var s=document.createElement("script");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
|
||||||
|
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 => {
|
||||||
|
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: ServerResponse, next: () => void) {
|
||||||
|
if (req[IGNORE]) return next();
|
||||||
|
|
||||||
|
if (!server_routes.some(route => route.pattern.test(req.path))) {
|
||||||
|
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 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 try_serialize(data: any) {
|
||||||
|
try {
|
||||||
|
return devalue(data);
|
||||||
|
} catch (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]};`);
|
||||||
|
}
|
||||||
767
src/middleware/mime-types.md
Normal file
767
src/middleware/mime-types.md
Normal file
@@ -0,0 +1,767 @@
|
|||||||
|
application/andrew-inset ez
|
||||||
|
application/applixware aw
|
||||||
|
application/atom+xml atom
|
||||||
|
application/atomcat+xml atomcat
|
||||||
|
application/atomsvc+xml atomsvc
|
||||||
|
application/ccxml+xml ccxml
|
||||||
|
application/cdmi-capability cdmia
|
||||||
|
application/cdmi-container cdmic
|
||||||
|
application/cdmi-domain cdmid
|
||||||
|
application/cdmi-object cdmio
|
||||||
|
application/cdmi-queue cdmiq
|
||||||
|
application/cu-seeme cu
|
||||||
|
application/davmount+xml davmount
|
||||||
|
application/docbook+xml dbk
|
||||||
|
application/dssc+der dssc
|
||||||
|
application/dssc+xml xdssc
|
||||||
|
application/ecmascript ecma
|
||||||
|
application/emma+xml emma
|
||||||
|
application/epub+zip epub
|
||||||
|
application/exi exi
|
||||||
|
application/font-tdpfr pfr
|
||||||
|
application/gml+xml gml
|
||||||
|
application/gpx+xml gpx
|
||||||
|
application/gxf gxf
|
||||||
|
application/hyperstudio stk
|
||||||
|
application/inkml+xml ink inkml
|
||||||
|
application/ipfix ipfix
|
||||||
|
application/java-archive jar
|
||||||
|
application/java-serialized-object ser
|
||||||
|
application/java-vm class
|
||||||
|
application/javascript js
|
||||||
|
application/json json map
|
||||||
|
application/jsonml+json jsonml
|
||||||
|
application/lost+xml lostxml
|
||||||
|
application/mac-binhex40 hqx
|
||||||
|
application/mac-compactpro cpt
|
||||||
|
application/mads+xml mads
|
||||||
|
application/marc mrc
|
||||||
|
application/marcxml+xml mrcx
|
||||||
|
application/mathematica ma nb mb
|
||||||
|
application/mathml+xml mathml
|
||||||
|
application/mbox mbox
|
||||||
|
application/mediaservercontrol+xml mscml
|
||||||
|
application/metalink+xml metalink
|
||||||
|
application/metalink4+xml meta4
|
||||||
|
application/mets+xml mets
|
||||||
|
application/mods+xml mods
|
||||||
|
application/mp21 m21 mp21
|
||||||
|
application/mp4 mp4s
|
||||||
|
application/msword doc dot
|
||||||
|
application/mxf mxf
|
||||||
|
application/octet-stream bin dms lrf mar so dist distz pkg bpk dump elc deploy
|
||||||
|
application/oda oda
|
||||||
|
application/oebps-package+xml opf
|
||||||
|
application/ogg ogx
|
||||||
|
application/omdoc+xml omdoc
|
||||||
|
application/onenote onetoc onetoc2 onetmp onepkg
|
||||||
|
application/oxps oxps
|
||||||
|
application/patch-ops-error+xml xer
|
||||||
|
application/pdf pdf
|
||||||
|
application/pgp-encrypted pgp
|
||||||
|
application/pgp-signature asc sig
|
||||||
|
application/pics-rules prf
|
||||||
|
application/pkcs10 p10
|
||||||
|
application/pkcs7-mime p7m p7c
|
||||||
|
application/pkcs7-signature p7s
|
||||||
|
application/pkcs8 p8
|
||||||
|
application/pkix-attr-cert ac
|
||||||
|
application/pkix-cert cer
|
||||||
|
application/pkix-crl crl
|
||||||
|
application/pkix-pkipath pkipath
|
||||||
|
application/pkixcmp pki
|
||||||
|
application/pls+xml pls
|
||||||
|
application/postscript ai eps ps
|
||||||
|
application/prs.cww cww
|
||||||
|
application/pskc+xml pskcxml
|
||||||
|
application/rdf+xml rdf
|
||||||
|
application/reginfo+xml rif
|
||||||
|
application/relax-ng-compact-syntax rnc
|
||||||
|
application/resource-lists+xml rl
|
||||||
|
application/resource-lists-diff+xml rld
|
||||||
|
application/rls-services+xml rs
|
||||||
|
application/rpki-ghostbusters gbr
|
||||||
|
application/rpki-manifest mft
|
||||||
|
application/rpki-roa roa
|
||||||
|
application/rsd+xml rsd
|
||||||
|
application/rss+xml rss
|
||||||
|
application/rtf rtf
|
||||||
|
application/sbml+xml sbml
|
||||||
|
application/scvp-cv-request scq
|
||||||
|
application/scvp-cv-response scs
|
||||||
|
application/scvp-vp-request spq
|
||||||
|
application/scvp-vp-response spp
|
||||||
|
application/sdp sdp
|
||||||
|
application/set-payment-initiation setpay
|
||||||
|
application/set-registration-initiation setreg
|
||||||
|
application/shf+xml shf
|
||||||
|
application/smil+xml smi smil
|
||||||
|
application/sparql-query rq
|
||||||
|
application/sparql-results+xml srx
|
||||||
|
application/srgs gram
|
||||||
|
application/srgs+xml grxml
|
||||||
|
application/sru+xml sru
|
||||||
|
application/ssdl+xml ssdl
|
||||||
|
application/ssml+xml ssml
|
||||||
|
application/tei+xml tei teicorpus
|
||||||
|
application/thraud+xml tfi
|
||||||
|
application/timestamped-data tsd
|
||||||
|
application/vnd.3gpp.pic-bw-large plb
|
||||||
|
application/vnd.3gpp.pic-bw-small psb
|
||||||
|
application/vnd.3gpp.pic-bw-var pvb
|
||||||
|
application/vnd.3gpp2.tcap tcap
|
||||||
|
application/vnd.3m.post-it-notes pwn
|
||||||
|
application/vnd.accpac.simply.aso aso
|
||||||
|
application/vnd.accpac.simply.imp imp
|
||||||
|
application/vnd.acucobol acu
|
||||||
|
application/vnd.acucorp atc acutc
|
||||||
|
application/vnd.adobe.air-application-installer-package+zip air
|
||||||
|
application/vnd.adobe.formscentral.fcdt fcdt
|
||||||
|
application/vnd.adobe.fxp fxp fxpl
|
||||||
|
application/vnd.adobe.xdp+xml xdp
|
||||||
|
application/vnd.adobe.xfdf xfdf
|
||||||
|
application/vnd.ahead.space ahead
|
||||||
|
application/vnd.airzip.filesecure.azf azf
|
||||||
|
application/vnd.airzip.filesecure.azs azs
|
||||||
|
application/vnd.amazon.ebook azw
|
||||||
|
application/vnd.americandynamics.acc acc
|
||||||
|
application/vnd.amiga.ami ami
|
||||||
|
application/vnd.android.package-archive apk
|
||||||
|
application/vnd.anser-web-certificate-issue-initiation cii
|
||||||
|
application/vnd.anser-web-funds-transfer-initiation fti
|
||||||
|
application/vnd.antix.game-component atx
|
||||||
|
application/vnd.apple.installer+xml mpkg
|
||||||
|
application/vnd.apple.mpegurl m3u8
|
||||||
|
application/vnd.aristanetworks.swi swi
|
||||||
|
application/vnd.astraea-software.iota iota
|
||||||
|
application/vnd.audiograph aep
|
||||||
|
application/vnd.blueice.multipass mpm
|
||||||
|
application/vnd.bmi bmi
|
||||||
|
application/vnd.businessobjects rep
|
||||||
|
application/vnd.chemdraw+xml cdxml
|
||||||
|
application/vnd.chipnuts.karaoke-mmd mmd
|
||||||
|
application/vnd.cinderella cdy
|
||||||
|
application/vnd.claymore cla
|
||||||
|
application/vnd.cloanto.rp9 rp9
|
||||||
|
application/vnd.clonk.c4group c4g c4d c4f c4p c4u
|
||||||
|
application/vnd.cluetrust.cartomobile-config c11amc
|
||||||
|
application/vnd.cluetrust.cartomobile-config-pkg c11amz
|
||||||
|
application/vnd.commonspace csp
|
||||||
|
application/vnd.contact.cmsg cdbcmsg
|
||||||
|
application/vnd.cosmocaller cmc
|
||||||
|
application/vnd.crick.clicker clkx
|
||||||
|
application/vnd.crick.clicker.keyboard clkk
|
||||||
|
application/vnd.crick.clicker.palette clkp
|
||||||
|
application/vnd.crick.clicker.template clkt
|
||||||
|
application/vnd.crick.clicker.wordbank clkw
|
||||||
|
application/vnd.criticaltools.wbs+xml wbs
|
||||||
|
application/vnd.ctc-posml pml
|
||||||
|
application/vnd.cups-ppd ppd
|
||||||
|
application/vnd.curl.car car
|
||||||
|
application/vnd.curl.pcurl pcurl
|
||||||
|
application/vnd.dart dart
|
||||||
|
application/vnd.data-vision.rdz rdz
|
||||||
|
application/vnd.dece.data uvf uvvf uvd uvvd
|
||||||
|
application/vnd.dece.ttml+xml uvt uvvt
|
||||||
|
application/vnd.dece.unspecified uvx uvvx
|
||||||
|
application/vnd.dece.zip uvz uvvz
|
||||||
|
application/vnd.denovo.fcselayout-link fe_launch
|
||||||
|
application/vnd.dna dna
|
||||||
|
application/vnd.dolby.mlp mlp
|
||||||
|
application/vnd.dpgraph dpg
|
||||||
|
application/vnd.dreamfactory dfac
|
||||||
|
application/vnd.ds-keypoint kpxx
|
||||||
|
application/vnd.dvb.ait ait
|
||||||
|
application/vnd.dvb.service svc
|
||||||
|
application/vnd.dynageo geo
|
||||||
|
application/vnd.ecowin.chart mag
|
||||||
|
application/vnd.enliven nml
|
||||||
|
application/vnd.epson.esf esf
|
||||||
|
application/vnd.epson.msf msf
|
||||||
|
application/vnd.epson.quickanime qam
|
||||||
|
application/vnd.epson.salt slt
|
||||||
|
application/vnd.epson.ssf ssf
|
||||||
|
application/vnd.eszigno3+xml es3 et3
|
||||||
|
application/vnd.ezpix-album ez2
|
||||||
|
application/vnd.ezpix-package ez3
|
||||||
|
application/vnd.fdf fdf
|
||||||
|
application/vnd.fdsn.mseed mseed
|
||||||
|
application/vnd.fdsn.seed seed dataless
|
||||||
|
application/vnd.flographit gph
|
||||||
|
application/vnd.fluxtime.clip ftc
|
||||||
|
application/vnd.framemaker fm frame maker book
|
||||||
|
application/vnd.frogans.fnc fnc
|
||||||
|
application/vnd.frogans.ltf ltf
|
||||||
|
application/vnd.fsc.weblaunch fsc
|
||||||
|
application/vnd.fujitsu.oasys oas
|
||||||
|
application/vnd.fujitsu.oasys2 oa2
|
||||||
|
application/vnd.fujitsu.oasys3 oa3
|
||||||
|
application/vnd.fujitsu.oasysgp fg5
|
||||||
|
application/vnd.fujitsu.oasysprs bh2
|
||||||
|
application/vnd.fujixerox.ddd ddd
|
||||||
|
application/vnd.fujixerox.docuworks xdw
|
||||||
|
application/vnd.fujixerox.docuworks.binder xbd
|
||||||
|
application/vnd.fuzzysheet fzs
|
||||||
|
application/vnd.genomatix.tuxedo txd
|
||||||
|
application/vnd.geogebra.file ggb
|
||||||
|
application/vnd.geogebra.tool ggt
|
||||||
|
application/vnd.geometry-explorer gex gre
|
||||||
|
application/vnd.geonext gxt
|
||||||
|
application/vnd.geoplan g2w
|
||||||
|
application/vnd.geospace g3w
|
||||||
|
application/vnd.gmx gmx
|
||||||
|
application/vnd.google-earth.kml+xml kml
|
||||||
|
application/vnd.google-earth.kmz kmz
|
||||||
|
application/vnd.grafeq gqf gqs
|
||||||
|
application/vnd.groove-account gac
|
||||||
|
application/vnd.groove-help ghf
|
||||||
|
application/vnd.groove-identity-message gim
|
||||||
|
application/vnd.groove-injector grv
|
||||||
|
application/vnd.groove-tool-message gtm
|
||||||
|
application/vnd.groove-tool-template tpl
|
||||||
|
application/vnd.groove-vcard vcg
|
||||||
|
application/vnd.hal+xml hal
|
||||||
|
application/vnd.handheld-entertainment+xml zmm
|
||||||
|
application/vnd.hbci hbci
|
||||||
|
application/vnd.hhe.lesson-player les
|
||||||
|
application/vnd.hp-hpgl hpgl
|
||||||
|
application/vnd.hp-hpid hpid
|
||||||
|
application/vnd.hp-hps hps
|
||||||
|
application/vnd.hp-jlyt jlt
|
||||||
|
application/vnd.hp-pcl pcl
|
||||||
|
application/vnd.hp-pclxl pclxl
|
||||||
|
application/vnd.hydrostatix.sof-data sfd-hdstx
|
||||||
|
application/vnd.ibm.minipay mpy
|
||||||
|
application/vnd.ibm.modcap afp listafp list3820
|
||||||
|
application/vnd.ibm.rights-management irm
|
||||||
|
application/vnd.ibm.secure-container sc
|
||||||
|
application/vnd.iccprofile icc icm
|
||||||
|
application/vnd.igloader igl
|
||||||
|
application/vnd.immervision-ivp ivp
|
||||||
|
application/vnd.immervision-ivu ivu
|
||||||
|
application/vnd.insors.igm igm
|
||||||
|
application/vnd.intercon.formnet xpw xpx
|
||||||
|
application/vnd.intergeo i2g
|
||||||
|
application/vnd.intu.qbo qbo
|
||||||
|
application/vnd.intu.qfx qfx
|
||||||
|
application/vnd.ipunplugged.rcprofile rcprofile
|
||||||
|
application/vnd.irepository.package+xml irp
|
||||||
|
application/vnd.is-xpr xpr
|
||||||
|
application/vnd.isac.fcs fcs
|
||||||
|
application/vnd.jam jam
|
||||||
|
application/vnd.jcp.javame.midlet-rms rms
|
||||||
|
application/vnd.jisp jisp
|
||||||
|
application/vnd.joost.joda-archive joda
|
||||||
|
application/vnd.kahootz ktz ktr
|
||||||
|
application/vnd.kde.karbon karbon
|
||||||
|
application/vnd.kde.kchart chrt
|
||||||
|
application/vnd.kde.kformula kfo
|
||||||
|
application/vnd.kde.kivio flw
|
||||||
|
application/vnd.kde.kontour kon
|
||||||
|
application/vnd.kde.kpresenter kpr kpt
|
||||||
|
application/vnd.kde.kspread ksp
|
||||||
|
application/vnd.kde.kword kwd kwt
|
||||||
|
application/vnd.kenameaapp htke
|
||||||
|
application/vnd.kidspiration kia
|
||||||
|
application/vnd.kinar kne knp
|
||||||
|
application/vnd.koan skp skd skt skm
|
||||||
|
application/vnd.kodak-descriptor sse
|
||||||
|
application/vnd.las.las+xml lasxml
|
||||||
|
application/vnd.llamagraphics.life-balance.desktop lbd
|
||||||
|
application/vnd.llamagraphics.life-balance.exchange+xml lbe
|
||||||
|
application/vnd.lotus-1-2-3 123
|
||||||
|
application/vnd.lotus-approach apr
|
||||||
|
application/vnd.lotus-freelance pre
|
||||||
|
application/vnd.lotus-notes nsf
|
||||||
|
application/vnd.lotus-organizer org
|
||||||
|
application/vnd.lotus-screencam scm
|
||||||
|
application/vnd.lotus-wordpro lwp
|
||||||
|
application/vnd.macports.portpkg portpkg
|
||||||
|
application/vnd.mcd mcd
|
||||||
|
application/vnd.medcalcdata mc1
|
||||||
|
application/vnd.mediastation.cdkey cdkey
|
||||||
|
application/vnd.mfer mwf
|
||||||
|
application/vnd.mfmp mfm
|
||||||
|
application/vnd.micrografx.flo flo
|
||||||
|
application/vnd.micrografx.igx igx
|
||||||
|
application/vnd.mif mif
|
||||||
|
application/vnd.mobius.daf daf
|
||||||
|
application/vnd.mobius.dis dis
|
||||||
|
application/vnd.mobius.mbk mbk
|
||||||
|
application/vnd.mobius.mqy mqy
|
||||||
|
application/vnd.mobius.msl msl
|
||||||
|
application/vnd.mobius.plc plc
|
||||||
|
application/vnd.mobius.txf txf
|
||||||
|
application/vnd.mophun.application mpn
|
||||||
|
application/vnd.mophun.certificate mpc
|
||||||
|
application/vnd.mozilla.xul+xml xul
|
||||||
|
application/vnd.ms-artgalry cil
|
||||||
|
application/vnd.ms-cab-compressed cab
|
||||||
|
application/vnd.ms-excel xls xlm xla xlc xlt xlw
|
||||||
|
application/vnd.ms-excel.addin.macroenabled.12 xlam
|
||||||
|
application/vnd.ms-excel.sheet.binary.macroenabled.12 xlsb
|
||||||
|
application/vnd.ms-excel.sheet.macroenabled.12 xlsm
|
||||||
|
application/vnd.ms-excel.template.macroenabled.12 xltm
|
||||||
|
application/vnd.ms-fontobject eot
|
||||||
|
application/vnd.ms-htmlhelp chm
|
||||||
|
application/vnd.ms-ims ims
|
||||||
|
application/vnd.ms-lrm lrm
|
||||||
|
application/vnd.ms-officetheme thmx
|
||||||
|
application/vnd.ms-pki.seccat cat
|
||||||
|
application/vnd.ms-pki.stl stl
|
||||||
|
application/vnd.ms-powerpoint ppt pps pot
|
||||||
|
application/vnd.ms-powerpoint.addin.macroenabled.12 ppam
|
||||||
|
application/vnd.ms-powerpoint.presentation.macroenabled.12 pptm
|
||||||
|
application/vnd.ms-powerpoint.slide.macroenabled.12 sldm
|
||||||
|
application/vnd.ms-powerpoint.slideshow.macroenabled.12 ppsm
|
||||||
|
application/vnd.ms-powerpoint.template.macroenabled.12 potm
|
||||||
|
application/vnd.ms-project mpp mpt
|
||||||
|
application/vnd.ms-word.document.macroenabled.12 docm
|
||||||
|
application/vnd.ms-word.template.macroenabled.12 dotm
|
||||||
|
application/vnd.ms-works wps wks wcm wdb
|
||||||
|
application/vnd.ms-wpl wpl
|
||||||
|
application/vnd.ms-xpsdocument xps
|
||||||
|
application/vnd.mseq mseq
|
||||||
|
application/vnd.musician mus
|
||||||
|
application/vnd.muvee.style msty
|
||||||
|
application/vnd.mynfc taglet
|
||||||
|
application/vnd.neurolanguage.nlu nlu
|
||||||
|
application/vnd.nitf ntf nitf
|
||||||
|
application/vnd.noblenet-directory nnd
|
||||||
|
application/vnd.noblenet-sealer nns
|
||||||
|
application/vnd.noblenet-web nnw
|
||||||
|
application/vnd.nokia.n-gage.data ngdat
|
||||||
|
application/vnd.nokia.n-gage.symbian.install n-gage
|
||||||
|
application/vnd.nokia.radio-preset rpst
|
||||||
|
application/vnd.nokia.radio-presets rpss
|
||||||
|
application/vnd.novadigm.edm edm
|
||||||
|
application/vnd.novadigm.edx edx
|
||||||
|
application/vnd.novadigm.ext ext
|
||||||
|
application/vnd.oasis.opendocument.chart odc
|
||||||
|
application/vnd.oasis.opendocument.chart-template otc
|
||||||
|
application/vnd.oasis.opendocument.database odb
|
||||||
|
application/vnd.oasis.opendocument.formula odf
|
||||||
|
application/vnd.oasis.opendocument.formula-template odft
|
||||||
|
application/vnd.oasis.opendocument.graphics odg
|
||||||
|
application/vnd.oasis.opendocument.graphics-template otg
|
||||||
|
application/vnd.oasis.opendocument.image odi
|
||||||
|
application/vnd.oasis.opendocument.image-template oti
|
||||||
|
application/vnd.oasis.opendocument.presentation odp
|
||||||
|
application/vnd.oasis.opendocument.presentation-template otp
|
||||||
|
application/vnd.oasis.opendocument.spreadsheet ods
|
||||||
|
application/vnd.oasis.opendocument.spreadsheet-template ots
|
||||||
|
application/vnd.oasis.opendocument.text odt
|
||||||
|
application/vnd.oasis.opendocument.text-master odm
|
||||||
|
application/vnd.oasis.opendocument.text-template ott
|
||||||
|
application/vnd.oasis.opendocument.text-web oth
|
||||||
|
application/vnd.olpc-sugar xo
|
||||||
|
application/vnd.oma.dd2+xml dd2
|
||||||
|
application/vnd.openofficeorg.extension oxt
|
||||||
|
application/vnd.openxmlformats-officedocument.presentationml.presentation pptx
|
||||||
|
application/vnd.openxmlformats-officedocument.presentationml.slide sldx
|
||||||
|
application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx
|
||||||
|
application/vnd.openxmlformats-officedocument.presentationml.template potx
|
||||||
|
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx
|
||||||
|
application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx
|
||||||
|
application/vnd.openxmlformats-officedocument.wordprocessingml.document docx
|
||||||
|
application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx
|
||||||
|
application/vnd.osgeo.mapguide.package mgp
|
||||||
|
application/vnd.osgi.dp dp
|
||||||
|
application/vnd.osgi.subsystem esa
|
||||||
|
application/vnd.palm pdb pqa oprc
|
||||||
|
application/vnd.pawaafile paw
|
||||||
|
application/vnd.pg.format str
|
||||||
|
application/vnd.pg.osasli ei6
|
||||||
|
application/vnd.picsel efif
|
||||||
|
application/vnd.pmi.widget wg
|
||||||
|
application/vnd.pocketlearn plf
|
||||||
|
application/vnd.powerbuilder6 pbd
|
||||||
|
application/vnd.previewsystems.box box
|
||||||
|
application/vnd.proteus.magazine mgz
|
||||||
|
application/vnd.publishare-delta-tree qps
|
||||||
|
application/vnd.pvi.ptid1 ptid
|
||||||
|
application/vnd.quark.quarkxpress qxd qxt qwd qwt qxl qxb
|
||||||
|
application/vnd.realvnc.bed bed
|
||||||
|
application/vnd.recordare.musicxml mxl
|
||||||
|
application/vnd.recordare.musicxml+xml musicxml
|
||||||
|
application/vnd.rig.cryptonote cryptonote
|
||||||
|
application/vnd.rim.cod cod
|
||||||
|
application/vnd.rn-realmedia rm
|
||||||
|
application/vnd.rn-realmedia-vbr rmvb
|
||||||
|
application/vnd.route66.link66+xml link66
|
||||||
|
application/vnd.sailingtracker.track st
|
||||||
|
application/vnd.seemail see
|
||||||
|
application/vnd.sema sema
|
||||||
|
application/vnd.semd semd
|
||||||
|
application/vnd.semf semf
|
||||||
|
application/vnd.shana.informed.formdata ifm
|
||||||
|
application/vnd.shana.informed.formtemplate itp
|
||||||
|
application/vnd.shana.informed.interchange iif
|
||||||
|
application/vnd.shana.informed.package ipk
|
||||||
|
application/vnd.simtech-mindmapper twd twds
|
||||||
|
application/vnd.smaf mmf
|
||||||
|
application/vnd.smart.teacher teacher
|
||||||
|
application/vnd.solent.sdkm+xml sdkm sdkd
|
||||||
|
application/vnd.spotfire.dxp dxp
|
||||||
|
application/vnd.spotfire.sfs sfs
|
||||||
|
application/vnd.stardivision.calc sdc
|
||||||
|
application/vnd.stardivision.draw sda
|
||||||
|
application/vnd.stardivision.impress sdd
|
||||||
|
application/vnd.stardivision.math smf
|
||||||
|
application/vnd.stardivision.writer sdw vor
|
||||||
|
application/vnd.stardivision.writer-global sgl
|
||||||
|
application/vnd.stepmania.package smzip
|
||||||
|
application/vnd.stepmania.stepchart sm
|
||||||
|
application/vnd.sun.xml.calc sxc
|
||||||
|
application/vnd.sun.xml.calc.template stc
|
||||||
|
application/vnd.sun.xml.draw sxd
|
||||||
|
application/vnd.sun.xml.draw.template std
|
||||||
|
application/vnd.sun.xml.impress sxi
|
||||||
|
application/vnd.sun.xml.impress.template sti
|
||||||
|
application/vnd.sun.xml.math sxm
|
||||||
|
application/vnd.sun.xml.writer sxw
|
||||||
|
application/vnd.sun.xml.writer.global sxg
|
||||||
|
application/vnd.sun.xml.writer.template stw
|
||||||
|
application/vnd.sus-calendar sus susp
|
||||||
|
application/vnd.svd svd
|
||||||
|
application/vnd.symbian.install sis sisx
|
||||||
|
application/vnd.syncml+xml xsm
|
||||||
|
application/vnd.syncml.dm+wbxml bdm
|
||||||
|
application/vnd.syncml.dm+xml xdm
|
||||||
|
application/vnd.tao.intent-module-archive tao
|
||||||
|
application/vnd.tcpdump.pcap pcap cap dmp
|
||||||
|
application/vnd.tmobile-livetv tmo
|
||||||
|
application/vnd.trid.tpt tpt
|
||||||
|
application/vnd.triscape.mxs mxs
|
||||||
|
application/vnd.trueapp tra
|
||||||
|
application/vnd.ufdl ufd ufdl
|
||||||
|
application/vnd.uiq.theme utz
|
||||||
|
application/vnd.umajin umj
|
||||||
|
application/vnd.unity unityweb
|
||||||
|
application/vnd.uoml+xml uoml
|
||||||
|
application/vnd.vcx vcx
|
||||||
|
application/vnd.visio vsd vst vss vsw
|
||||||
|
application/vnd.visionary vis
|
||||||
|
application/vnd.vsf vsf
|
||||||
|
application/vnd.wap.wbxml wbxml
|
||||||
|
application/vnd.wap.wmlc wmlc
|
||||||
|
application/vnd.wap.wmlscriptc wmlsc
|
||||||
|
application/vnd.webturbo wtb
|
||||||
|
application/vnd.wolfram.player nbp
|
||||||
|
application/vnd.wordperfect wpd
|
||||||
|
application/vnd.wqd wqd
|
||||||
|
application/vnd.wt.stf stf
|
||||||
|
application/vnd.xara xar
|
||||||
|
application/vnd.xfdl xfdl
|
||||||
|
application/vnd.yamaha.hv-dic hvd
|
||||||
|
application/vnd.yamaha.hv-script hvs
|
||||||
|
application/vnd.yamaha.hv-voice hvp
|
||||||
|
application/vnd.yamaha.openscoreformat osf
|
||||||
|
application/vnd.yamaha.openscoreformat.osfpvg+xml osfpvg
|
||||||
|
application/vnd.yamaha.smaf-audio saf
|
||||||
|
application/vnd.yamaha.smaf-phrase spf
|
||||||
|
application/vnd.yellowriver-custom-menu cmp
|
||||||
|
application/vnd.zul zir zirz
|
||||||
|
application/vnd.zzazz.deck+xml zaz
|
||||||
|
application/voicexml+xml vxml
|
||||||
|
application/widget wgt
|
||||||
|
application/winhlp hlp
|
||||||
|
application/wsdl+xml wsdl
|
||||||
|
application/wspolicy+xml wspolicy
|
||||||
|
application/x-7z-compressed 7z
|
||||||
|
application/x-abiword abw
|
||||||
|
application/x-ace-compressed ace
|
||||||
|
application/x-apple-diskimage dmg
|
||||||
|
application/x-authorware-bin aab x32 u32 vox
|
||||||
|
application/x-authorware-map aam
|
||||||
|
application/x-authorware-seg aas
|
||||||
|
application/x-bcpio bcpio
|
||||||
|
application/x-bittorrent torrent
|
||||||
|
application/x-blorb blb blorb
|
||||||
|
application/x-bzip bz
|
||||||
|
application/x-bzip2 bz2 boz
|
||||||
|
application/x-cbr cbr cba cbt cbz cb7
|
||||||
|
application/x-cdlink vcd
|
||||||
|
application/x-cfs-compressed cfs
|
||||||
|
application/x-chat chat
|
||||||
|
application/x-chess-pgn pgn
|
||||||
|
application/x-conference nsc
|
||||||
|
application/x-cpio cpio
|
||||||
|
application/x-csh csh
|
||||||
|
application/x-debian-package deb udeb
|
||||||
|
application/x-dgc-compressed dgc
|
||||||
|
application/x-director dir dcr dxr cst cct cxt w3d fgd swa
|
||||||
|
application/x-doom wad
|
||||||
|
application/x-dtbncx+xml ncx
|
||||||
|
application/x-dtbook+xml dtb
|
||||||
|
application/x-dtbresource+xml res
|
||||||
|
application/x-dvi dvi
|
||||||
|
application/x-envoy evy
|
||||||
|
application/x-eva eva
|
||||||
|
application/x-font-bdf bdf
|
||||||
|
application/x-font-ghostscript gsf
|
||||||
|
application/x-font-linux-psf psf
|
||||||
|
application/x-font-pcf pcf
|
||||||
|
application/x-font-snf snf
|
||||||
|
application/x-font-type1 pfa pfb pfm afm
|
||||||
|
application/x-freearc arc
|
||||||
|
application/x-futuresplash spl
|
||||||
|
application/x-gca-compressed gca
|
||||||
|
application/x-glulx ulx
|
||||||
|
application/x-gnumeric gnumeric
|
||||||
|
application/x-gramps-xml gramps
|
||||||
|
application/x-gtar gtar
|
||||||
|
application/x-hdf hdf
|
||||||
|
application/x-install-instructions install
|
||||||
|
application/x-iso9660-image iso
|
||||||
|
application/x-java-jnlp-file jnlp
|
||||||
|
application/x-latex latex
|
||||||
|
application/x-lzh-compressed lzh lha
|
||||||
|
application/x-mie mie
|
||||||
|
application/x-mobipocket-ebook prc mobi
|
||||||
|
application/x-ms-application application
|
||||||
|
application/x-ms-shortcut lnk
|
||||||
|
application/x-ms-wmd wmd
|
||||||
|
application/x-ms-wmz wmz
|
||||||
|
application/x-ms-xbap xbap
|
||||||
|
application/x-msaccess mdb
|
||||||
|
application/x-msbinder obd
|
||||||
|
application/x-mscardfile crd
|
||||||
|
application/x-msclip clp
|
||||||
|
application/x-msdownload exe dll com bat msi
|
||||||
|
application/x-msmediaview mvb m13 m14
|
||||||
|
application/x-msmetafile wmf wmz emf emz
|
||||||
|
application/x-msmoney mny
|
||||||
|
application/x-mspublisher pub
|
||||||
|
application/x-msschedule scd
|
||||||
|
application/x-msterminal trm
|
||||||
|
application/x-mswrite wri
|
||||||
|
application/x-netcdf nc cdf
|
||||||
|
application/x-nzb nzb
|
||||||
|
application/x-pkcs12 p12 pfx
|
||||||
|
application/x-pkcs7-certificates p7b spc
|
||||||
|
application/x-pkcs7-certreqresp p7r
|
||||||
|
application/x-rar-compressed rar
|
||||||
|
application/x-research-info-systems ris
|
||||||
|
application/x-sh sh
|
||||||
|
application/x-shar shar
|
||||||
|
application/x-shockwave-flash swf
|
||||||
|
application/x-silverlight-app xap
|
||||||
|
application/x-sql sql
|
||||||
|
application/x-stuffit sit
|
||||||
|
application/x-stuffitx sitx
|
||||||
|
application/x-subrip srt
|
||||||
|
application/x-sv4cpio sv4cpio
|
||||||
|
application/x-sv4crc sv4crc
|
||||||
|
application/x-t3vm-image t3
|
||||||
|
application/x-tads gam
|
||||||
|
application/x-tar tar
|
||||||
|
application/x-tcl tcl
|
||||||
|
application/x-tex tex
|
||||||
|
application/x-tex-tfm tfm
|
||||||
|
application/x-texinfo texinfo texi
|
||||||
|
application/x-tgif obj
|
||||||
|
application/x-ustar ustar
|
||||||
|
application/x-wais-source src
|
||||||
|
application/x-x509-ca-cert der crt
|
||||||
|
application/x-xfig fig
|
||||||
|
application/x-xliff+xml xlf
|
||||||
|
application/x-xpinstall xpi
|
||||||
|
application/x-xz xz
|
||||||
|
application/x-zmachine z1 z2 z3 z4 z5 z6 z7 z8
|
||||||
|
application/xaml+xml xaml
|
||||||
|
application/xcap-diff+xml xdf
|
||||||
|
application/xenc+xml xenc
|
||||||
|
application/xhtml+xml xhtml xht
|
||||||
|
application/xml xml xsl
|
||||||
|
application/xml-dtd dtd
|
||||||
|
application/xop+xml xop
|
||||||
|
application/xproc+xml xpl
|
||||||
|
application/xslt+xml xslt
|
||||||
|
application/xspf+xml xspf
|
||||||
|
application/xv+xml mxml xhvml xvml xvm
|
||||||
|
application/yang yang
|
||||||
|
application/yin+xml yin
|
||||||
|
application/zip zip
|
||||||
|
audio/adpcm adp
|
||||||
|
audio/basic au snd
|
||||||
|
audio/midi mid midi kar rmi
|
||||||
|
audio/mp4 m4a mp4a
|
||||||
|
audio/mpeg mpga mp2 mp2a mp3 m2a m3a
|
||||||
|
audio/ogg oga ogg spx
|
||||||
|
audio/s3m s3m
|
||||||
|
audio/silk sil
|
||||||
|
audio/vnd.dece.audio uva uvva
|
||||||
|
audio/vnd.digital-winds eol
|
||||||
|
audio/vnd.dra dra
|
||||||
|
audio/vnd.dts dts
|
||||||
|
audio/vnd.dts.hd dtshd
|
||||||
|
audio/vnd.lucent.voice lvp
|
||||||
|
audio/vnd.ms-playready.media.pya pya
|
||||||
|
audio/vnd.nuera.ecelp4800 ecelp4800
|
||||||
|
audio/vnd.nuera.ecelp7470 ecelp7470
|
||||||
|
audio/vnd.nuera.ecelp9600 ecelp9600
|
||||||
|
audio/vnd.rip rip
|
||||||
|
audio/webm weba
|
||||||
|
audio/x-aac aac
|
||||||
|
audio/x-aiff aif aiff aifc
|
||||||
|
audio/x-caf caf
|
||||||
|
audio/x-flac flac
|
||||||
|
audio/x-matroska mka
|
||||||
|
audio/x-mpegurl m3u
|
||||||
|
audio/x-ms-wax wax
|
||||||
|
audio/x-ms-wma wma
|
||||||
|
audio/x-pn-realaudio ram ra
|
||||||
|
audio/x-pn-realaudio-plugin rmp
|
||||||
|
audio/x-wav wav
|
||||||
|
audio/xm xm
|
||||||
|
chemical/x-cdx cdx
|
||||||
|
chemical/x-cif cif
|
||||||
|
chemical/x-cmdf cmdf
|
||||||
|
chemical/x-cml cml
|
||||||
|
chemical/x-csml csml
|
||||||
|
chemical/x-xyz xyz
|
||||||
|
font/collection ttc
|
||||||
|
font/otf otf
|
||||||
|
font/ttf ttf
|
||||||
|
font/woff woff
|
||||||
|
font/woff2 woff2
|
||||||
|
image/bmp bmp
|
||||||
|
image/cgm cgm
|
||||||
|
image/g3fax g3
|
||||||
|
image/gif gif
|
||||||
|
image/ief ief
|
||||||
|
image/jpeg jpeg jpg jpe
|
||||||
|
image/ktx ktx
|
||||||
|
image/png png
|
||||||
|
image/prs.btif btif
|
||||||
|
image/sgi sgi
|
||||||
|
image/svg+xml svg svgz
|
||||||
|
image/tiff tiff tif
|
||||||
|
image/vnd.adobe.photoshop psd
|
||||||
|
image/vnd.dece.graphic uvi uvvi uvg uvvg
|
||||||
|
image/vnd.djvu djvu djv
|
||||||
|
image/vnd.dvb.subtitle sub
|
||||||
|
image/vnd.dwg dwg
|
||||||
|
image/vnd.dxf dxf
|
||||||
|
image/vnd.fastbidsheet fbs
|
||||||
|
image/vnd.fpx fpx
|
||||||
|
image/vnd.fst fst
|
||||||
|
image/vnd.fujixerox.edmics-mmr mmr
|
||||||
|
image/vnd.fujixerox.edmics-rlc rlc
|
||||||
|
image/vnd.ms-modi mdi
|
||||||
|
image/vnd.ms-photo wdp
|
||||||
|
image/vnd.net-fpx npx
|
||||||
|
image/vnd.wap.wbmp wbmp
|
||||||
|
image/vnd.xiff xif
|
||||||
|
image/webp webp
|
||||||
|
image/x-3ds 3ds
|
||||||
|
image/x-cmu-raster ras
|
||||||
|
image/x-cmx cmx
|
||||||
|
image/x-freehand fh fhc fh4 fh5 fh7
|
||||||
|
image/x-icon ico
|
||||||
|
image/x-mrsid-image sid
|
||||||
|
image/x-pcx pcx
|
||||||
|
image/x-pict pic pct
|
||||||
|
image/x-portable-anymap pnm
|
||||||
|
image/x-portable-bitmap pbm
|
||||||
|
image/x-portable-graymap pgm
|
||||||
|
image/x-portable-pixmap ppm
|
||||||
|
image/x-rgb rgb
|
||||||
|
image/x-tga tga
|
||||||
|
image/x-xbitmap xbm
|
||||||
|
image/x-xpixmap xpm
|
||||||
|
image/x-xwindowdump xwd
|
||||||
|
message/rfc822 eml mime
|
||||||
|
model/iges igs iges
|
||||||
|
model/mesh msh mesh silo
|
||||||
|
model/vnd.collada+xml dae
|
||||||
|
model/vnd.dwf dwf
|
||||||
|
model/vnd.gdl gdl
|
||||||
|
model/vnd.gtw gtw
|
||||||
|
model/vnd.mts mts
|
||||||
|
model/vnd.vtu vtu
|
||||||
|
model/vrml wrl vrml
|
||||||
|
model/x3d+binary x3db x3dbz
|
||||||
|
model/x3d+vrml x3dv x3dvz
|
||||||
|
model/x3d+xml x3d x3dz
|
||||||
|
text/cache-manifest appcache
|
||||||
|
text/calendar ics ifb
|
||||||
|
text/css css
|
||||||
|
text/csv csv
|
||||||
|
text/html html htm
|
||||||
|
text/n3 n3
|
||||||
|
text/plain txt text conf def list log in
|
||||||
|
text/prs.lines.tag dsc
|
||||||
|
text/richtext rtx
|
||||||
|
text/sgml sgml sgm
|
||||||
|
text/tab-separated-values tsv
|
||||||
|
text/troff t tr roff man me ms
|
||||||
|
text/turtle ttl
|
||||||
|
text/uri-list uri uris urls
|
||||||
|
text/vcard vcard
|
||||||
|
text/vnd.curl curl
|
||||||
|
text/vnd.curl.dcurl dcurl
|
||||||
|
text/vnd.curl.mcurl mcurl
|
||||||
|
text/vnd.curl.scurl scurl
|
||||||
|
text/vnd.dvb.subtitle sub
|
||||||
|
text/vnd.fly fly
|
||||||
|
text/vnd.fmi.flexstor flx
|
||||||
|
text/vnd.graphviz gv
|
||||||
|
text/vnd.in3d.3dml 3dml
|
||||||
|
text/vnd.in3d.spot spot
|
||||||
|
text/vnd.sun.j2me.app-descriptor jad
|
||||||
|
text/vnd.wap.wml wml
|
||||||
|
text/vnd.wap.wmlscript wmls
|
||||||
|
text/x-asm s asm
|
||||||
|
text/x-c c cc cxx cpp h hh dic
|
||||||
|
text/x-fortran f for f77 f90
|
||||||
|
text/x-java-source java
|
||||||
|
text/x-nfo nfo
|
||||||
|
text/x-opml opml
|
||||||
|
text/x-pascal p pas
|
||||||
|
text/x-setext etx
|
||||||
|
text/x-sfv sfv
|
||||||
|
text/x-uuencode uu
|
||||||
|
text/x-vcalendar vcs
|
||||||
|
text/x-vcard vcf
|
||||||
|
video/3gpp 3gp
|
||||||
|
video/3gpp2 3g2
|
||||||
|
video/h261 h261
|
||||||
|
video/h263 h263
|
||||||
|
video/h264 h264
|
||||||
|
video/jpeg jpgv
|
||||||
|
video/jpm jpm jpgm
|
||||||
|
video/mj2 mj2 mjp2
|
||||||
|
video/mp4 mp4 mp4v mpg4
|
||||||
|
video/mpeg mpeg mpg mpe m1v m2v
|
||||||
|
video/ogg ogv
|
||||||
|
video/quicktime qt mov
|
||||||
|
video/vnd.dece.hd uvh uvvh
|
||||||
|
video/vnd.dece.mobile uvm uvvm
|
||||||
|
video/vnd.dece.pd uvp uvvp
|
||||||
|
video/vnd.dece.sd uvs uvvs
|
||||||
|
video/vnd.dece.video uvv uvvv
|
||||||
|
video/vnd.dvb.file dvb
|
||||||
|
video/vnd.fvt fvt
|
||||||
|
video/vnd.mpegurl mxu m4u
|
||||||
|
video/vnd.ms-playready.media.pyv pyv
|
||||||
|
video/vnd.uvvu.mp4 uvu uvvu
|
||||||
|
video/vnd.vivo viv
|
||||||
|
video/webm webm
|
||||||
|
video/x-f4v f4v
|
||||||
|
video/x-fli fli
|
||||||
|
video/x-flv flv
|
||||||
|
video/x-m4v m4v
|
||||||
|
video/x-matroska mkv mk3d mks
|
||||||
|
video/x-mng mng
|
||||||
|
video/x-ms-asf asf asx
|
||||||
|
video/x-ms-vob vob
|
||||||
|
video/x-ms-wm wm
|
||||||
|
video/x-ms-wmv wmv
|
||||||
|
video/x-ms-wmx wmx
|
||||||
|
video/x-ms-wvx wvx
|
||||||
|
video/x-msvideo avi
|
||||||
|
video/x-sgi-movie movie
|
||||||
|
video/x-smv smv
|
||||||
|
x-conference/x-cooltalk ice
|
||||||
20
src/middleware/mime.ts
Normal file
20
src/middleware/mime.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import mime_raw from './mime-types.md';
|
||||||
|
|
||||||
|
const map: Map<string, string> = new Map();
|
||||||
|
|
||||||
|
mime_raw.split('\n').forEach((row: string) => {
|
||||||
|
const match = /(.+?)\t+(.+)/.exec(row);
|
||||||
|
if (!match) return;
|
||||||
|
|
||||||
|
const type = match[1];
|
||||||
|
const extensions = match[2].split(' ');
|
||||||
|
|
||||||
|
extensions.forEach(ext => {
|
||||||
|
map.set(ext, type);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export function lookup(file: string) {
|
||||||
|
const match = /\.([^\.]+)$/.exec(file);
|
||||||
|
return match && map.get(match[1]);
|
||||||
|
}
|
||||||
51
src/rollup.ts
Normal file
51
src/rollup.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { locations, dev } from './config';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
dev: dev(),
|
||||||
|
|
||||||
|
client: {
|
||||||
|
input: () => {
|
||||||
|
return `${locations.app()}/client.js`
|
||||||
|
},
|
||||||
|
|
||||||
|
output: () => {
|
||||||
|
let dir = `${locations.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 `${locations.app()}/server.js`
|
||||||
|
},
|
||||||
|
|
||||||
|
output: () => {
|
||||||
|
return {
|
||||||
|
dir: locations.dest(),
|
||||||
|
format: 'cjs',
|
||||||
|
sourcemap: dev()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
serviceworker: {
|
||||||
|
input: () => {
|
||||||
|
return `${locations.app()}/service-worker.js`;
|
||||||
|
},
|
||||||
|
|
||||||
|
output: () => {
|
||||||
|
return {
|
||||||
|
file: `${locations.dest()}/service-worker.js`,
|
||||||
|
format: 'iife'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
504
src/runtime/index.ts
Normal file
504
src/runtime/index.ts
Normal file
@@ -0,0 +1,504 @@
|
|||||||
|
import { detach, findAnchor, scroll_state, which } from './utils';
|
||||||
|
import { Component, ComponentConstructor, Params, Query, Redirect, Manifest, RouteData, ScrollPosition, Store, Target, ComponentLoader } from './interfaces';
|
||||||
|
|
||||||
|
const initial_data = typeof window !== 'undefined' && window.__SAPPER__;
|
||||||
|
|
||||||
|
export let root: Component;
|
||||||
|
let target: Node;
|
||||||
|
let store: Store;
|
||||||
|
let manifest: Manifest;
|
||||||
|
let segments: string[] = [];
|
||||||
|
|
||||||
|
type RootProps = {
|
||||||
|
path: string;
|
||||||
|
params: Record<string, string>;
|
||||||
|
query: Record<string, string>;
|
||||||
|
child: Child;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Child = {
|
||||||
|
segment?: string;
|
||||||
|
props?: any;
|
||||||
|
component?: Component;
|
||||||
|
};
|
||||||
|
|
||||||
|
const root_props: RootProps = {
|
||||||
|
path: null,
|
||||||
|
params: null,
|
||||||
|
query: null,
|
||||||
|
child: {
|
||||||
|
segment: null,
|
||||||
|
component: null,
|
||||||
|
props: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { root as component }; // legacy reasons — drop in a future version
|
||||||
|
|
||||||
|
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;
|
||||||
|
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 pages
|
||||||
|
if (manifest.ignore.some(pattern => pattern.test(path))) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < manifest.pages.length; i += 1) {
|
||||||
|
const page = manifest.pages[i];
|
||||||
|
|
||||||
|
const match = page.pattern.exec(path);
|
||||||
|
if (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] = decodeURIComponent((value || '').replace(/\+/g, ' '));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { url, path, page, match, query };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_token: {};
|
||||||
|
|
||||||
|
function render(data: any, nullable_depth: number, scroll: ScrollPosition, token: {}) {
|
||||||
|
if (current_token !== token) return;
|
||||||
|
|
||||||
|
if (root) {
|
||||||
|
// first, clear out highest-level root component
|
||||||
|
let level = data.child;
|
||||||
|
for (let i = 0; i < nullable_depth; i += 1) {
|
||||||
|
if (i === nullable_depth) break;
|
||||||
|
level = level.props.child;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { component } = level;
|
||||||
|
level.component = null;
|
||||||
|
root.set({ child: data.child });
|
||||||
|
|
||||||
|
// then render new stuff
|
||||||
|
level.component = component;
|
||||||
|
root.set(data);
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(data, root_data);
|
||||||
|
|
||||||
|
root = new manifest.root({
|
||||||
|
target,
|
||||||
|
data,
|
||||||
|
store,
|
||||||
|
hydrate: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scroll) {
|
||||||
|
window.scrollTo(scroll.x, scroll.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(root_props, data);
|
||||||
|
ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changed(a: Record<string, string | true>, b: Record<string, string | true>) {
|
||||||
|
return JSON.stringify(a) !== JSON.stringify(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
let root_preload: Promise<any>;
|
||||||
|
let root_data: any;
|
||||||
|
|
||||||
|
function load_css(chunk: string) {
|
||||||
|
const href = `${initial_data.baseUrl}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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function load_component(component: ComponentLoader): Promise<ComponentConstructor> {
|
||||||
|
// 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].default);
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepare_page(target: Target): Promise<{
|
||||||
|
redirect?: Redirect;
|
||||||
|
data?: any;
|
||||||
|
nullable_depth?: number;
|
||||||
|
}> {
|
||||||
|
const { page, path, query } = target;
|
||||||
|
const new_segments = path.split('/').filter(Boolean);
|
||||||
|
let changed_from = 0;
|
||||||
|
|
||||||
|
while (
|
||||||
|
segments[changed_from] &&
|
||||||
|
new_segments[changed_from] &&
|
||||||
|
segments[changed_from] === new_segments[changed_from]
|
||||||
|
) changed_from += 1;
|
||||||
|
|
||||||
|
let redirect: Redirect = null;
|
||||||
|
let error: { statusCode: number, message: Error | string } = null;
|
||||||
|
|
||||||
|
const preload_context = {
|
||||||
|
store,
|
||||||
|
fetch: (url: string, opts?: any) => window.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: (statusCode: number, message: Error | string) => {
|
||||||
|
error = { statusCode, message };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!root_preload) {
|
||||||
|
root_preload = manifest.root.preload
|
||||||
|
? initial_data.preloaded[0] || manifest.root.preload.call(preload_context, {
|
||||||
|
path,
|
||||||
|
query,
|
||||||
|
params: {}
|
||||||
|
})
|
||||||
|
: {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(page.parts.map(async (part, i) => {
|
||||||
|
if (i < changed_from) return null;
|
||||||
|
if (!part) return null;
|
||||||
|
|
||||||
|
const Component = await load_component(part.component);
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
path,
|
||||||
|
query,
|
||||||
|
params: part.params ? part.params(target.match) : {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const preloaded = ready || !initial_data.preloaded[i + 1]
|
||||||
|
? Component.preload ? await Component.preload.call(preload_context, req) : {}
|
||||||
|
: initial_data.preloaded[i + 1];
|
||||||
|
|
||||||
|
return { Component, preloaded };
|
||||||
|
})).catch(err => {
|
||||||
|
error = { statusCode: 500, message: err };
|
||||||
|
return [];
|
||||||
|
}).then(async results => {
|
||||||
|
if (!root_data) root_data = await root_preload;
|
||||||
|
|
||||||
|
if (redirect) {
|
||||||
|
return { redirect };
|
||||||
|
}
|
||||||
|
|
||||||
|
segments = new_segments;
|
||||||
|
|
||||||
|
const get_params = page.parts[page.parts.length - 1].params || (() => ({}));
|
||||||
|
const params = get_params(target.match);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
const props = {
|
||||||
|
path,
|
||||||
|
query,
|
||||||
|
params,
|
||||||
|
error: typeof error.message === 'string' ? new Error(error.message) : error.message,
|
||||||
|
status: error.statusCode
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: Object.assign({}, props, {
|
||||||
|
preloading: false,
|
||||||
|
child: {
|
||||||
|
component: manifest.error,
|
||||||
|
props
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = { path, query };
|
||||||
|
const data = {
|
||||||
|
path,
|
||||||
|
preloading: false,
|
||||||
|
child: Object.assign({}, root_props.child, {
|
||||||
|
segment: segments[0]
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if (changed(query, root_props.query)) data.query = query;
|
||||||
|
if (changed(params, root_props.params)) data.params = params;
|
||||||
|
|
||||||
|
let level = data.child;
|
||||||
|
let nullable_depth = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < page.parts.length; i += 1) {
|
||||||
|
const part = page.parts[i];
|
||||||
|
if (!part) continue;
|
||||||
|
|
||||||
|
const get_params = part.params || (() => ({}));
|
||||||
|
|
||||||
|
if (i < changed_from) {
|
||||||
|
level.props.path = path;
|
||||||
|
level.props.query = query;
|
||||||
|
level.props.child = Object.assign({}, level.props.child);
|
||||||
|
|
||||||
|
nullable_depth += 1;
|
||||||
|
} else {
|
||||||
|
level.component = results[i].Component;
|
||||||
|
level.props = Object.assign({}, level.props, props, {
|
||||||
|
params: get_params(target.match),
|
||||||
|
}, results[i].preloaded);
|
||||||
|
|
||||||
|
level.props.child = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
level = level.props.child;
|
||||||
|
level.segment = segments[i + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data, nullable_depth };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function navigate(target: Target, id: number): Promise<any> {
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (root) {
|
||||||
|
root.set({ preloading: true });
|
||||||
|
}
|
||||||
|
const loaded = prefetching && prefetching.href === target.url.href ?
|
||||||
|
prefetching.promise :
|
||||||
|
prepare_page(target);
|
||||||
|
|
||||||
|
prefetching = null;
|
||||||
|
|
||||||
|
const token = current_token = {};
|
||||||
|
const { redirect, data, nullable_depth } = await loaded;
|
||||||
|
|
||||||
|
if (redirect) {
|
||||||
|
await goto(redirect.location, { replaceState: true });
|
||||||
|
} else {
|
||||||
|
render(data, nullable_depth, scroll_history[id], token);
|
||||||
|
if (document.activeElement) document.activeElement.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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 === 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);
|
||||||
|
if (target) {
|
||||||
|
navigate(target, event.state.id);
|
||||||
|
} else {
|
||||||
|
window.location.href = window.location.href;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// hashchange
|
||||||
|
cid = ++uid;
|
||||||
|
history.replaceState({ id: cid }, '', window.location.href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prefetching: {
|
||||||
|
href: string;
|
||||||
|
promise: Promise<{ redirect?: Redirect, data?: any, nullable_depth?: number }>;
|
||||||
|
} = null;
|
||||||
|
|
||||||
|
export function prefetch(href: string) {
|
||||||
|
const target: Target = select_route(new URL(href, document.baseURI));
|
||||||
|
|
||||||
|
if (target && (!prefetching || href !== prefetching.href)) {
|
||||||
|
prefetching = {
|
||||||
|
href,
|
||||||
|
promise: prepare_page(target)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>findAnchor(<Node>event.target);
|
||||||
|
if (!a || a.rel !== 'prefetch') return;
|
||||||
|
|
||||||
|
prefetch(a.href);
|
||||||
|
}
|
||||||
|
|
||||||
|
let inited: boolean;
|
||||||
|
let ready = false;
|
||||||
|
|
||||||
|
export function init(opts: {
|
||||||
|
App: ComponentConstructor,
|
||||||
|
target: Node,
|
||||||
|
manifest: Manifest,
|
||||||
|
store?: (data: any) => Store,
|
||||||
|
routes?: any // legacy
|
||||||
|
}) {
|
||||||
|
if (opts instanceof HTMLElement) {
|
||||||
|
throw new Error(`The signature of init(...) has changed — see https://sapper.svelte.technology/guide#0-11-to-0-12 for more information`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.routes) {
|
||||||
|
throw new Error(`As of Sapper 0.15, opts.routes should be opts.manifest`);
|
||||||
|
}
|
||||||
|
|
||||||
|
target = opts.target;
|
||||||
|
manifest = opts.manifest;
|
||||||
|
|
||||||
|
if (opts && opts.store) {
|
||||||
|
store = opts.store(initial_data.store);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inited) { // this check makes HMR possible
|
||||||
|
window.addEventListener('click', handle_click);
|
||||||
|
window.addEventListener('popstate', handle_popstate);
|
||||||
|
|
||||||
|
// prefetch
|
||||||
|
window.addEventListener('touchstart', trigger_prefetch);
|
||||||
|
window.addEventListener('mousemove', handle_mousemove);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (!initial_data.error) {
|
||||||
|
const target = select_route(new URL(window.location.href));
|
||||||
|
if (target) return navigate(target, uid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function goto(href: string, opts = { replaceState: false }) {
|
||||||
|
const target = select_route(new URL(href, document.baseURI));
|
||||||
|
let promise;
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
promise = navigate(target, null);
|
||||||
|
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
|
||||||
|
} else {
|
||||||
|
window.location.href = href;
|
||||||
|
promise = new Promise(f => {}); // never resolves
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prefetchRoutes(pathnames: string[]) {
|
||||||
|
if (!manifest) throw new Error(`You must call init() first`);
|
||||||
|
|
||||||
|
return manifest.pages
|
||||||
|
.filter(route => {
|
||||||
|
if (!pathnames) return true;
|
||||||
|
return pathnames.some(pathname => route.pattern.test(pathname));
|
||||||
|
})
|
||||||
|
.reduce((promise: Promise<any>, route) => promise.then(() => {
|
||||||
|
return Promise.all(route.parts.map(part => part && load_component(part.component)));
|
||||||
|
}), Promise.resolve());
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove this in 0.9
|
||||||
|
export { prefetchRoutes as preloadRoutes };
|
||||||
54
src/runtime/interfaces.ts
Normal file
54
src/runtime/interfaces.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { Store } from '../interfaces';
|
||||||
|
|
||||||
|
export { Store };
|
||||||
|
export type Params = Record<string, string>;
|
||||||
|
export type Query = Record<string, string | true>;
|
||||||
|
export type RouteData = { params: Params, query: Query, path: string };
|
||||||
|
|
||||||
|
export interface ComponentConstructor {
|
||||||
|
new (options: { target: Node, data: any, store: Store, 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 Page = {
|
||||||
|
pattern: RegExp;
|
||||||
|
parts: Array<{
|
||||||
|
component: ComponentLoader;
|
||||||
|
params?: (match: RegExpExecArray) => Record<string, string>;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Manifest = {
|
||||||
|
ignore: RegExp[];
|
||||||
|
root: ComponentConstructor;
|
||||||
|
error: () => Promise<{ default: ComponentConstructor }>;
|
||||||
|
pages: Page[]
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScrollPosition = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Target = {
|
||||||
|
url: URL;
|
||||||
|
path: string;
|
||||||
|
page: Page;
|
||||||
|
match: RegExpExecArray;
|
||||||
|
query: Record<string, string | true>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Redirect = {
|
||||||
|
statusCode: number;
|
||||||
|
location: string;
|
||||||
|
};
|
||||||
19
src/runtime/utils.ts
Normal file
19
src/runtime/utils.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
10
src/utils.ts
Normal file
10
src/utils.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
55
src/webpack.ts
Normal file
55
src/webpack.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { locations, dev } from './config';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
dev: dev(),
|
||||||
|
|
||||||
|
client: {
|
||||||
|
entry: () => {
|
||||||
|
return {
|
||||||
|
main: `${locations.app()}/client`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
output: () => {
|
||||||
|
return {
|
||||||
|
path: `${locations.dest()}/client`,
|
||||||
|
filename: '[hash]/[name].js',
|
||||||
|
chunkFilename: '[hash]/[name].[id].js',
|
||||||
|
publicPath: `client/`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
server: {
|
||||||
|
entry: () => {
|
||||||
|
return {
|
||||||
|
server: `${locations.app()}/server`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
output: () => {
|
||||||
|
return {
|
||||||
|
path: locations.dest(),
|
||||||
|
filename: '[name].js',
|
||||||
|
chunkFilename: '[hash]/[name].[id].js',
|
||||||
|
libraryTarget: 'commonjs2'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
serviceworker: {
|
||||||
|
entry: () => {
|
||||||
|
return {
|
||||||
|
'service-worker': `${locations.app()}/service-worker`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
output: () => {
|
||||||
|
return {
|
||||||
|
path: locations.dest(),
|
||||||
|
filename: '[name].js',
|
||||||
|
chunkFilename: '[name].[id].[hash].js'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
2
test/app/.gitignore
vendored
2
test/app/.gitignore
vendored
@@ -3,3 +3,5 @@ node_modules
|
|||||||
.sapper
|
.sapper
|
||||||
yarn.lock
|
yarn.lock
|
||||||
cypress/screenshots
|
cypress/screenshots
|
||||||
|
templates/.*
|
||||||
|
dist
|
||||||
|
|||||||
14
test/app/app/client.js
Normal file
14
test/app/app/client.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { init, goto, prefetchRoutes } from '../../../runtime.js';
|
||||||
|
import { Store } from 'svelte/store.js';
|
||||||
|
import { manifest } from './manifest/client.js';
|
||||||
|
|
||||||
|
window.init = () => {
|
||||||
|
return init({
|
||||||
|
target: document.querySelector('#sapper'),
|
||||||
|
manifest,
|
||||||
|
store: data => new Store(data)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.prefetchRoutes = prefetchRoutes;
|
||||||
|
window.goto = goto;
|
||||||
128
test/app/app/server.js
Normal file
128
test/app/app/server.js
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import { resolve } from 'url';
|
||||||
|
import express from 'express';
|
||||||
|
import serve from 'serve-static';
|
||||||
|
import sapper from '../../../dist/middleware.js';
|
||||||
|
import { Store } from 'svelte/store.js';
|
||||||
|
import { manifest } 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 = express();
|
||||||
|
|
||||||
|
const { PORT = 3000, BASEPATH = '' } = process.env;
|
||||||
|
const base = `http://localhost:${PORT}${BASEPATH}/`;
|
||||||
|
|
||||||
|
// this allows us to do e.g. `fetch('/api/blog')` on the server
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
global.fetch = (url, opts) => {
|
||||||
|
return fetch(resolve(base, url), opts);
|
||||||
|
};
|
||||||
|
|
||||||
|
const middlewares = [
|
||||||
|
serve('assets'),
|
||||||
|
|
||||||
|
// set test cookie
|
||||||
|
(req, res, next) => {
|
||||||
|
res.setHeader('Set-Cookie', 'test=woohoo!; Max-Age=3600');
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
|
||||||
|
// emit messages so we can capture requests
|
||||||
|
(req, res, next) => {
|
||||||
|
if (!pending) return next();
|
||||||
|
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
|
||||||
|
// set up some values for the store
|
||||||
|
(req, res, next) => {
|
||||||
|
req.hello = 'hello';
|
||||||
|
res.locals = { name: 'world' };
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
|
||||||
|
sapper({
|
||||||
|
manifest,
|
||||||
|
store: (req, res) => {
|
||||||
|
return new Store({
|
||||||
|
title: `${req.hello} ${res.locals.name}`
|
||||||
|
});
|
||||||
|
},
|
||||||
|
ignore: [
|
||||||
|
/foobar/i,
|
||||||
|
'/buzz',
|
||||||
|
'fizz',
|
||||||
|
x => x === '/hello'
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
app.get(`${BASEPATH}/non-sapper-redirect-from`, (req, res) => {
|
||||||
|
res.writeHead(301, {
|
||||||
|
Location: `${BASEPATH}/non-sapper-redirect-to`
|
||||||
|
});
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (BASEPATH) {
|
||||||
|
app.use(BASEPATH, ...middlewares);
|
||||||
|
} else {
|
||||||
|
app.use(...middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
['foobar', 'buzz', 'fizzer', 'hello'].forEach(uri => {
|
||||||
|
app.get('/'+uri, (req, res) => res.end(uri));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT);
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
const ASSETS = `cache__timestamp__`;
|
import { assets, shell, timestamp, routes } from './manifest/service-worker.js';
|
||||||
|
|
||||||
|
const ASSETS = `cachetimestamp`;
|
||||||
|
|
||||||
// `shell` is an array of all the files generated by webpack,
|
// `shell` is an array of all the files generated by webpack,
|
||||||
// `assets` is an array of everything in the `assets` directory
|
// `assets` is an array of everything in the `assets` directory
|
||||||
const to_cache = __shell__.concat(__assets__);
|
const to_cache = shell.concat(assets);
|
||||||
const cached = new Set(to_cache);
|
const cached = new Set(to_cache);
|
||||||
|
|
||||||
// `routes` is an array of `{ pattern: RegExp }` objects that
|
|
||||||
// match the pages in your app
|
|
||||||
const routes = __routes__;
|
|
||||||
|
|
||||||
self.addEventListener('install', event => {
|
self.addEventListener('install', event => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
caches
|
caches
|
||||||
@@ -1,19 +1,15 @@
|
|||||||
<!doctype>
|
<!doctype html>
|
||||||
<html lang='en'>
|
<html lang='en'>
|
||||||
<head>
|
<head>
|
||||||
<meta charset='utf-8'>
|
<meta charset='utf-8'>
|
||||||
<meta name='viewport' content='width=device-width'>
|
<meta name='viewport' content='width=device-width'>
|
||||||
<meta name='theme-color' content='#aa1e1e'>
|
<meta name='theme-color' content='#aa1e1e'>
|
||||||
|
|
||||||
<link rel='stylesheet' href='/global.css'>
|
%sapper.base%
|
||||||
<link rel='manifest' href='/manifest.json'>
|
|
||||||
<link rel='icon' type='image/png' href='/favicon.png'>
|
|
||||||
|
|
||||||
<script>
|
<link rel='stylesheet' href='global.css'>
|
||||||
if ('serviceWorker' in navigator) {
|
<link rel='manifest' href='manifest.json'>
|
||||||
navigator.serviceWorker.register('/service-worker.js');
|
<link rel='icon' type='image/png' href='favicon.png'>
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Sapper generates a <style> tag containing critical CSS
|
<!-- Sapper generates a <style> tag containing critical CSS
|
||||||
for the current page. CSS for the rest of the app is
|
for the current page. CSS for the rest of the app is
|
||||||
@@ -32,6 +28,6 @@
|
|||||||
<!-- Sapper creates a <script> tag containing `templates/main.js`
|
<!-- Sapper creates a <script> tag containing `templates/main.js`
|
||||||
and anything else it needs to hydrate the app and
|
and anything else it needs to hydrate the app and
|
||||||
initialise the router -->
|
initialise the router -->
|
||||||
<script src='%sapper.main%'></script>
|
%sapper.scripts%
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
4931
test/app/package-lock.json
generated
4931
test/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "TODO",
|
|
||||||
"description": "TODO",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "node server.js",
|
|
||||||
"build": "sapper build",
|
|
||||||
"start": "cross-env NODE_ENV=production node server.js",
|
|
||||||
"prestart": "npm run build"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"compression": "^1.7.1",
|
|
||||||
"cross-env": "^5.1.1",
|
|
||||||
"css-loader": "^0.28.7",
|
|
||||||
"express": "^4.16.2",
|
|
||||||
"extract-text-webpack-plugin": "^3.0.2",
|
|
||||||
"glob": "^7.1.2",
|
|
||||||
"marked": "^0.3.7",
|
|
||||||
"node-fetch": "^1.7.3",
|
|
||||||
"npm-run-all": "^4.1.2",
|
|
||||||
"serve-static": "^1.13.1",
|
|
||||||
"style-loader": "^0.19.0",
|
|
||||||
"svelte": "^1.49.1",
|
|
||||||
"svelte-loader": "^2.2.1",
|
|
||||||
"uglifyjs-webpack-plugin": "^1.1.2",
|
|
||||||
"webpack": "^3.10.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
20
test/app/routes/[x]/[y]/[z].html
Normal file
20
test/app/routes/[x]/[y]/[z].html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<span>z: {segment} {count}</span>
|
||||||
|
<a href="foo/bar/qux"></a>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import counts from '../_counts.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preload() {
|
||||||
|
return {
|
||||||
|
count: counts.z += 1
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
oncreate() {
|
||||||
|
this.set({
|
||||||
|
segment: this.get().params.z
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
22
test/app/routes/[x]/[y]/_layout.html
Normal file
22
test/app/routes/[x]/[y]/_layout.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<span>y: {segment} {count}</span>
|
||||||
|
<svelte:component this={child.component} {...child.props}/>
|
||||||
|
|
||||||
|
<span>child segment: {child.segment}</span>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import counts from '../_counts.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preload() {
|
||||||
|
return {
|
||||||
|
count: counts.y += 1
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
oncreate() {
|
||||||
|
this.set({
|
||||||
|
segment: this.get().params.y
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
5
test/app/routes/[x]/_counts.js
Normal file
5
test/app/routes/[x]/_counts.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
x: process.browser ? 1 : 0,
|
||||||
|
y: process.browser ? 1 : 0,
|
||||||
|
z: process.browser ? 1 : 0
|
||||||
|
};
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<Nav page={{page}}/>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<slot></slot>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Nav from './Nav.html';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Nav
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
<nav>
|
|
||||||
<ul>
|
|
||||||
<li><a class='{{page === "home" ? "selected" : ""}}' href='/'>home</a></li>
|
|
||||||
<li><a class='{{page === "about" ? "selected" : ""}}' href='/about'>about</a></li>
|
|
||||||
|
|
||||||
<!-- for the blog link, we're using rel=prefetch so that Sapper prefetches
|
|
||||||
the blog data when we hover over the link or tap it on a touchscreen -->
|
|
||||||
<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>
|
|
||||||
6
test/app/routes/_error.html
Normal file
6
test/app/routes/_error.html
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svelte:head>
|
||||||
|
<title>{status}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<h1>{status}</h1>
|
||||||
|
<p>{error.message}</p>
|
||||||
15
test/app/routes/_layout.html
Normal file
15
test/app/routes/_layout.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{#if preloading}
|
||||||
|
<progress class='preloading-progress' value=0.5/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<svelte:component this={child.component} {rootPreloadFunctionRan} {...child.props}/>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
preload() {
|
||||||
|
return {
|
||||||
|
rootPreloadFunctionRan: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -1,19 +1,27 @@
|
|||||||
<:Head>
|
<svelte:head>
|
||||||
<title>About</title>
|
<title>About</title>
|
||||||
</:Head>
|
</svelte:head>
|
||||||
|
|
||||||
<Layout page='about'>
|
<h1>About this site</h1>
|
||||||
<h1>About this site</h1>
|
|
||||||
|
|
||||||
<p>This is the 'about' page. There's not much here.</p>
|
<p>This is the 'about' page. There's not much here.</p>
|
||||||
</Layout>
|
|
||||||
|
<button class='prefetch' on:click='prefetch("blog/why-the-name")'>Why the name?</button>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Layout from './_components/Layout.html';
|
import { goto, prefetch } from '../../../runtime.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
oncreate() {
|
||||||
Layout
|
window.goto = goto;
|
||||||
|
},
|
||||||
|
|
||||||
|
ondestroy() {
|
||||||
|
window.goto = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
prefetch
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
9
test/app/routes/api/delete/[id].js
Normal file
9
test/app/routes/api/delete/[id].js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export function del(req, res) {
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
});
|
||||||
|
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
id: req.params.id
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -1,66 +1,35 @@
|
|||||||
<:Head>
|
<svelte:head>
|
||||||
<title>{{post.title}}</title>
|
<title>{post.title}</title>
|
||||||
</:Head>
|
</svelte:head>
|
||||||
|
|
||||||
<Layout page='blog'>
|
<h1>{post.title}</h1>
|
||||||
<h1>{{post.title}}</h1>
|
|
||||||
|
|
||||||
<div class='content'>
|
<div class='content'>
|
||||||
{{{post.html}}}
|
{@html post.html}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/*
|
|
||||||
By default, CSS is locally scoped to the component,
|
|
||||||
and any unused styles are dead-code-eliminated.
|
|
||||||
In this page, Svelte can't know which elements are
|
|
||||||
going to appear inside the {{{post.html}}} block,
|
|
||||||
so we have to use the :global(...) modifier to target
|
|
||||||
all elements inside .content
|
|
||||||
*/
|
|
||||||
.content :global(h2) {
|
|
||||||
font-size: 1.4em;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content :global(pre) {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
box-shadow: inset 1px 1px 5px rgba(0,0,0,0.05);
|
|
||||||
padding: 0.5em;
|
|
||||||
border-radius: 2px;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content :global(pre) :global(code) {
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content :global(ul) {
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content :global(li) {
|
|
||||||
margin: 0 0 0.5em 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Layout from '../_components/Layout.html';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
|
||||||
Layout
|
|
||||||
},
|
|
||||||
|
|
||||||
preload({ params, query }) {
|
preload({ params, query }) {
|
||||||
// the `slug` parameter is available because this file
|
// the `slug` parameter is available because this file
|
||||||
// is called [slug].html
|
// is called [slug].html
|
||||||
const { slug } = params;
|
const { slug } = params;
|
||||||
|
|
||||||
return fetch(`/api/blog/${slug}`).then(r => r.json()).then(post => {
|
if (slug === 'throw-an-error') {
|
||||||
return { post };
|
return this.error(500, 'something went wrong');
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(`blog/${slug}.json`).then(r => {
|
||||||
|
if (r.status === 200) {
|
||||||
|
return r.json().then(post => ({ post }));
|
||||||
|
this.error(r.status, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r.status === 404) {
|
||||||
|
this.error(404, 'Not found');
|
||||||
|
} else {
|
||||||
|
throw new Error('Something went wrong');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ export function get(req, res, next) {
|
|||||||
const { slug } = req.params;
|
const { slug } = req.params;
|
||||||
|
|
||||||
if (slug in lookup) {
|
if (slug in lookup) {
|
||||||
res.set({
|
res.writeHead(200, {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Cache-Control': `max-age=${30 * 60 * 1e3}` // cache for 30 minutes
|
'Cache-Control': `no-cache`
|
||||||
});
|
});
|
||||||
|
|
||||||
res.end(lookup[slug]);
|
res.end(lookup[slug]);
|
||||||
@@ -14,7 +14,7 @@ const posts = [
|
|||||||
html: `
|
html: `
|
||||||
<p>First, you have to know what <a href='https://svelte.technology'>Svelte</a> is. Svelte is a UI framework with a bold new idea: rather than providing a library that you write code with (like React or Vue, for example), it's a compiler that turns your components into highly optimized vanilla JavaScript. If you haven't already read the <a href='https://svelte.technology/blog/frameworks-without-the-framework'>introductory blog post</a>, you should!</p>
|
<p>First, you have to know what <a href='https://svelte.technology'>Svelte</a> is. Svelte is a UI framework with a bold new idea: rather than providing a library that you write code with (like React or Vue, for example), it's a compiler that turns your components into highly optimized vanilla JavaScript. If you haven't already read the <a href='https://svelte.technology/blog/frameworks-without-the-framework'>introductory blog post</a>, you should!</p>
|
||||||
|
|
||||||
<p>Sapper is a Next.js-style framework (<a href='/blog/how-is-sapper-different-from-next'>more on that here</a>) built around Svelte. It makes it embarrassingly easy to create extremely high performance web apps. Out of the box, you get:</p>
|
<p>Sapper is a Next.js-style framework (<a href='blog/how-is-sapper-different-from-next'>more on that here</a>) built around Svelte. It makes it embarrassingly easy to create extremely high performance web apps. Out of the box, you get:</p>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>Code-splitting, dynamic imports and hot module replacement, powered by webpack</li>
|
<li>Code-splitting, dynamic imports and hot module replacement, powered by webpack</li>
|
||||||
@@ -70,8 +70,8 @@ const posts = [
|
|||||||
<ul>
|
<ul>
|
||||||
<li>It's powered by <a href='https://svelte.technology'>Svelte</a> instead of React, so it's faster and your apps are smaller</li>
|
<li>It's powered by <a href='https://svelte.technology'>Svelte</a> instead of React, so it's faster and your apps are smaller</li>
|
||||||
<li>Instead of route masking, we encode route parameters in filenames. For example, the page you're looking at right now is <code>routes/blog/[slug].html</code></li>
|
<li>Instead of route masking, we encode route parameters in filenames. For example, the page you're looking at right now is <code>routes/blog/[slug].html</code></li>
|
||||||
<li>As well as pages (Svelte components, which render on server or client), you can create <em>server routes</em> in your <code>routes</code> directory. These are just <code>.js</code> files that export functions corresponding to HTTP methods, and receive Express <code>request</code> and <code>response</code> objects as arguments. This makes it very easy to, for example, add a JSON API such as the one powering this very page (look in <code>routes/api/blog</code>)</li>
|
<li>As well as pages (Svelte components, which render on server or client), you can create <em>server routes</em> in your <code>routes</code> directory. These are just <code>.js</code> files that export functions corresponding to HTTP methods, and receive Express <code>request</code> and <code>response</code> objects as arguments. This makes it very easy to, for example, add a JSON API such as the one <a href='blog/how-is-sapper-different-from-next.json'>powering this very page</a></li>
|
||||||
<li>Links are just <code><a></code> elements, rather than framework-specific <code><Link></code> components. That means, for example, that <a href='/blog/how-can-i-get-involved'>this link right here</a>, despite being inside a blob of HTML, works with the router as you'd expect.</li>
|
<li>Links are just <code><a></code> elements, rather than framework-specific <code><Link></code> components. That means, for example, that <a href='blog/how-can-i-get-involved'>this link right here</a>, despite being inside a blob of HTML, works with the router as you'd expect.</li>
|
||||||
</ul>
|
</ul>
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
@@ -82,6 +82,38 @@ const posts = [
|
|||||||
html: `
|
html: `
|
||||||
<p>We're so glad you asked! Come on over to the <a href='https://github.com/sveltejs/svelte'>Svelte</a> and <a href='https://github.com/sveltejs/sapper'>Sapper</a> repos, and join us in the <a href='https://gitter.im/sveltejs/svelte'>Gitter chatroom</a>. Everyone is welcome, especially you!</p>
|
<p>We're so glad you asked! Come on over to the <a href='https://github.com/sveltejs/svelte'>Svelte</a> and <a href='https://github.com/sveltejs/sapper'>Sapper</a> repos, and join us in the <a href='https://gitter.im/sveltejs/svelte'>Gitter chatroom</a>. Everyone is welcome, especially you!</p>
|
||||||
`
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: 'A very long post with deep links',
|
||||||
|
slug: 'a-very-long-post',
|
||||||
|
html: `
|
||||||
|
<h2 id='one'>One</h2>
|
||||||
|
<p>I'll have a vodka rocks. (Mom, it's breakfast time.) And a piece of toast. Let me out that Queen. Fried cheese… with club sauce.</p>
|
||||||
|
<p>Her lawyers are claiming the seal is worth $250,000. And that's not even including Buster's Swatch. This was a big get for God. What, so the guy we are meeting with can't even grow his own hair? COME ON! She's always got to wedge herself in the middle of us so that she can control everything. Yeah. Mom's awesome. It's, like, Hey, you want to go down to the whirlpool? Yeah, I don't have a husband. I call it Swing City. The CIA should've just Googled for his hideout, evidently. There are dozens of us! DOZENS! Yeah, like I'm going to take a whiz through this $5,000 suit. COME ON.</p>
|
||||||
|
|
||||||
|
<h2 id='two'>Two</h2>
|
||||||
|
<p>Tobias Fünke costume. Heart attack never stopped old big bear.</p>
|
||||||
|
<p>Nellie is blowing them all AWAY. I will be a bigger and hairier mole than the one on your inner left thigh! I'll sacrifice anything for my children.</p>
|
||||||
|
<p>Up yours, granny! You couldn't handle it! Hey, Dad. Look at you. You're a year older…and a year closer to death. Buster: Oh yeah, I guess that's kind of funny. Bob Loblaw Law Blog. The guy runs a prison, he can have any piece of ass he wants.</p>
|
||||||
|
|
||||||
|
<h2 id='three'>Three</h2>
|
||||||
|
<p>I prematurely shot my wad on what was supposed to be a dry run, so now I'm afraid I have something of a mess on my hands. Dead Dove DO NOT EAT. Never once touched my per diem. I'd go to Craft Service, get some raw veggies, bacon, Cup-A-Soup…baby, I got a stew goin'. You're losing blood, aren't you? Gob: Probably, my socks are wet. Sure, let the little fruit do it. HUZZAH! Although George Michael had only got to second base, he'd gone in head first, like Pete Rose. I will pack your sweet pink mouth with so much ice cream you'll be the envy of every Jerry and Jane on the block!</p>
|
||||||
|
<p>Gosh Mom… after all these years, God's not going to take a call from you. Come on, this is a Bluth family celebration. It's no place for children.</p>
|
||||||
|
<p>And I wouldn't just lie there, if that's what you're thinking. That's not what I WAS thinking. Who? i just dont want him to point out my cracker ass in front of ann. When a man needs to prove to a woman that he's actually… When a man loves a woman… Heyyyyyy Uncle Father Oscar. [Stabbing Gob] White power! Gob: I'm white! Let me take off my assistant's skirt and put on my Barbra-Streisand-in-The-Prince-of-Tides ass-masking therapist pantsuit. In the mid '90s, Tobias formed a folk music band with Lindsay and Maebe which he called Dr. Funke's 100 Percent Natural Good Time Family Band Solution. The group was underwritten by the Natural Food Life Company, a division of Chem-Grow, an Allen Crayne acqusition, which was part of the Squimm Group. Their motto was simple: We keep you alive.</p>
|
||||||
|
|
||||||
|
<h2 id='four'>Four</h2>
|
||||||
|
<p>If you didn't have adult onset diabetes, I wouldn't mind giving you a little sugar. Everybody dance NOW. And the soup of the day is bread. Great, now I'm gonna smell to high heaven like a tuna melt!</p>
|
||||||
|
<p>That's how Tony Wonder lost a nut. She calls it a Mayonegg. Go ahead, touch the Cornballer. There's a new daddy in town. A discipline daddy.</p>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: 'Encödïng test',
|
||||||
|
slug: 'encödïng-test',
|
||||||
|
html: `
|
||||||
|
<p>It works</p>
|
||||||
|
`
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -1,38 +1,23 @@
|
|||||||
<:Head>
|
<svelte:head>
|
||||||
<title>Blog</title>
|
<title>Blog</title>
|
||||||
</:Head>
|
</svelte:head>
|
||||||
|
|
||||||
<Layout page='blog'>
|
<h1>Recent posts</h1>
|
||||||
<h1>Recent posts</h1>
|
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{{#each posts as post}}
|
{#each posts as post}
|
||||||
<!-- we're using the non-standard `rel=prefetch` attribute to
|
<!-- we're using the non-standard `rel=prefetch` attribute to
|
||||||
tell Sapper to load the data for the page as soon as
|
tell Sapper to load the data for the page as soon as
|
||||||
the user hovers over the link or taps it, instead of
|
the user hovers over the link or taps it, instead of
|
||||||
waiting for the 'click' event -->
|
waiting for the 'click' event -->
|
||||||
<li><a rel='prefetch' href='/blog/{{post.slug}}'>{{post.title}}</a></li>
|
<li><a rel='prefetch' href='blog/{post.slug}'>{post.title}</a></li>
|
||||||
{{/each}}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
ul {
|
|
||||||
margin: 0 0 1em 0;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Layout from '../_components/Layout.html';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
|
||||||
Layout
|
|
||||||
},
|
|
||||||
|
|
||||||
preload({ params, query }) {
|
preload({ params, query }) {
|
||||||
return fetch(`/api/blog`).then(r => r.json()).then(posts => {
|
return fetch(`blog.json`).then(r => r.json()).then(posts => {
|
||||||
return { posts };
|
return { posts };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const contents = JSON.stringify(posts.map(post => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export function get(req, res) {
|
export function get(req, res) {
|
||||||
res.set({
|
res.writeHead(200, {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Cache-Control': `max-age=${30 * 60 * 1e3}` // cache for 30 minutes
|
'Cache-Control': `max-age=${30 * 60 * 1e3}` // cache for 30 minutes
|
||||||
});
|
});
|
||||||
1
test/app/routes/const.html
Normal file
1
test/app/routes/const.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<h1>reserved words are okay as routes</h1>
|
||||||
11
test/app/routes/credentials/index.html
Normal file
11
test/app/routes/credentials/index.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<h1>{message}</h1>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
preload({ query }) {
|
||||||
|
return this.fetch(`credentials/test.json`, {
|
||||||
|
credentials: query.creds
|
||||||
|
}).then(r => r.json());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
28
test/app/routes/credentials/test.json.js
Normal file
28
test/app/routes/credentials/test.json.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
export function get(req, res) {
|
||||||
|
const cookies = req.headers.cookie
|
||||||
|
? req.headers.cookie.split(/,\s+/).reduce((cookies, cookie) => {
|
||||||
|
const [pair] = cookie.split('; ');
|
||||||
|
const [name, value] = pair.split('=');
|
||||||
|
cookies[name] = value;
|
||||||
|
return cookies;
|
||||||
|
}, {})
|
||||||
|
: {};
|
||||||
|
|
||||||
|
if (cookies.test) {
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
});
|
||||||
|
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
message: cookies.test
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
res.writeHead(403, {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
});
|
||||||
|
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
message: 'unauthorized'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
15
test/app/routes/delete-test.html
Normal file
15
test/app/routes/delete-test.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<button class='del' on:click='del()'>delete</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
del() {
|
||||||
|
fetch(`api/delete/42`, { method: 'DELETE' })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
window.deleted = data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
12
test/app/routes/echo/page/[slug].html
Normal file
12
test/app/routes/echo/page/[slug].html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<h1>{slug} ({message})</h1>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
preload({ params, query }) {
|
||||||
|
return {
|
||||||
|
slug: params.slug,
|
||||||
|
message: query.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
15
test/app/routes/echo/server-route/[slug].js
Normal file
15
test/app/routes/echo/server-route/[slug].js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export function get(req, res) {
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'text/html'
|
||||||
|
});
|
||||||
|
|
||||||
|
res.end(`
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head><meta charset="utf-8"></head>
|
||||||
|
<body>
|
||||||
|
<h1>${req.params.slug}</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
}
|
||||||
11
test/app/routes/fünke.html
Normal file
11
test/app/routes/fünke.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<h1>{phrase}</h1>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
preload() {
|
||||||
|
return this.fetch('fünke.json').then(r => r.json()).then(phrase => {
|
||||||
|
return { phrase };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
9
test/app/routes/fünke.json.js
Normal file
9
test/app/routes/fünke.json.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export function get(req, res) {
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
});
|
||||||
|
|
||||||
|
res.end(JSON.stringify(
|
||||||
|
"I'm afraid I just blue myself"
|
||||||
|
));
|
||||||
|
}
|
||||||
@@ -1,58 +1,31 @@
|
|||||||
<:Head>
|
<svelte:head>
|
||||||
<title>Sapper project template</title>
|
<title>Sapper project template</title>
|
||||||
</:Head>
|
</svelte:head>
|
||||||
|
|
||||||
<Layout page='home'>
|
<h1>Great success!</h1>
|
||||||
<h1>Great success!</h1>
|
|
||||||
|
|
||||||
<figure>
|
<a href='.'>home</a>
|
||||||
<img src='/great-success.png'>
|
<a href='about'>about</a>
|
||||||
<figcaption>HIGH FIVE!</figcaption>
|
<a href='slow-preload'>slow preload</a>
|
||||||
</figure>
|
<a href='non-sapper-redirect-from'>redirect</a>
|
||||||
|
<a href='redirect-from'>redirect</a>
|
||||||
|
<a href='redirect-root'>redirect (root)</a>
|
||||||
|
<a href='blog/nope'>broken link</a>
|
||||||
|
<a href='blog/throw-an-error'>error link</a>
|
||||||
|
<a href='credentials?creds=include'>credentials</a>
|
||||||
|
<a rel=prefetch class='{page === "blog" ? "selected" : ""}' href='blog'>blog</a>
|
||||||
|
<a href="const">const</a>
|
||||||
|
<a href="echo/page/encöded?message=hëllö+wörld">echo/page/encöded?message=hëllö+wörld</a>
|
||||||
|
<a href="echo/page/empty?message">echo/page/empty?message</a>
|
||||||
|
|
||||||
<p><strong>Try editing this file (routes/index.html) to test hot module reloading.</strong></p>
|
<div class='hydrate-test'></div>
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
h1, figure, p {
|
|
||||||
text-align: center;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
text-align: center;
|
||||||
font-size: 2.8em;
|
font-size: 2.8em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin: 0 0 0.5em 0;
|
margin: 0 0 0.5em 0;
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
figure {
|
|
||||||
margin: 0 0 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 400px;
|
|
||||||
margin: 0 0 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 1em auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 480px) {
|
|
||||||
h1 {
|
|
||||||
font-size: 4em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Layout from './_components/Layout.html';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Layout
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
1
test/app/routes/missing-index/ok.html
Normal file
1
test/app/routes/missing-index/ok.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<h1>it works</h1>
|
||||||
1
test/app/routes/non-sapper-redirect-to.html
Normal file
1
test/app/routes/non-sapper-redirect-to.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<h1>redirected</h1>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user