mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-13 19:45:26 +00:00
Compare commits
748 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
664c093391 | ||
|
|
4375feac83 | ||
|
|
4d7d448597 | ||
|
|
2e2b8dcd83 | ||
|
|
b915bab070 | ||
|
|
8530d06d00 | ||
|
|
a43764a971 | ||
|
|
4f6efbda79 | ||
|
|
5573258a10 | ||
|
|
2185f89669 | ||
|
|
e30842caa8 | ||
|
|
ff24877d8f | ||
|
|
9cf90ce01d | ||
|
|
e7f9ddae86 | ||
|
|
ffa1e1f704 | ||
|
|
80bb958b47 | ||
|
|
532f559fc5 | ||
|
|
0bd1b0b8e2 | ||
|
|
10c5ff4169 | ||
|
|
273823dfd7 | ||
|
|
8f064fe5ac | ||
|
|
f29e7efbd6 | ||
|
|
e66e3cd7eb | ||
|
|
ff415b391b | ||
|
|
91182ad0a2 | ||
|
|
467041a3cd | ||
|
|
520949c5e1 | ||
|
|
8c07d9d2ac | ||
|
|
7bd684a80e | ||
|
|
cbb5e8755b | ||
|
|
7ef72dbb77 | ||
|
|
87ff9c2aeb | ||
|
|
2d1f535314 | ||
|
|
cd1b53b80d | ||
|
|
0a7be736c0 | ||
|
|
5ee53a98c6 | ||
|
|
0e8ed6612c | ||
|
|
5ec748b95d | ||
|
|
64b16715cd | ||
|
|
9ea5e5e251 | ||
|
|
68b78f56d6 | ||
|
|
68e93a8fa0 | ||
|
|
e377515867 | ||
|
|
99ae39b8a8 | ||
|
|
1b489f4687 | ||
|
|
91f2c6e49c | ||
|
|
f5e07e9f78 | ||
|
|
17297a9794 | ||
|
|
9ef4f33e38 | ||
|
|
30966ee7f2 | ||
|
|
ae90f774e1 | ||
|
|
0706b5f50a | ||
|
|
499b377bfd | ||
|
|
1baeb79d4b | ||
|
|
0cc5ff95d6 | ||
|
|
e90525c1e8 | ||
|
|
6ccae0cd33 | ||
|
|
8b60d568dc | ||
|
|
64c2394c9d | ||
|
|
b28037291a | ||
|
|
bf9cbe2f3b | ||
|
|
2c507b5a2e | ||
|
|
4a92fbbbfa | ||
|
|
b16440ff0f | ||
|
|
64223b572b | ||
|
|
1b6dfd3580 | ||
|
|
c0b833862a | ||
|
|
45f4c47a3e | ||
|
|
48b87edb5b | ||
|
|
f9f283603e | ||
|
|
a56ee6bdb7 | ||
|
|
a18af2a473 | ||
|
|
fe5a8fb1e7 | ||
|
|
57a26e3511 | ||
|
|
bebb0dd595 | ||
|
|
afba0491ed | ||
|
|
350d37e210 | ||
|
|
96fc19e939 | ||
|
|
5be3809d9e | ||
|
|
15cc4bf296 | ||
|
|
c7cce985e3 | ||
|
|
e00b315dec | ||
|
|
afcd643035 | ||
|
|
7cc2a03aae | ||
|
|
002718b609 | ||
|
|
45d216c64d | ||
|
|
3d69d483d7 | ||
|
|
54da524467 | ||
|
|
ee95240ca6 | ||
|
|
74d5d1f9c0 | ||
|
|
8c2688b1be | ||
|
|
e170e4af9b | ||
|
|
bc31c73c33 | ||
|
|
7798f8f684 | ||
|
|
70fd7038b0 | ||
|
|
c6af2ddfa3 | ||
|
|
65d0172abe | ||
|
|
1e22031765 | ||
|
|
46bf8f2b78 | ||
|
|
553db81b7b | ||
|
|
67cc29ed38 | ||
|
|
36f930f489 | ||
|
|
3b098caa6e | ||
|
|
d63b9437b5 | ||
|
|
e51c733e3f | ||
|
|
708fe4c74b | ||
|
|
4259fc8e58 | ||
|
|
f05a8e52a0 | ||
|
|
76cb6d97f3 | ||
|
|
5d0b7af47b | ||
|
|
bb737eeb32 | ||
|
|
86dee17040 | ||
|
|
01a709e017 | ||
|
|
f87f0e3b80 | ||
|
|
8226e9bc1f | ||
|
|
d6d0a15015 | ||
|
|
ddec58ebd4 | ||
|
|
9d904b3911 | ||
|
|
c36df0d650 | ||
|
|
ae19288797 | ||
|
|
de308d5bb0 | ||
|
|
99b096a5c4 | ||
|
|
36fc8a947b | ||
|
|
6393a30b13 | ||
|
|
458be49b35 | ||
|
|
f8d742bdd0 | ||
|
|
7e698f1613 | ||
|
|
70b5cc86dc | ||
|
|
19a5dcad1d | ||
|
|
85e25d6380 | ||
|
|
6e2383b66b | ||
|
|
200c5fcbd2 | ||
|
|
9cbb8bdc33 | ||
|
|
3d39836cfb | ||
|
|
24f2855f89 | ||
|
|
d5bf206d2a | ||
|
|
8abc01551e | ||
|
|
62b8a79e9f | ||
|
|
7f255563a4 | ||
|
|
32f4a50f25 | ||
|
|
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 | ||
|
|
51d45cf38f | ||
|
|
92fbc28e11 | ||
|
|
c1b1b3ed63 | ||
|
|
cd8b9ddb14 | ||
|
|
924855d248 | ||
|
|
8b516ef9bd | ||
|
|
8604088f3d | ||
|
|
8da3ca16ab | ||
|
|
d0dd1d6cc9 | ||
|
|
8270463281 | ||
|
|
514331b5e3 | ||
|
|
63d39575be | ||
|
|
3af5503009 | ||
|
|
c1de442dd1 | ||
|
|
0c6b7e3836 | ||
|
|
dc5e2543cb | ||
|
|
ecc7b80d91 | ||
|
|
40024e7d86 | ||
|
|
6f71f7ad4d | ||
|
|
6eb99b195e | ||
|
|
9e08fee9a1 | ||
|
|
442ce366e2 | ||
|
|
dc929fcd83 | ||
|
|
2dc246398b | ||
|
|
b7ac067459 | ||
|
|
8b50ff34b8 | ||
|
|
62abdb2a87 | ||
|
|
34d0bae4a1 | ||
|
|
4f0b336627 | ||
|
|
e71bf298fb | ||
|
|
e4936375db | ||
|
|
08ff7ad234 | ||
|
|
5995b7ae6a | ||
|
|
71ed3864b7 | ||
|
|
bd7f6e2b1a | ||
|
|
dd1f2d79ff | ||
|
|
dccd3cdeb0 | ||
|
|
b3b5d9f352 | ||
|
|
10ddaeb7a3 | ||
|
|
060f9b2f5e | ||
|
|
32dfa94247 | ||
|
|
797cc3cde1 | ||
|
|
9eca90067c | ||
|
|
57f293e872 | ||
|
|
7e65c481d8 | ||
|
|
0fe93cd177 | ||
|
|
67fe570f6d | ||
|
|
a3d44aba31 | ||
|
|
80ae909b73 | ||
|
|
892b18cf80 | ||
|
|
0eb96bf01f | ||
|
|
419f5c5235 | ||
|
|
4c61ed5fdd | ||
|
|
c19447cf05 | ||
|
|
cb2364f476 | ||
|
|
de427d400e | ||
|
|
e810ead93f | ||
|
|
f5a19ef34b | ||
|
|
b8c03d330b | ||
|
|
6e769496ec | ||
|
|
e46aceb2fe | ||
|
|
a87cac2481 | ||
|
|
608fdb7533 | ||
|
|
80166b5a7d | ||
|
|
24b259f80b | ||
|
|
8a9f4bd268 | ||
|
|
d940da1a77 | ||
|
|
91269f5705 | ||
|
|
80a9818e95 |
14
.gitignore
vendored
14
.gitignore
vendored
@@ -1,2 +1,14 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
yarn.lock
|
||||
yarn-error.log
|
||||
node_modules
|
||||
cypress/screenshots
|
||||
test/app/.sapper
|
||||
test/app/src/manifest
|
||||
__sapper__
|
||||
test/app/export
|
||||
test/app/build
|
||||
sapper
|
||||
runtime.js
|
||||
dist
|
||||
!rollup.config.js
|
||||
21
.travis.yml
Normal file
21
.travis.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
sudo: false
|
||||
|
||||
language: node_js
|
||||
|
||||
node_js:
|
||||
- "6"
|
||||
- "stable"
|
||||
|
||||
env:
|
||||
global:
|
||||
- BUILD_TIMEOUT=10000
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- xvfb
|
||||
|
||||
install:
|
||||
- export DISPLAY=':99.0'
|
||||
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||
- npm ci || npm i
|
||||
490
CHANGELOG.md
Normal file
490
CHANGELOG.md
Normal file
@@ -0,0 +1,490 @@
|
||||
# sapper changelog
|
||||
|
||||
## 0.22.1
|
||||
|
||||
* Fix `pkg.files`
|
||||
|
||||
## 0.22.0
|
||||
|
||||
* Move generated files into `__sapper__` ([#453](https://github.com/sveltejs/sapper/pull/453))
|
||||
* Change default build and export directories to `__sapper__/build` and `__sapper__/export` ([#453](https://github.com/sveltejs/sapper/pull/453))
|
||||
|
||||
## 0.21.1
|
||||
|
||||
* Read template from build directory in production
|
||||
|
||||
## 0.21.0
|
||||
|
||||
* Change project folder structure ([#432](https://github.com/sveltejs/sapper/issues/432))
|
||||
* Escape filenames ([#446](https://github.com/sveltejs/sapper/pull/446/))
|
||||
|
||||
## 0.20.4
|
||||
|
||||
* Fix legacy build CSS ([#439](https://github.com/sveltejs/sapper/issues/439))
|
||||
* Enable debugging in Chrome and VSCode ([#435](https://github.com/sveltejs/sapper/issues/435))
|
||||
|
||||
## 0.20.3
|
||||
|
||||
* Inject `nonce` attribute if `res.locals.nonce` is present ([#424](https://github.com/sveltejs/sapper/pull/424))
|
||||
* Prevent service worker caching ([#428](https://github.com/sveltejs/sapper/pull/428))
|
||||
* Consistent caching for HTML responses ([#429](https://github.com/sveltejs/sapper/pull/429))
|
||||
|
||||
## 0.20.2
|
||||
|
||||
* Add `immutable` cache control header for hashed assets ([#425](https://github.com/sveltejs/sapper/pull/425))
|
||||
* Handle value-less query string params ([#426](https://github.com/sveltejs/sapper/issues/426))
|
||||
|
||||
## 0.20.1
|
||||
|
||||
* Update shimport
|
||||
|
||||
## 0.20.0
|
||||
|
||||
* Decode `req.params` and `req.query` ([#417](https://github.com/sveltejs/sapper/issues/417))
|
||||
* Decode URLs before writing files in `sapper export` ([#414](https://github.com/sveltejs/sapper/pull/414))
|
||||
* Generate server sourcemaps for Rollup apps in dev mode ([#418](https://github.com/sveltejs/sapper/pull/418))
|
||||
|
||||
## 0.19.3
|
||||
|
||||
* Better unicode route handling ([#347](https://github.com/sveltejs/sapper/issues/347))
|
||||
|
||||
## 0.19.2
|
||||
|
||||
* Ignore editor tmp files ([#220](https://github.com/sveltejs/sapper/issues/220))
|
||||
* Ignore clicks an `<a>` element without `href` ([#235](https://github.com/sveltejs/sapper/issues/235))
|
||||
* Allow routes that are reserved JavaScript words ([#315](https://github.com/sveltejs/sapper/issues/315))
|
||||
* Print out webpack errors ([#403](https://github.com/sveltejs/sapper/issues/403))
|
||||
|
||||
## 0.19.1
|
||||
|
||||
* Don't include local origin in export redirects ([#409](https://github.com/sveltejs/sapper/pull/409))
|
||||
|
||||
## 0.19.0
|
||||
|
||||
* Extract styles out of JS into .css files, for Rollup apps ([#388](https://github.com/sveltejs/sapper/issues/388))
|
||||
* Fix `prefetchRoutes` ([#380](https://github.com/sveltejs/sapper/issues/380))
|
||||
|
||||
## 0.18.7
|
||||
|
||||
* Support differential bundling for Rollup apps via a `--legacy` flag ([#280](https://github.com/sveltejs/sapper/issues/280))
|
||||
|
||||
## 0.18.6
|
||||
|
||||
* Bundle missing dependency
|
||||
|
||||
## 0.18.5
|
||||
|
||||
* Bugfix
|
||||
|
||||
## 0.18.4
|
||||
|
||||
* Handle non-Sapper responses when exporting ([#382](https://github.com/sveltejs/sapper/issues/392))
|
||||
* Add `--dev-port` flag to `sapper dev` ([#381](https://github.com/sveltejs/sapper/issues/381))
|
||||
|
||||
## 0.18.3
|
||||
|
||||
* Fix service worker Rollup build config
|
||||
|
||||
## 0.18.2
|
||||
|
||||
* Update `pkg.files`
|
||||
|
||||
## 0.18.1
|
||||
|
||||
* Add live reloading ([#385](https://github.com/sveltejs/sapper/issues/385))
|
||||
|
||||
## 0.18.0
|
||||
|
||||
* Rollup support ([#379](https://github.com/sveltejs/sapper/pull/379))
|
||||
* Fail `export` if a page times out (configurable with `--timeout`) ([#378](https://github.com/sveltejs/sapper/pull/378))
|
||||
|
||||
## 0.17.1
|
||||
|
||||
* Print which file is causing build errors/warnings ([#371](https://github.com/sveltejs/sapper/pull/371))
|
||||
|
||||
## 0.17.0
|
||||
|
||||
* Use `cheap-watch` instead of `chokidar` ([#364](https://github.com/sveltejs/sapper/issues/364))
|
||||
|
||||
## 0.16.1
|
||||
|
||||
* Fix file watching regression in previous version
|
||||
|
||||
## 0.16.0
|
||||
|
||||
* Slim down installed package ([#363](https://github.com/sveltejs/sapper/pull/363))
|
||||
|
||||
## 0.15.8
|
||||
|
||||
* Only set `preloading: true` on navigation, not prefetch ([#352](https://github.com/sveltejs/sapper/issues/352))
|
||||
* Provide fallback for missing preload errors ([#361](https://github.com/sveltejs/sapper/pull/361))
|
||||
|
||||
## 0.15.7
|
||||
|
||||
* Strip leading slash from redirects ([#291](https://github.com/sveltejs/sapper/issues/291))
|
||||
* Pass `(req, res)` to store getter ([#344](https://github.com/sveltejs/sapper/issues/344))
|
||||
|
||||
## 0.15.6
|
||||
|
||||
* Fix exporting with custom basepath ([#342](https://github.com/sveltejs/sapper/pull/342))
|
||||
|
||||
## 0.15.5
|
||||
|
||||
* Faster `export` with more explanatory output ([#335](https://github.com/sveltejs/sapper/pull/335))
|
||||
* Only blur `activeElement` if it exists ([#332](https://github.com/sveltejs/sapper/issues/332))
|
||||
* Don't emit `client_info.json` or `server_info.json` ([#318](https://github.com/sveltejs/sapper/issues/318))
|
||||
|
||||
## 0.15.4
|
||||
|
||||
* Add `ignore` option ([#326](https://github.com/sveltejs/sapper/pull/326))
|
||||
|
||||
## 0.15.3
|
||||
|
||||
* Crawl pages in parallel when exporting ([#329](https://github.com/sveltejs/sapper/pull/329))
|
||||
* Don't minify inline JS when exporting ([#328](https://github.com/sveltejs/sapper/pull/328))
|
||||
|
||||
## 0.15.2
|
||||
|
||||
* Collapse component chains where no intermediate layout component is specified ([#312](https://github.com/sveltejs/sapper/issues/312))
|
||||
|
||||
## 0.15.1
|
||||
|
||||
* Prevent confusing error when no root layout is specified
|
||||
|
||||
## 0.15.0
|
||||
|
||||
* Nested routes (consult [migration guide](https://sapper.svelte.technology/guide#0-14-to-0-15) and docs on [layouts](https://sapper.svelte.technology/guide#layouts)) ([#262](https://github.com/sveltejs/sapper/issues/262))
|
||||
|
||||
## 0.14.2
|
||||
|
||||
* Prevent unsafe replacements ([#307](https://github.com/sveltejs/sapper/pull/307))
|
||||
|
||||
## 0.14.1
|
||||
|
||||
* Route parameters can be qualified with regex characters ([#283](https://github.com/sveltejs/sapper/pull/283))
|
||||
|
||||
## 0.14.0
|
||||
|
||||
* `4xx.html` and `5xx.html` are replaced with `_error.html` ([#209](https://github.com/sveltejs/sapper/issues/209))
|
||||
* Treat `foo/index.json.js` and `foo.json.js` as equivalents ([#297](https://github.com/sveltejs/sapper/issues/297))
|
||||
* Return a promise from `goto` ([#270](https://github.com/sveltejs/sapper/issues/270))
|
||||
* Use store when rendering error pages ([#293](https://github.com/sveltejs/sapper/issues/293))
|
||||
* Prevent console errors when visiting an error page ([#279](https://github.com/sveltejs/sapper/issues/279))
|
||||
|
||||
## 0.13.6
|
||||
|
||||
* Fix `baseUrl` synthesis ([#296](https://github.com/sveltejs/sapper/issues/296))
|
||||
|
||||
## 0.13.5
|
||||
|
||||
* Fix handling of fatal errors ([#289](https://github.com/sveltejs/sapper/issues/289))
|
||||
|
||||
## 0.13.4
|
||||
|
||||
* Focus `<body>` after navigation ([#287](https://github.com/sveltejs/sapper/issues/287))
|
||||
* Fix timing of hot reload updates
|
||||
* Emit `fatal` event if server crashes ([#285](https://github.com/sveltejs/sapper/pull/285))
|
||||
* Emit `stdout` and `stderr` events on dev watcher ([#285](https://github.com/sveltejs/sapper/pull/285))
|
||||
* Always refresh client assets in dev ([#286](https://github.com/sveltejs/sapper/pull/286))
|
||||
* Correctly initialise rebuild stats
|
||||
|
||||
## 0.13.3
|
||||
|
||||
* Make `fatal` events clonable for IPC purposes
|
||||
|
||||
## 0.13.2
|
||||
|
||||
* Emit a `basepath` event ([#284](https://github.com/sveltejs/sapper/pull/284))
|
||||
|
||||
## 0.13.1
|
||||
|
||||
* Reinstate ten-second interval between dev server heartbeats ([#276](https://github.com/sveltejs/sapper/issues/276))
|
||||
|
||||
## 0.13.0
|
||||
|
||||
* Expose `dev`, `build`, `export` and `find_page` APIs ([#272](https://github.com/sveltejs/sapper/issues/272))
|
||||
|
||||
## 0.12.0
|
||||
|
||||
* Each app has a single `<App>` component. See the [migration guide](https://sapper.svelte.technology/guide#0-11-to-0-12) for more information ([#157](https://github.com/sveltejs/sapper/issues/157))
|
||||
* Process exits with error code 1 if build/export fails ([#208](https://github.com/sveltejs/sapper/issues/208))
|
||||
|
||||
## 0.11.1
|
||||
|
||||
* Limit routes with leading dots to `.well-known` URIs ([#252](https://github.com/sveltejs/sapper/issues/252))
|
||||
* Allow server routes to sit in front of pages ([#236](https://github.com/sveltejs/sapper/pull/236))
|
||||
|
||||
## 0.11.0
|
||||
|
||||
* Create launcher file ([#240](https://github.com/sveltejs/sapper/issues/240))
|
||||
* Only keep necessary parts of webpack stats ([#251](https://github.com/sveltejs/sapper/pull/251))
|
||||
* Allow `NODE_ENV` to be overridden when building ([#241](https://github.com/sveltejs/sapper/issues/241))
|
||||
|
||||
## 0.10.7
|
||||
|
||||
* Allow routes to have a leading `.` ([#243](https://github.com/sveltejs/sapper/pull/243))
|
||||
* Only encode necessary characters in routes ([#234](https://github.com/sveltejs/sapper/pull/234))
|
||||
* Preserve existing `process.env` when exporting ([#245](https://github.com/sveltejs/sapper/pull/245))
|
||||
|
||||
## 0.10.6
|
||||
|
||||
* Fix error reporting in `sapper start`
|
||||
|
||||
## 0.10.5
|
||||
|
||||
* Fix missing service worker ([#231](https://github.com/sveltejs/sapper/pull/231))
|
||||
|
||||
## 0.10.4
|
||||
|
||||
* Upgrade chokidar, this time with a fix ([#227](https://github.com/sveltejs/sapper/pull/227))
|
||||
|
||||
## 0.10.3
|
||||
|
||||
* Downgrade chokidar ([#212](https://github.com/sveltejs/sapper/issues/212))
|
||||
|
||||
## 0.10.2
|
||||
|
||||
* Attach `store` to error pages
|
||||
* Fix sorting edge case ([#215](https://github.com/sveltejs/sapper/pull/215))
|
||||
|
||||
## 0.10.1
|
||||
|
||||
* Fix server-side `fetch` paths ([#207](https://github.com/sveltejs/sapper/pull/207))
|
||||
|
||||
## 0.10.0
|
||||
|
||||
* Support mounting on a path (this requires `app/template.html` to include `%sapper.base%`) ([#180](https://github.com/sveltejs/sapper/issues/180))
|
||||
* Support per-request server-side `Store` with client-side hydration ([#178](https://github.com/sveltejs/sapper/issues/178))
|
||||
* Add `this.fetch` to `preload`, with credentials support ([#178](https://github.com/sveltejs/sapper/issues/178))
|
||||
* Exclude sourcemaps from preload links and `<script>` block ([#204](https://github.com/sveltejs/sapper/pull/204))
|
||||
* Register service worker in `<script>` block
|
||||
|
||||
|
||||
## 0.9.6
|
||||
|
||||
* 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
|
||||
|
||||
* Create destination directory when building, don't assume it's already there from dev mode
|
||||
* We have tests now!
|
||||
|
||||
## 0.2.1
|
||||
|
||||
* Inject HMR logic in dev mode
|
||||
|
||||
## 0.2.0
|
||||
|
||||
* Separate `sapper build` from prod server ([#21](https://github.com/sveltejs/sapper/issues/21))
|
||||
|
||||
## 0.1.3-5
|
||||
|
||||
* Fix typo
|
||||
|
||||
## 0.1.2
|
||||
|
||||
* Use `atime.getTime()` and `mtime.getTime()` instead of `atimeMs` and `mtimeMs` ([#11](https://github.com/sveltejs/sapper/issues/11))
|
||||
* Make dest dir before anyone tries to write to it ([#18](https://github.com/sveltejs/sapper/pull/18))
|
||||
|
||||
## 0.1.1
|
||||
|
||||
* Expose resolved pathname to `sapper/runtime/app.js` as `__app__` inside main.js
|
||||
|
||||
## 0.1.0
|
||||
|
||||
* First public preview
|
||||
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
|
||||
|
||||
Combat-ready apps, engineered by Svelte.
|
||||
|
||||
## 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
|
||||
[Military-grade progressive web apps, powered by Svelte.](https://sapper.svelte.technology)
|
||||
|
||||
|
||||
## 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;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`listening on port ${PORT}`);
|
||||
});
|
||||
Clone the [starter project template](https://github.com/sveltejs/sapper-template) with [degit](https://github.com/rich-harris/degit)...
|
||||
|
||||
```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...
|
||||
|
||||
|
||||
## Routing
|
||||
|
||||
Like Next, routes are defined by the project directory structure, but with some crucial differences:
|
||||
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
```bash
|
||||
cd my-app
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
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
|
||||
// routes/api/post/[id].js
|
||||
export async function get(req) {
|
||||
return await getPostFromDatabase(req.params.id);
|
||||
}
|
||||
```bash
|
||||
npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
## 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
|
||||
window.addEventListener('click', event => {
|
||||
let a = event.target;
|
||||
while (a && a.nodeName !== 'A') a = a.parentNode;
|
||||
if (!a) return;
|
||||
|
||||
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);
|
||||
```bash
|
||||
git clone git@github.com:sveltejs/sapper.git
|
||||
cd sapper
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
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
|
||||
* An equivalent of `getInitialProps`
|
||||
* Critical CSS
|
||||
* `store` integration
|
||||
* Route transitions
|
||||
* Equivalent of `next export`
|
||||
* A good story for realtime/GraphQL stuff
|
||||
* Service worker
|
||||
* Using `Link...rel=preload` headers to push main.js/[route].js plus styles
|
||||
* ...and lots of other things that haven't occurred to me yet.
|
||||
```bash
|
||||
cd sapper
|
||||
npm link
|
||||
```
|
||||
|
||||
Then, to link from `sapper-template` (or any other given project):
|
||||
|
||||
```bash
|
||||
cd sapper-template
|
||||
npm link sapper
|
||||
```
|
||||
|
||||
You should be good to test changes locally.
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[LIL](LICENSE)
|
||||
21
appveyor.yml
Normal file
21
appveyor.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
version: "{build}"
|
||||
|
||||
shallow_clone: true
|
||||
|
||||
init:
|
||||
- git config --global core.autocrlf false
|
||||
|
||||
build: off
|
||||
|
||||
environment:
|
||||
matrix:
|
||||
# node.js
|
||||
- nodejs_version: 10.5
|
||||
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version
|
||||
- npm ci
|
||||
|
||||
test_script:
|
||||
- node --version && npm --version
|
||||
- npm test
|
||||
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');
|
||||
111
connect.js
111
connect.js
@@ -1,111 +0,0 @@
|
||||
require('svelte/ssr/register');
|
||||
const esm = require('@std/esm');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const glob = require('glob');
|
||||
const rimraf = require('rimraf');
|
||||
const mkdirp = require('mkdirp');
|
||||
const create_routes = require('./utils/create_routes.js');
|
||||
const create_templates = require('./utils/create_templates.js');
|
||||
const create_app = require('./utils/create_app.js');
|
||||
const create_webpack_compiler = require('./utils/create_webpack_compiler.js');
|
||||
const { src, dest, dev } = require('./lib/config.js');
|
||||
|
||||
const esmRequire = esm(module, {
|
||||
esm: 'js'
|
||||
});
|
||||
|
||||
module.exports = function connect(opts) {
|
||||
mkdirp(dest);
|
||||
rimraf.sync(path.join(dest, '**/*'));
|
||||
|
||||
let routes = create_routes(
|
||||
glob.sync('**/*.+(html|js|mjs)', { cwd: src })
|
||||
);
|
||||
|
||||
create_app(src, dest, routes, opts);
|
||||
|
||||
const webpack_compiler = create_webpack_compiler(
|
||||
dest,
|
||||
routes,
|
||||
dev
|
||||
);
|
||||
|
||||
const templates = create_templates();
|
||||
|
||||
return async function(req, res, next) {
|
||||
const url = req.url.replace(/\?.+/, '');
|
||||
|
||||
if (url.startsWith('/client/')) {
|
||||
res.set({
|
||||
'Content-Type': 'application/javascript'
|
||||
});
|
||||
fs.createReadStream(`${dest}${url}`).pipe(res);
|
||||
return;
|
||||
}
|
||||
|
||||
// whatever happens, we're going to serve some HTML
|
||||
res.set({
|
||||
'Content-Type': 'text/html'
|
||||
});
|
||||
|
||||
try {
|
||||
for (const route of routes) {
|
||||
if (route.test(url)) {
|
||||
req.params = route.exec(url);
|
||||
|
||||
const chunk = await webpack_compiler.get_chunk(route.id);
|
||||
const mod = require(chunk);
|
||||
|
||||
if (route.type === 'page') {
|
||||
const main = await webpack_compiler.client_main;
|
||||
|
||||
let data = { params: req.params, query: req.query };
|
||||
if (mod.default.preload) data = Object.assign(data, await mod.default.preload(data));
|
||||
|
||||
const { html, head, css } = mod.default.render(data);
|
||||
|
||||
const page = templates.render(200, {
|
||||
main,
|
||||
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) {
|
||||
if (handler.length === 2) {
|
||||
handler(req, res);
|
||||
} else {
|
||||
const data = await handler(req);
|
||||
|
||||
// TODO headers, error handling
|
||||
if (typeof data === 'string') {
|
||||
res.end(data);
|
||||
} else {
|
||||
res.end(JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.status(404).end(templates.render(404, {
|
||||
status: 404,
|
||||
url
|
||||
}));
|
||||
} catch(err) {
|
||||
// TODO nice error pages
|
||||
res.status(500);
|
||||
res.end(err.stack);
|
||||
}
|
||||
};
|
||||
};
|
||||
9
cypress.json
Normal file
9
cypress.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:3000",
|
||||
"videoRecording": false,
|
||||
"fixturesFolder": "test/cypress/fixtures",
|
||||
"integrationFolder": "test/cypress/integration",
|
||||
"pluginsFile": false,
|
||||
"screenshotsFolder": "test/cypress/screenshots",
|
||||
"supportFile": "test/cypress/support/index.js"
|
||||
}
|
||||
1
index.js
Normal file
1
index.js
Normal file
@@ -0,0 +1 @@
|
||||
throw new Error(`As of Sapper 0.22, you should not import 'sapper' directly. See https://sapper.svelte.technology/guide#0-21-to-0-22 for more information`);
|
||||
@@ -1,12 +0,0 @@
|
||||
const path = require('path');
|
||||
|
||||
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'
|
||||
);
|
||||
@@ -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,2 +1,5 @@
|
||||
--require source-map-support/register
|
||||
--require ts-node/register
|
||||
--recursive
|
||||
utils/**/*.test.js
|
||||
test/unit/*/*.ts
|
||||
test/common/test.js
|
||||
9039
package-lock.json
generated
9039
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
87
package.json
87
package.json
@@ -1,26 +1,82 @@
|
||||
{
|
||||
"name": "sapper",
|
||||
"version": "0.0.8",
|
||||
"description": "Combat-ready apps, engineered by Svelte",
|
||||
"main": "connect.js",
|
||||
"version": "0.22.1",
|
||||
"description": "Military-grade apps, engineered by Svelte",
|
||||
"bin": {
|
||||
"sapper": "./sapper"
|
||||
},
|
||||
"files": [
|
||||
"*.js",
|
||||
"webpack",
|
||||
"config",
|
||||
"sapper",
|
||||
"dist/*.js",
|
||||
"templates/*.js"
|
||||
],
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@std/esm": "^0.18.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"rimraf": "^2.6.2",
|
||||
"webpack": "^3.10.0"
|
||||
"html-minifier": "^3.5.16",
|
||||
"shimport": "0.0.11",
|
||||
"source-map-support": "^0.5.6",
|
||||
"sourcemap-codec": "^1.4.1",
|
||||
"string-hash": "^1.1.3",
|
||||
"tslib": "^1.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "^4.0.1",
|
||||
"svelte": "^1.47.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^1.47.1"
|
||||
"@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",
|
||||
"agadoo": "^1.0.1",
|
||||
"cheap-watch": "^0.3.0",
|
||||
"compression": "^1.7.1",
|
||||
"cookie": "^0.3.1",
|
||||
"devalue": "^1.0.4",
|
||||
"eslint": "^4.13.1",
|
||||
"eslint-plugin-import": "^2.12.0",
|
||||
"express": "^4.16.3",
|
||||
"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": {
|
||||
"test": "mocha --opts mocha.opts"
|
||||
"cy:open": "cypress open",
|
||||
"test": "mocha --opts mocha.opts",
|
||||
"pretest": "npm run build",
|
||||
"build": "rm -rf dist && rollup -c",
|
||||
"prepare": "npm run build",
|
||||
"dev": "rollup -cw",
|
||||
"prepublishOnly": "npm test",
|
||||
"update_mime_types": "curl http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types | grep -e \"^[^#]\" > src/middleware/mime-types.md"
|
||||
},
|
||||
"repository": "https://github.com/sveltejs/sapper",
|
||||
"keywords": [
|
||||
@@ -35,8 +91,5 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/sveltejs/sapper/issues"
|
||||
},
|
||||
"homepage": "https://github.com/sveltejs/sapper#readme",
|
||||
"@std/esm": {
|
||||
"esm": "js"
|
||||
}
|
||||
"homepage": "https://github.com/sveltejs/sapper#readme"
|
||||
}
|
||||
|
||||
65
rollup.config.js
Normal file
65
rollup.config.js
Normal file
@@ -0,0 +1,65 @@
|
||||
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';
|
||||
import { builtinModules } from 'module';
|
||||
|
||||
const external = [].concat(
|
||||
Object.keys(pkg.dependencies),
|
||||
Object.keys(process.binding('natives')),
|
||||
'sapper/core.js'
|
||||
);
|
||||
|
||||
function template(kind, external) {
|
||||
return {
|
||||
input: `templates/src/${kind}/index.ts`,
|
||||
output: {
|
||||
file: `templates/${kind}.js`,
|
||||
format: 'es'
|
||||
},
|
||||
external,
|
||||
plugins: [
|
||||
resolve(),
|
||||
commonjs(),
|
||||
string({
|
||||
include: '**/*.md'
|
||||
}),
|
||||
typescript({
|
||||
typescript: require('typescript'),
|
||||
target: "ES2017"
|
||||
})
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
export default [
|
||||
template('client', ['__ROOT__', '__ERROR__']),
|
||||
template('server', builtinModules),
|
||||
|
||||
{
|
||||
input: [
|
||||
`src/api.ts`,
|
||||
`src/cli.ts`,
|
||||
`src/core.ts`,
|
||||
`src/rollup.ts`,
|
||||
`src/webpack.ts`
|
||||
],
|
||||
output: {
|
||||
dir: 'dist',
|
||||
format: 'cjs',
|
||||
sourcemap: true
|
||||
},
|
||||
external,
|
||||
plugins: [
|
||||
json(),
|
||||
resolve(),
|
||||
commonjs(),
|
||||
typescript({
|
||||
typescript: require('typescript')
|
||||
})
|
||||
],
|
||||
experimentalCodeSplitting: true
|
||||
}
|
||||
];
|
||||
@@ -1,22 +0,0 @@
|
||||
const app = {
|
||||
init(callback) {
|
||||
window.addEventListener('click', event => {
|
||||
let a = event.target;
|
||||
while (a && a.nodeName !== 'A') a = a.parentNode;
|
||||
if (!a) return;
|
||||
|
||||
if (callback(new URL(a.href))) {
|
||||
event.preventDefault();
|
||||
history.pushState({}, '', a.href);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('popstate', event => {
|
||||
callback(window.location);
|
||||
});
|
||||
|
||||
callback(window.location);
|
||||
}
|
||||
};
|
||||
|
||||
export default app;
|
||||
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 };
|
||||
111
src/api/build.ts
Normal file
111
src/api/build.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import mkdirp from 'mkdirp';
|
||||
import rimraf from 'rimraf';
|
||||
import { EventEmitter } from 'events';
|
||||
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 } from '../interfaces';
|
||||
import read_template from '../core/read_template';
|
||||
|
||||
type Opts = {
|
||||
legacy: boolean;
|
||||
bundler: 'rollup' | 'webpack';
|
||||
};
|
||||
|
||||
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 src/template.html
|
||||
// TODO compile this to a function? could be quicker than str.replace(...).replace(...).replace(...)
|
||||
const template = read_template();
|
||||
|
||||
// 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 src/manifest/client.js and src/manifest/server.js
|
||||
create_main_manifests({ bundler: opts.bundler, manifest_data });
|
||||
|
||||
const { client, server, serviceworker } = await create_compilers(opts.bundler);
|
||||
|
||||
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 } = await create_compilers(opts.bundler);
|
||||
|
||||
const client_result = await client.compile();
|
||||
|
||||
emitter.emit('build', <events.BuildEvent>{
|
||||
type: 'client (legacy)',
|
||||
// TODO duration/warnings
|
||||
result: client_result
|
||||
});
|
||||
|
||||
client_result.to_json(manifest_data, dirs);
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
488
src/api/dev.ts
Normal file
488
src/api/dev.ts
Normal file
@@ -0,0 +1,488 @@
|
||||
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';
|
||||
import read_template from '../core/read_template';
|
||||
|
||||
export function dev(opts) {
|
||||
return new Watcher(opts);
|
||||
}
|
||||
|
||||
class Watcher extends EventEmitter {
|
||||
bundler: string;
|
||||
dirs: {
|
||||
src: string;
|
||||
dest: string;
|
||||
routes: string;
|
||||
rollup: string;
|
||||
webpack: string;
|
||||
}
|
||||
port: number;
|
||||
closed: boolean;
|
||||
|
||||
dev_port: number;
|
||||
live: boolean;
|
||||
hot: boolean;
|
||||
|
||||
devtools_port: number;
|
||||
|
||||
dev_server: DevServer;
|
||||
proc: child_process.ChildProcess;
|
||||
filewatchers: Array<{ close: () => void }>;
|
||||
deferred: Deferred;
|
||||
|
||||
crashed: boolean;
|
||||
restarting: boolean;
|
||||
current_build: {
|
||||
changed: Set<string>;
|
||||
rebuilding: Set<string>;
|
||||
unique_warnings: Set<string>;
|
||||
unique_errors: Set<string>;
|
||||
}
|
||||
|
||||
constructor({
|
||||
src = locations.src(),
|
||||
dest = locations.dest(),
|
||||
routes = locations.routes(),
|
||||
'dev-port': dev_port,
|
||||
live,
|
||||
hot,
|
||||
'devtools-port': devtools_port,
|
||||
bundler,
|
||||
webpack = 'webpack',
|
||||
rollup = 'rollup',
|
||||
port = +process.env.PORT
|
||||
}: {
|
||||
src: string,
|
||||
dest: string,
|
||||
routes: string,
|
||||
'dev-port': number,
|
||||
live: boolean,
|
||||
hot: boolean,
|
||||
'devtools-port': number,
|
||||
bundler?: string,
|
||||
webpack: string,
|
||||
rollup: string,
|
||||
port: number
|
||||
}) {
|
||||
super();
|
||||
|
||||
this.bundler = validate_bundler(bundler);
|
||||
this.dirs = { src, dest, routes, webpack, rollup };
|
||||
this.port = port;
|
||||
this.closed = false;
|
||||
|
||||
this.dev_port = dev_port;
|
||||
this.live = live;
|
||||
this.hot = hot;
|
||||
|
||||
this.devtools_port = devtools_port;
|
||||
|
||||
this.filewatchers = [];
|
||||
|
||||
this.current_build = {
|
||||
changed: new Set(),
|
||||
rebuilding: new Set(),
|
||||
unique_errors: new Set(),
|
||||
unique_warnings: new Set()
|
||||
};
|
||||
|
||||
// remove this in a future version
|
||||
const template = read_template();
|
||||
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);
|
||||
|
||||
// Chrome looks for debugging targets on ports 9222 and 9229 by default
|
||||
if (!this.devtools_port) this.devtools_port = await ports.find(9222);
|
||||
|
||||
let manifest_data: ManifestData;
|
||||
|
||||
try {
|
||||
manifest_data = create_manifest_data();
|
||||
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.src()}/template.html`, () => {
|
||||
this.dev_server.send({
|
||||
action: 'reload'
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
let deferred = new Deferred();
|
||||
|
||||
// TODO watch the configs themselves?
|
||||
const compilers: Compilers = await create_compilers(this.bundler, 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();
|
||||
}
|
||||
|
||||
// we need to give the child process its own DevTools port,
|
||||
// otherwise Node will try to use the parent's (and fail)
|
||||
const debugArgRegex = /--inspect(?:-brk|-port)?|--debug-port/;
|
||||
const execArgv = process.execArgv.slice();
|
||||
if (execArgv.some((arg: string) => !!arg.match(debugArgRegex))) {
|
||||
execArgv.push(`--inspect-port=${this.devtools_port}`);
|
||||
}
|
||||
|
||||
this.proc = child_process.fork(`${dest}/server.js`, [], {
|
||||
cwd: process.cwd(),
|
||||
env: Object.assign({
|
||||
PORT: this.port
|
||||
}, process.env),
|
||||
stdio: ['ipc'],
|
||||
execArgv
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
194
src/api/export.ts
Normal file
194
src/api/export.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
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,
|
||||
static: 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(opts.static).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/interfaces';
|
||||
|
||||
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 = '__sapper__/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__/build')
|
||||
.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 = '__sapper__/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(),
|
||||
src: locations.src(),
|
||||
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);
|
||||
}
|
||||
}
|
||||
51
src/cli/export.ts
Normal file
51
src/cli/export.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
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(),
|
||||
static: locations.static(),
|
||||
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);
|
||||
}
|
||||
38
src/cli/utils/validate_bundler.ts
Normal file
38
src/cli/utils/validate_bundler.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import * as fs from 'fs';
|
||||
|
||||
export default function validate_bundler(bundler?: 'rollup' | 'webpack') {
|
||||
if (!bundler) {
|
||||
bundler = (
|
||||
fs.existsSync('rollup.config.js') ? 'rollup' :
|
||||
fs.existsSync('webpack.config.js') ? 'webpack' :
|
||||
null
|
||||
);
|
||||
|
||||
if (!bundler) {
|
||||
// TODO remove in a future version
|
||||
deprecate_dir('rollup');
|
||||
deprecate_dir('webpack');
|
||||
|
||||
throw new Error(`Could not find rollup.config.js or webpack.config.js`);
|
||||
}
|
||||
}
|
||||
|
||||
if (bundler !== 'rollup' && bundler !== 'webpack') {
|
||||
throw new Error(`'${bundler}' is not a valid option for --bundler — must be either 'rollup' or 'webpack'`);
|
||||
}
|
||||
|
||||
return bundler;
|
||||
}
|
||||
|
||||
function deprecate_dir(bundler: 'rollup' | 'webpack') {
|
||||
try {
|
||||
const stats = fs.statSync(bundler);
|
||||
if (!stats.isDirectory()) return;
|
||||
} catch (err) {
|
||||
// do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO link to docs, once those docs exist
|
||||
throw new Error(`As of Sapper 0.21, build configuration should be placed in a single ${bundler}.config.js file`);
|
||||
}
|
||||
11
src/config.ts
Normal file
11
src/config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import * as path from 'path';
|
||||
|
||||
export const dev = () => process.env.NODE_ENV !== 'production';
|
||||
|
||||
export const locations = {
|
||||
base: () => path.resolve(process.env.SAPPER_BASE || ''),
|
||||
src: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_SRC || 'src'),
|
||||
static: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_STATIC || 'static'),
|
||||
routes: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_ROUTES || 'src/routes'),
|
||||
dest: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_DEST || `__sapper__/${dev() ? 'dev' : 'build'}`)
|
||||
};
|
||||
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';
|
||||
167
src/core/create_compilers/RollupCompiler.ts
Normal file
167
src/core/create_compilers/RollupCompiler.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import * as path from 'path';
|
||||
import relative from 'require-relative';
|
||||
import { CompileResult } from './interfaces';
|
||||
import RollupResult from './RollupResult';
|
||||
|
||||
let rollup: any;
|
||||
|
||||
export default class RollupCompiler {
|
||||
_: Promise<any>;
|
||||
_oninvalid: (filename: string) => void;
|
||||
_start: number;
|
||||
input: string;
|
||||
warnings: any[];
|
||||
errors: any[];
|
||||
chunks: any[];
|
||||
css_files: Array<{ id: string, code: string }>;
|
||||
|
||||
constructor(config: any) {
|
||||
this._ = this.get_config(config);
|
||||
this.input = null;
|
||||
this.warnings = [];
|
||||
this.errors = [];
|
||||
this.chunks = [];
|
||||
this.css_files = [];
|
||||
}
|
||||
|
||||
async get_config(mod: any) {
|
||||
// TODO this is hacky, and doesn't need to apply to all three compilers
|
||||
(mod.plugins || (mod.plugins = [])).push({
|
||||
name: 'sapper-internal',
|
||||
options: (opts: any) => {
|
||||
this.input = opts.input;
|
||||
},
|
||||
renderChunk: (code: string, chunk: any) => {
|
||||
this.chunks.push(chunk);
|
||||
},
|
||||
transform: (code: string, id: string) => {
|
||||
if (/\.css$/.test(id)) {
|
||||
this.css_files.push({ id, code });
|
||||
return ``;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const onwarn = mod.onwarn || ((warning: any, handler: (warning: any) => void) => {
|
||||
handler(warning);
|
||||
});
|
||||
|
||||
mod.onwarn = (warning: any) => {
|
||||
onwarn(warning, (warning: any) => {
|
||||
this.warnings.push(warning);
|
||||
});
|
||||
};
|
||||
|
||||
return mod;
|
||||
}
|
||||
|
||||
oninvalid(cb: (filename: string) => void) {
|
||||
this._oninvalid = cb;
|
||||
}
|
||||
|
||||
async compile(): Promise<CompileResult> {
|
||||
const config = await this._;
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
try {
|
||||
const bundle = await rollup.rollup(config);
|
||||
await bundle.write(config.output);
|
||||
|
||||
return new RollupResult(Date.now() - start, this);
|
||||
} catch (err) {
|
||||
if (err.filename) {
|
||||
// TODO this is a bit messy. Also, can
|
||||
// Rollup emit other kinds of error?
|
||||
err.message = [
|
||||
`Failed to build — error in ${err.filename}: ${err.message}`,
|
||||
err.frame
|
||||
].filter(Boolean).join('\n');
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async watch(cb: (err?: Error, stats?: any) => void) {
|
||||
const config = await this._;
|
||||
|
||||
const watcher = rollup.watch(config);
|
||||
|
||||
watcher.on('change', (id: string) => {
|
||||
this.chunks = [];
|
||||
this.warnings = [];
|
||||
this.errors = [];
|
||||
this._oninvalid(id);
|
||||
});
|
||||
|
||||
watcher.on('event', (event: any) => {
|
||||
switch (event.code) {
|
||||
case 'FATAL':
|
||||
// TODO kill the process?
|
||||
if (event.error.filename) {
|
||||
// TODO this is a bit messy. Also, can
|
||||
// Rollup emit other kinds of error?
|
||||
event.error.message = [
|
||||
`Failed to build — error in ${event.error.filename}: ${event.error.message}`,
|
||||
event.error.frame
|
||||
].filter(Boolean).join('\n');
|
||||
}
|
||||
|
||||
cb(event.error);
|
||||
break;
|
||||
|
||||
case 'ERROR':
|
||||
this.errors.push(event.error);
|
||||
cb(null, new RollupResult(Date.now() - this._start, this));
|
||||
break;
|
||||
|
||||
case 'START':
|
||||
case 'END':
|
||||
// TODO is there anything to do with this info?
|
||||
break;
|
||||
|
||||
case 'BUNDLE_START':
|
||||
this._start = Date.now();
|
||||
break;
|
||||
|
||||
case 'BUNDLE_END':
|
||||
cb(null, new RollupResult(Date.now() - this._start, this));
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(`Unexpected event ${event.code}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async load_config() {
|
||||
if (!rollup) rollup = relative('rollup', process.cwd());
|
||||
|
||||
const input = path.resolve('rollup.config.js');
|
||||
|
||||
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 config: any = require(input);
|
||||
delete require.cache[input];
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
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')
|
||||
};
|
||||
}
|
||||
|
||||
46
src/core/create_compilers/WebpackCompiler.ts
Normal file
46
src/core/create_compilers/WebpackCompiler.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import relative from 'require-relative';
|
||||
import { CompileResult } from './interfaces';
|
||||
import WebpackResult from './WebpackResult';
|
||||
|
||||
let webpack: any;
|
||||
|
||||
export class WebpackCompiler {
|
||||
_: any;
|
||||
|
||||
constructor(config: any) {
|
||||
if (!webpack) webpack = relative('webpack', process.cwd());
|
||||
this._ = webpack(config);
|
||||
}
|
||||
|
||||
oninvalid(cb: (filename: string) => void) {
|
||||
this._.hooks.invalid.tap('sapper', cb);
|
||||
}
|
||||
|
||||
compile(): Promise<CompileResult> {
|
||||
return new Promise((fulfil, reject) => {
|
||||
this._.run((err: Error, stats: any) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const result = new WebpackResult(stats);
|
||||
|
||||
if (result.errors.length) {
|
||||
console.error(stats.toString({ colors: true }));
|
||||
reject(new Error(`Encountered errors while building app`));
|
||||
}
|
||||
|
||||
else {
|
||||
fulfil(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
watch(cb: (err?: Error, stats?: any) => void) {
|
||||
this._.watch({}, (err?: Error, stats?: any) => {
|
||||
cb(err, stats && new WebpackResult(stats));
|
||||
});
|
||||
}
|
||||
}
|
||||
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 });
|
||||
}
|
||||
}
|
||||
232
src/core/create_compilers/extract_css.ts
Normal file
232
src/core/create_compilers/extract_css.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
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';
|
||||
import { posixify } from '../utils'
|
||||
|
||||
const inline_sourcemap_header = 'data:application/json;charset=utf-8;base64,';
|
||||
|
||||
function extract_sourcemap(raw: string, id: string) {
|
||||
let raw_map: string;
|
||||
let map = null;
|
||||
|
||||
const code = raw.replace(/\/\*#\s+sourceMappingURL=(.+)\s+\*\//g, (m, url) => {
|
||||
if (raw_map) {
|
||||
// TODO should not happen!
|
||||
throw new Error(`Found multiple sourcemaps in single CSS file (${id})`);
|
||||
}
|
||||
|
||||
raw_map = url;
|
||||
return '';
|
||||
}).trim();
|
||||
|
||||
if (raw_map) {
|
||||
if (raw_map.startsWith(inline_sourcemap_header)) {
|
||||
const json = Buffer.from(raw_map.slice(inline_sourcemap_header.length), 'base64').toString();
|
||||
map = JSON.parse(json);
|
||||
} else {
|
||||
// TODO do we want to handle non-inline sourcemaps? could be a rabbit hole
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
code,
|
||||
map
|
||||
};
|
||||
}
|
||||
|
||||
type SourceMap = {
|
||||
version: 3;
|
||||
file: string;
|
||||
sources: string[];
|
||||
sourcesContent: string[];
|
||||
names: string[];
|
||||
mappings: string;
|
||||
};
|
||||
|
||||
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 = posixify(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;
|
||||
}
|
||||
|
||||
let main = client_result.assets.main;
|
||||
if (process.env.SAPPER_LEGACY_BUILD) main = `legacy/${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;
|
||||
}
|
||||
44
src/core/create_compilers/index.ts
Normal file
44
src/core/create_compilers/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import * as path from 'path';
|
||||
import RollupCompiler from './RollupCompiler';
|
||||
import { WebpackCompiler } from './WebpackCompiler';
|
||||
|
||||
export type Compiler = RollupCompiler | WebpackCompiler;
|
||||
|
||||
export type Compilers = {
|
||||
client: Compiler;
|
||||
server: Compiler;
|
||||
serviceworker?: Compiler;
|
||||
}
|
||||
|
||||
export default async function create_compilers(bundler: 'rollup' | 'webpack'): Promise<Compilers> {
|
||||
if (bundler === 'rollup') {
|
||||
const config = await RollupCompiler.load_config();
|
||||
validate_config(config, 'rollup');
|
||||
|
||||
return {
|
||||
client: new RollupCompiler(config.client),
|
||||
server: new RollupCompiler(config.server),
|
||||
serviceworker: config.serviceworker && new RollupCompiler(config.serviceworker)
|
||||
};
|
||||
}
|
||||
|
||||
if (bundler === 'webpack') {
|
||||
const config = require(path.resolve('webpack.config.js'));
|
||||
validate_config(config, 'webpack');
|
||||
|
||||
return {
|
||||
client: new WebpackCompiler(config.client),
|
||||
server: new WebpackCompiler(config.server),
|
||||
serviceworker: config.serviceworker && new WebpackCompiler(config.serviceworker)
|
||||
};
|
||||
}
|
||||
|
||||
// this shouldn't be possible...
|
||||
throw new Error(`Invalid bundler option '${bundler}'`);
|
||||
}
|
||||
|
||||
function validate_config(config: any, bundler: 'rollup' | 'webpack') {
|
||||
if (!config.client || !config.server) {
|
||||
throw new Error(`${bundler}.config.js must export a { client, server, serviceworker? } object`);
|
||||
}
|
||||
}
|
||||
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[]>
|
||||
}
|
||||
}
|
||||
307
src/core/create_manifest_data.ts
Normal file
307
src/core/create_manifest_data.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
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 {
|
||||
// TODO remove in a future version
|
||||
if (!fs.existsSync(cwd)) {
|
||||
throw new Error(`As of Sapper 0.21, the routes/ directory should become src/routes/`);
|
||||
}
|
||||
|
||||
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 ? '\\\/?$' : '$')
|
||||
);
|
||||
}
|
||||
220
src/core/create_manifests.ts
Normal file
220
src/core/create_manifests.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import glob from 'tiny-glob/sync.js';
|
||||
import { posixify, stringify, 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 = '__sapper__';
|
||||
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[];
|
||||
}) {
|
||||
let files;
|
||||
|
||||
// TODO remove in a future version
|
||||
if (fs.existsSync(locations.static())) {
|
||||
files = glob('**', { cwd: locations.static(), filesOnly: true });
|
||||
} else {
|
||||
if (fs.existsSync('assets')) {
|
||||
throw new Error(`As of Sapper 0.21, the assets/ directory should become static/`);
|
||||
}
|
||||
|
||||
files = [];
|
||||
}
|
||||
|
||||
let code = `
|
||||
// This file is generated by Sapper — do not edit it!
|
||||
export const timestamp = ${Date.now()};
|
||||
|
||||
export const files = [\n\t${files.map((x: string) => stringify(x)).join(',\n\t')}\n];
|
||||
export { files as assets }; // legacy
|
||||
|
||||
export const shell = [\n\t${client_files.map((x: string) => stringify(x)).join(',\n\t')}\n];
|
||||
|
||||
export const routes = [\n\t${manifest_data.pages.map((r: Page) => `{ pattern: ${r.pattern} }`).join(',\n\t')}\n];
|
||||
`.replace(/^\t\t/gm, '').trim();
|
||||
|
||||
write_if_changed(`__sapper__/service-worker.js`, code);
|
||||
}
|
||||
|
||||
function generate_client(
|
||||
manifest_data: ManifestData,
|
||||
path_to_routes: string,
|
||||
bundler: string,
|
||||
dev_port?: number
|
||||
) {
|
||||
const template_file = path.resolve(__dirname, '../templates/client.js');
|
||||
const template = fs.readFileSync(template_file, 'utf-8');
|
||||
|
||||
const page_ids = new Set(manifest_data.pages.map(page =>
|
||||
page.pattern.toString()));
|
||||
|
||||
const server_routes_to_ignore = manifest_data.server_routes.filter(route =>
|
||||
!page_ids.has(route.pattern.toString()));
|
||||
|
||||
const component_indexes: Record<string, number> = {};
|
||||
|
||||
const components = `[
|
||||
${manifest_data.components.map((component, i) => {
|
||||
const annotation = bundler === 'webpack'
|
||||
? `/* webpackChunkName: "${component.name}" */ `
|
||||
: '';
|
||||
|
||||
const source = get_file(path_to_routes, component);
|
||||
|
||||
component_indexes[component.name] = i;
|
||||
|
||||
return `{
|
||||
js: () => import(${annotation}${stringify(source)}),
|
||||
css: "__SAPPER_CSS_PLACEHOLDER:${stringify(component.file, false)}__"
|
||||
}`;
|
||||
}).join(',\n\t\t')}
|
||||
]`.replace(/^\t/gm, '').trim();
|
||||
|
||||
let needs_decode = false;
|
||||
|
||||
let 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) {
|
||||
needs_decode = true;
|
||||
const props = part.params.map((param, i) => `${param}: d(match[${i + 1}])`);
|
||||
return `{ i: ${component_indexes[part.component.name]}, params: match => ({ ${props.join(', ')} }) }`;
|
||||
}
|
||||
|
||||
return `{ i: ${component_indexes[part.component.name]} }`;
|
||||
}).join(',\n\t\t\t\t')}
|
||||
]
|
||||
}`).join(',\n\n\t\t')}
|
||||
]`.replace(/^\t/gm, '').trim();
|
||||
|
||||
if (needs_decode) {
|
||||
pages = `(d => ${pages})(decodeURIComponent)`
|
||||
}
|
||||
|
||||
let footer = '';
|
||||
|
||||
if (dev()) {
|
||||
const sapper_dev_client = posixify(
|
||||
path.resolve(__dirname, '../sapper-dev-client.js')
|
||||
);
|
||||
|
||||
footer = `
|
||||
|
||||
import(${stringify(sapper_dev_client)}).then(client => {
|
||||
client.connect(${dev_port});
|
||||
});`.replace(/^\t{3}/gm, '');
|
||||
}
|
||||
|
||||
return `// This file is generated by Sapper — do not edit it!\n` + template
|
||||
.replace('__ROOT__', stringify(get_file(path_to_routes, manifest_data.root), false))
|
||||
.replace('__ERROR__', stringify(posixify(`${path_to_routes}/_error.html`), false))
|
||||
.replace('__IGNORE__', `[${server_routes_to_ignore.map(route => route.pattern).join(', ')}]`)
|
||||
.replace('__COMPONENTS__', components)
|
||||
.replace('__PAGES__', pages) +
|
||||
footer;
|
||||
}
|
||||
|
||||
function generate_server(
|
||||
manifest_data: ManifestData,
|
||||
path_to_routes: string
|
||||
) {
|
||||
const template_file = path.resolve(__dirname, '../templates/server.js');
|
||||
const template = fs.readFileSync(template_file, 'utf-8');
|
||||
|
||||
const imports = [].concat(
|
||||
manifest_data.server_routes.map(route =>
|
||||
`import * as __${route.name} from ${stringify(posixify(`${path_to_routes}/${route.file}`))};`),
|
||||
manifest_data.components.map(component =>
|
||||
`import __${component.name} from ${stringify(get_file(path_to_routes, component))};`),
|
||||
`import root from ${stringify(get_file(path_to_routes, manifest_data.root))};`,
|
||||
`import error from ${stringify(posixify(`${path_to_routes}/_error.html`))};`
|
||||
);
|
||||
|
||||
let code = `
|
||||
${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: ${stringify(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
|
||||
};`.replace(/^\t\t/gm, '').trim();
|
||||
|
||||
return `// This file is generated by Sapper — do not edit it!\n` + template
|
||||
.replace('__BUILD__DIR__', JSON.stringify(locations.dest()))
|
||||
.replace('__SRC__DIR__', JSON.stringify(locations.src()))
|
||||
.replace('__DEV__', dev() ? 'true' : 'false')
|
||||
.replace(/const manifest = __MANIFEST__;/, code);
|
||||
}
|
||||
|
||||
function get_file(path_to_routes: string, component: PageComponent) {
|
||||
if (component.default) {
|
||||
return `./default-layout.html`;
|
||||
}
|
||||
|
||||
return posixify(`${path_to_routes}/${component.file}`);
|
||||
}
|
||||
17
src/core/read_template.ts
Normal file
17
src/core/read_template.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as fs from 'fs';
|
||||
import { locations } from '../config';
|
||||
|
||||
export default function read_template(dir = locations.src()) {
|
||||
try {
|
||||
return fs.readFileSync(`${dir}/template.html`, 'utf-8');
|
||||
} catch (err) {
|
||||
if (fs.existsSync(`app/template.html`)) {
|
||||
throw new Error(`As of Sapper 0.21, the default folder structure has been changed:
|
||||
app/ --> src/
|
||||
routes/ --> src/routes/
|
||||
assets/ --> static/`);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
81
src/core/utils.ts
Normal file
81
src/core/utils.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
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 stringify(string: string, includeQuotes: boolean = true) {
|
||||
const quoted = JSON.stringify(string);
|
||||
return includeQuotes ? quoted : quoted.slice(1, -1);
|
||||
}
|
||||
|
||||
export function fudge_mtime(file: string) {
|
||||
// need to fudge the mtime so that webpack doesn't go doolally
|
||||
const { atime, mtime } = fs.statSync(file);
|
||||
fs.utimesSync(
|
||||
file,
|
||||
new Date(atime.getTime() - 999999),
|
||||
new Date(mtime.getTime() - 999999)
|
||||
);
|
||||
}
|
||||
|
||||
export const reserved_words = new Set([
|
||||
'arguments',
|
||||
'await',
|
||||
'break',
|
||||
'case',
|
||||
'catch',
|
||||
'class',
|
||||
'const',
|
||||
'continue',
|
||||
'debugger',
|
||||
'default',
|
||||
'delete',
|
||||
'do',
|
||||
'else',
|
||||
'enum',
|
||||
'eval',
|
||||
'export',
|
||||
'extends',
|
||||
'false',
|
||||
'finally',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'implements',
|
||||
'import',
|
||||
'in',
|
||||
'instanceof',
|
||||
'interface',
|
||||
'let',
|
||||
'new',
|
||||
'null',
|
||||
'package',
|
||||
'private',
|
||||
'protected',
|
||||
'public',
|
||||
'return',
|
||||
'static',
|
||||
'super',
|
||||
'switch',
|
||||
'this',
|
||||
'throw',
|
||||
'true',
|
||||
'try',
|
||||
'typeof',
|
||||
'var',
|
||||
'void',
|
||||
'while',
|
||||
'with',
|
||||
'yield',
|
||||
]);
|
||||
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,
|
||||
src: string,
|
||||
routes: string,
|
||||
webpack: string,
|
||||
rollup: string
|
||||
};
|
||||
|
||||
export type ManifestData = {
|
||||
root: PageComponent;
|
||||
components: PageComponent[];
|
||||
pages: Page[];
|
||||
server_routes: ServerRoute[];
|
||||
};
|
||||
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.src()}/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.src()}/server.js`
|
||||
},
|
||||
|
||||
output: () => {
|
||||
return {
|
||||
dir: locations.dest(),
|
||||
format: 'cjs',
|
||||
sourcemap: dev()
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
serviceworker: {
|
||||
input: () => {
|
||||
return `${locations.src()}/service-worker.js`;
|
||||
},
|
||||
|
||||
output: () => {
|
||||
return {
|
||||
file: `${locations.dest()}/service-worker.js`,
|
||||
format: 'iife'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
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.src()}/client`
|
||||
};
|
||||
},
|
||||
|
||||
output: () => {
|
||||
return {
|
||||
path: `${locations.dest()}/client`,
|
||||
filename: '[hash]/[name].js',
|
||||
chunkFilename: '[hash]/[name].[id].js',
|
||||
publicPath: `client/`
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
server: {
|
||||
entry: () => {
|
||||
return {
|
||||
server: `${locations.src()}/server`
|
||||
};
|
||||
},
|
||||
|
||||
output: () => {
|
||||
return {
|
||||
path: locations.dest(),
|
||||
filename: '[name].js',
|
||||
chunkFilename: '[hash]/[name].[id].js',
|
||||
libraryTarget: 'commonjs2'
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
serviceworker: {
|
||||
entry: () => {
|
||||
return {
|
||||
'service-worker': `${locations.src()}/service-worker`
|
||||
};
|
||||
},
|
||||
|
||||
output: () => {
|
||||
return {
|
||||
path: locations.dest(),
|
||||
filename: '[name].js',
|
||||
chunkFilename: '[name].[id].[hash].js'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
import app from 'sapper/runtime/app.js';
|
||||
import { detachNode } from 'svelte/shared.js';
|
||||
|
||||
const target = document.querySelector('__selector__');
|
||||
let component;
|
||||
|
||||
app.init(url => {
|
||||
if (url.origin !== window.location.origin) return;
|
||||
|
||||
let match;
|
||||
let params = {};
|
||||
const query = {};
|
||||
|
||||
function render(mod) {
|
||||
const route = { query, params };
|
||||
|
||||
Promise.resolve(
|
||||
mod.default.preload ? mod.default.preload(route) : {}
|
||||
).then(preloaded => {
|
||||
if (component) {
|
||||
component.destroy();
|
||||
} else {
|
||||
// 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) detachNode(start.nextSibling);
|
||||
detachNode(start);
|
||||
detachNode(end);
|
||||
}
|
||||
|
||||
target.innerHTML = '';
|
||||
}
|
||||
|
||||
component = new mod.default({
|
||||
target,
|
||||
data: Object.assign(route, preloaded),
|
||||
hydrate: !!component
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ROUTES
|
||||
|
||||
return true;
|
||||
});
|
||||
372
templates/src/client/app.ts
Normal file
372
templates/src/client/app.ts
Normal file
@@ -0,0 +1,372 @@
|
||||
import RootComponent from '__ROOT__';
|
||||
import ErrorComponent from '__ERROR__';
|
||||
import {
|
||||
Target,
|
||||
ScrollPosition,
|
||||
Component,
|
||||
Redirect,
|
||||
ComponentLoader,
|
||||
ComponentConstructor,
|
||||
RootProps,
|
||||
Page
|
||||
} from './types';
|
||||
import goto from './goto';
|
||||
|
||||
const ignore = __IGNORE__;
|
||||
export const components: ComponentLoader[] = __COMPONENTS__;
|
||||
export const pages: Page[] = __PAGES__;
|
||||
|
||||
let ready = false;
|
||||
let root_component: Component;
|
||||
let segments: string[] = [];
|
||||
let current_token: {};
|
||||
let root_preload: Promise<any>;
|
||||
let root_data: any;
|
||||
|
||||
const root_props: RootProps = {
|
||||
path: null,
|
||||
params: null,
|
||||
query: null,
|
||||
child: {
|
||||
segment: null,
|
||||
component: null,
|
||||
props: {}
|
||||
}
|
||||
};
|
||||
|
||||
export let prefetching: {
|
||||
href: string;
|
||||
promise: Promise<{ redirect?: Redirect, data?: any, nullable_depth?: number }>;
|
||||
} = null;
|
||||
export function set_prefetching(href, promise) {
|
||||
prefetching = { href, promise };
|
||||
}
|
||||
|
||||
export let store;
|
||||
export function set_store(fn) {
|
||||
store = fn(initial_data.store);
|
||||
}
|
||||
|
||||
export let target: Node;
|
||||
export function set_target(element) {
|
||||
target = element;
|
||||
}
|
||||
|
||||
export let uid = 1;
|
||||
export function set_uid(n) {
|
||||
uid = n;
|
||||
}
|
||||
|
||||
export let cid: number;
|
||||
export function set_cid(n) {
|
||||
cid = n;
|
||||
}
|
||||
|
||||
export const initial_data = typeof __SAPPER__ !== 'undefined' && __SAPPER__;
|
||||
|
||||
const _history = typeof history !== 'undefined' ? history : {
|
||||
pushState: (state: any, title: string, href: string) => {},
|
||||
replaceState: (state: any, title: string, href: string) => {},
|
||||
scrollRestoration: ''
|
||||
};
|
||||
export { _history as history };
|
||||
|
||||
export const scroll_history: Record<string, ScrollPosition> = {};
|
||||
|
||||
export function select_route(url: URL): Target {
|
||||
if (url.origin !== location.origin) return null;
|
||||
if (!url.pathname.startsWith(initial_data.baseUrl)) return null;
|
||||
|
||||
const path = url.pathname.slice(initial_data.baseUrl.length);
|
||||
|
||||
// avoid accidental clashes between server routes and pages
|
||||
if (ignore.some(pattern => pattern.test(path))) return;
|
||||
|
||||
for (let i = 0; i < pages.length; i += 1) {
|
||||
const page = 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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function scroll_state() {
|
||||
return {
|
||||
x: scrollX,
|
||||
y: scrollY
|
||||
};
|
||||
}
|
||||
|
||||
export 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_component) {
|
||||
root_component.set({ preloading: true });
|
||||
}
|
||||
const loaded = prefetching && prefetching.href === target.url.href ?
|
||||
prefetching.promise :
|
||||
prepare_page(target);
|
||||
|
||||
prefetching = null;
|
||||
|
||||
const token = current_token = {};
|
||||
|
||||
return loaded.then(({ redirect, data, nullable_depth }) => {
|
||||
if (redirect) {
|
||||
return goto(redirect.location, { replaceState: true });
|
||||
}
|
||||
|
||||
render(data, nullable_depth, scroll_history[id], token);
|
||||
if (document.activeElement) document.activeElement.blur();
|
||||
});
|
||||
}
|
||||
|
||||
function render(data: any, nullable_depth: number, scroll: ScrollPosition, token: {}) {
|
||||
if (current_token !== token) return;
|
||||
|
||||
if (root_component) {
|
||||
// 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_component.set({ child: data.child });
|
||||
|
||||
// then render new stuff
|
||||
level.component = component;
|
||||
root_component.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_component = new RootComponent({
|
||||
target,
|
||||
data,
|
||||
store,
|
||||
hydrate: true
|
||||
});
|
||||
}
|
||||
|
||||
if (scroll) {
|
||||
scrollTo(scroll.x, scroll.y);
|
||||
}
|
||||
|
||||
Object.assign(root_props, data);
|
||||
ready = true;
|
||||
}
|
||||
|
||||
export 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) => 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 = RootComponent.preload
|
||||
? initial_data.preloaded[0] || RootComponent.preload.call(preload_context, {
|
||||
path,
|
||||
query,
|
||||
params: {}
|
||||
})
|
||||
: {};
|
||||
}
|
||||
|
||||
return Promise.all(page.parts.map((part, i) => {
|
||||
if (i < changed_from) return null;
|
||||
if (!part) return null;
|
||||
|
||||
return load_component(components[part.i]).then(Component => {
|
||||
const req = {
|
||||
path,
|
||||
query,
|
||||
params: part.params ? part.params(target.match) : {}
|
||||
};
|
||||
|
||||
let preloaded;
|
||||
if (ready || !initial_data.preloaded[i + 1]) {
|
||||
preloaded = Component.preload
|
||||
? Component.preload.call(preload_context, req)
|
||||
: {};
|
||||
} else {
|
||||
preloaded = initial_data.preloaded[i + 1];
|
||||
}
|
||||
|
||||
return Promise.resolve(preloaded).then(preloaded => {
|
||||
return { Component, preloaded };
|
||||
});
|
||||
});
|
||||
})).catch(err => {
|
||||
error = { statusCode: 500, message: err };
|
||||
return [];
|
||||
}).then(results => {
|
||||
if (root_data) {
|
||||
return results;
|
||||
} else {
|
||||
return Promise.resolve(root_preload).then(value => {
|
||||
root_data = value;
|
||||
return results;
|
||||
});
|
||||
}
|
||||
}).then(results => {
|
||||
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: ErrorComponent,
|
||||
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 };
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
export 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 detach(node: Node) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
|
||||
function changed(a: Record<string, string | true>, b: Record<string, string | true>) {
|
||||
return JSON.stringify(a) !== JSON.stringify(b);
|
||||
}
|
||||
13
templates/src/client/goto/index.ts
Normal file
13
templates/src/client/goto/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { history, select_route, navigate, cid } from '../app';
|
||||
|
||||
export default function goto(href: string, opts = { replaceState: false }) {
|
||||
const target = select_route(new URL(href, document.baseURI));
|
||||
|
||||
if (target) {
|
||||
history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
|
||||
return navigate(target, null).then(() => {});
|
||||
}
|
||||
|
||||
location.href = href;
|
||||
return new Promise(f => {}); // never resolves
|
||||
}
|
||||
4
templates/src/client/index.ts
Normal file
4
templates/src/client/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as start } from './start/index';
|
||||
export { default as goto } from './goto/index';
|
||||
export { default as prefetch } from './prefetch/index';
|
||||
export { default as prefetchRoutes } from './prefetchRoutes/index';
|
||||
10
templates/src/client/prefetch/index.ts
Normal file
10
templates/src/client/prefetch/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { select_route, prefetching, set_prefetching, prepare_page } from '../app';
|
||||
import { Target } from '../types';
|
||||
|
||||
export default function prefetch(href: string) {
|
||||
const target: Target = select_route(new URL(href, document.baseURI));
|
||||
|
||||
if (target && (!prefetching || href !== prefetching.href)) {
|
||||
set_prefetching(href, prepare_page(target));
|
||||
}
|
||||
}
|
||||
12
templates/src/client/prefetchRoutes/index.ts
Normal file
12
templates/src/client/prefetchRoutes/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { components, pages, load_component } from "../app";
|
||||
|
||||
export default function prefetchRoutes(pathnames: string[]) {
|
||||
return 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(components[part.i])));
|
||||
}), Promise.resolve());
|
||||
}
|
||||
138
templates/src/client/start/index.ts
Normal file
138
templates/src/client/start/index.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import {
|
||||
cid,
|
||||
history,
|
||||
initial_data,
|
||||
navigate,
|
||||
scroll_history,
|
||||
scroll_state,
|
||||
select_route,
|
||||
set_store,
|
||||
set_target,
|
||||
uid,
|
||||
set_uid,
|
||||
set_cid
|
||||
} from '../app';
|
||||
import prefetch from '../prefetch/index';
|
||||
import { Store } from '../types';
|
||||
|
||||
export default function start(opts: {
|
||||
target: Node,
|
||||
store?: (data: any) => Store
|
||||
}) {
|
||||
if ('scrollRestoration' in history) {
|
||||
history.scrollRestoration = 'manual';
|
||||
}
|
||||
|
||||
set_target(opts.target);
|
||||
if (opts.store) set_store(opts.store);
|
||||
|
||||
addEventListener('click', handle_click);
|
||||
addEventListener('popstate', handle_popstate);
|
||||
|
||||
// prefetch
|
||||
addEventListener('touchstart', trigger_prefetch);
|
||||
addEventListener('mousemove', handle_mousemove);
|
||||
|
||||
return Promise.resolve().then(() => {
|
||||
const { hash, href } = location;
|
||||
|
||||
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(location.href));
|
||||
if (target) return navigate(target, uid);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mousemove_timeout: NodeJS.Timer;
|
||||
|
||||
function handle_mousemove(event: MouseEvent) {
|
||||
clearTimeout(mousemove_timeout);
|
||||
mousemove_timeout = setTimeout(() => {
|
||||
trigger_prefetch(event);
|
||||
}, 20);
|
||||
}
|
||||
|
||||
function trigger_prefetch(event: MouseEvent | TouchEvent) {
|
||||
const a: HTMLAnchorElement = <HTMLAnchorElement>find_anchor(<Node>event.target);
|
||||
if (!a || a.rel !== 'prefetch') return;
|
||||
|
||||
prefetch(a.href);
|
||||
}
|
||||
|
||||
function handle_click(event: MouseEvent) {
|
||||
// Adapted from https://github.com/visionmedia/page.js
|
||||
// MIT license https://github.com/visionmedia/page.js#license
|
||||
if (which(event) !== 1) return;
|
||||
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
|
||||
if (event.defaultPrevented) return;
|
||||
|
||||
const a: HTMLAnchorElement | SVGAElement = <HTMLAnchorElement | SVGAElement>find_anchor(<Node>event.target);
|
||||
if (!a) return;
|
||||
|
||||
if (!a.href) return;
|
||||
|
||||
// check if link is inside an svg
|
||||
// in this case, both href and target are always inside an object
|
||||
const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
|
||||
const href = String(svg ? (<SVGAElement>a).href.baseVal : a.href);
|
||||
|
||||
if (href === location.href) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore if tag has
|
||||
// 1. 'download' attribute
|
||||
// 2. rel='external' attribute
|
||||
if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
|
||||
|
||||
// Ignore if <a> has a target
|
||||
if (svg ? (<SVGAElement>a).target.baseVal : a.target) return;
|
||||
|
||||
const url = new URL(href);
|
||||
|
||||
// Don't handle hash changes
|
||||
if (url.pathname === location.pathname && url.search === location.search) return;
|
||||
|
||||
const target = select_route(url);
|
||||
if (target) {
|
||||
navigate(target, null);
|
||||
event.preventDefault();
|
||||
history.pushState({ id: cid }, '', url.href);
|
||||
}
|
||||
}
|
||||
|
||||
function which(event: MouseEvent) {
|
||||
return event.which === null ? event.button : event.which;
|
||||
}
|
||||
|
||||
function find_anchor(node: Node) {
|
||||
while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG <a> elements have a lowercase name
|
||||
return node;
|
||||
}
|
||||
|
||||
function handle_popstate(event: PopStateEvent) {
|
||||
scroll_history[cid] = scroll_state();
|
||||
|
||||
if (event.state) {
|
||||
const url = new URL(location.href);
|
||||
const target = select_route(url);
|
||||
if (target) {
|
||||
navigate(target, event.state.id);
|
||||
} else {
|
||||
location.href = location.href;
|
||||
}
|
||||
} else {
|
||||
// hashchange
|
||||
set_uid(uid + 1);
|
||||
set_cid(uid);
|
||||
history.replaceState({ id: cid }, '', location.href);
|
||||
}
|
||||
}
|
||||
68
templates/src/client/types.ts
Normal file
68
templates/src/client/types.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
export type Params = Record<string, string>;
|
||||
export type Query = Record<string, string | true>;
|
||||
export type RouteData = { params: Params, query: Query, path: string };
|
||||
|
||||
type Child = {
|
||||
segment?: string;
|
||||
props?: any;
|
||||
component?: Component;
|
||||
};
|
||||
|
||||
export type RootProps = {
|
||||
path: string;
|
||||
params: Record<string, string>;
|
||||
query: Record<string, string>;
|
||||
child: Child;
|
||||
};
|
||||
|
||||
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<{
|
||||
i: number;
|
||||
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;
|
||||
};
|
||||
|
||||
export type Store = {
|
||||
get: () => any;
|
||||
}
|
||||
1
templates/src/server/index.ts
Normal file
1
templates/src/server/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as middleware } from './middleware/index';
|
||||
319
templates/src/server/middleware/get_page_handler.ts
Normal file
319
templates/src/server/middleware/get_page_handler.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import cookie from 'cookie';
|
||||
import devalue from 'devalue';
|
||||
import fetch from 'node-fetch';
|
||||
import { URL } from 'url';
|
||||
import { build_dir, dev, src_dir, IGNORE } from '../placeholders';
|
||||
import { Manifest, Page, Props, Req, Res, Store } from './types';
|
||||
|
||||
export function get_page_handler(
|
||||
manifest: Manifest,
|
||||
store_getter: (req: Req, res: Res) => Store
|
||||
) {
|
||||
const get_build_info = dev
|
||||
? () => JSON.parse(fs.readFileSync(path.join(build_dir, 'build.json'), 'utf-8'))
|
||||
: (assets => () => assets)(JSON.parse(fs.readFileSync(path.join(build_dir, 'build.json'), 'utf-8')));
|
||||
|
||||
const template = dev
|
||||
? () => read_template(src_dir)
|
||||
: (str => () => str)(read_template(build_dir));
|
||||
|
||||
const has_service_worker = fs.existsSync(path.join(build_dir, 'service-worker.js'));
|
||||
|
||||
const { server_routes, pages } = manifest;
|
||||
const error_route = manifest.error;
|
||||
|
||||
function handle_error(req: Req, res: Res, statusCode: number, error: Error | string) {
|
||||
handle_page({
|
||||
pattern: null,
|
||||
parts: [
|
||||
{ name: null, component: error_route }
|
||||
]
|
||||
}, req, res, statusCode, error || new Error('Unknown error in preload function'));
|
||||
}
|
||||
|
||||
function handle_page(page: Page, req: Req, res: Res, 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(',')}};`;
|
||||
|
||||
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
|
||||
// TODO embed build_info in placeholder.ts
|
||||
if (build_info.css && build_info.css.main) {
|
||||
const css_chunks = new Set();
|
||||
if (build_info.css.main) css_chunks.add(build_info.css.main);
|
||||
page.parts.forEach(part => {
|
||||
if (!part) return;
|
||||
const css_chunks_for_part = build_info.css.chunks[part.file];
|
||||
|
||||
if (css_chunks_for_part) {
|
||||
css_chunks_for_part.forEach(file => {
|
||||
css_chunks.add(file);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
styles = Array.from(css_chunks)
|
||||
.map(href => `<link rel="stylesheet" href="client/${href}">`)
|
||||
.join('')
|
||||
} else {
|
||||
styles = (css && css.code ? `<style>${css.code}</style>` : '');
|
||||
}
|
||||
|
||||
// users can set a CSP nonce using res.locals.nonce
|
||||
const nonce_attr = (res.locals && res.locals.nonce) ? ` nonce="${res.locals.nonce}"` : '';
|
||||
|
||||
const body = template()
|
||||
.replace('%sapper.base%', () => `<base href="${req.baseUrl}/">`)
|
||||
.replace('%sapper.scripts%', () => `<script${nonce_attr}>${script}</script>`)
|
||||
.replace('%sapper.html%', () => html)
|
||||
.replace('%sapper.head%', () => `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`)
|
||||
.replace('%sapper.styles%', () => styles);
|
||||
|
||||
res.statusCode = status;
|
||||
res.end(body);
|
||||
}).catch(err => {
|
||||
if (error) {
|
||||
// we encountered an error while rendering the error page — oops
|
||||
res.statusCode = 500;
|
||||
res.end(`<pre>${escape_html(err.message)}</pre>`);
|
||||
} else {
|
||||
handle_error(req, res, 500, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return function find_route(req: Req, res: Res, next: () => void) {
|
||||
if (req[IGNORE]) return next();
|
||||
|
||||
if (!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 read_template(dir = build_dir) {
|
||||
return fs.readFileSync(`${dir}/template.html`, 'utf-8');
|
||||
}
|
||||
|
||||
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]};`);
|
||||
}
|
||||
78
templates/src/server/middleware/get_server_route_handler.ts
Normal file
78
templates/src/server/middleware/get_server_route_handler.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { IGNORE } from '../placeholders';
|
||||
import { Req, Res, ServerRoute } from './types';
|
||||
|
||||
export function get_server_route_handler(routes: ServerRoute[]) {
|
||||
function handle_route(route: ServerRoute, req: Req, res: Res, next: () => void) {
|
||||
req.params = route.params(route.pattern.exec(req.path));
|
||||
|
||||
const method = req.method.toLowerCase();
|
||||
// 'delete' cannot be exported from a module because it is a keyword,
|
||||
// so check for 'del' instead
|
||||
const method_export = method === 'delete' ? 'del' : method;
|
||||
const handle_method = route.handlers[method_export];
|
||||
if (handle_method) {
|
||||
if (process.env.SAPPER_EXPORT) {
|
||||
const { write, end, setHeader } = res;
|
||||
const chunks: any[] = [];
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
// intercept data so that it can be exported
|
||||
res.write = function(chunk: any) {
|
||||
chunks.push(Buffer.from(chunk));
|
||||
write.apply(res, arguments);
|
||||
};
|
||||
|
||||
res.setHeader = function(name: string, value: string) {
|
||||
headers[name.toLowerCase()] = value;
|
||||
setHeader.apply(res, arguments);
|
||||
};
|
||||
|
||||
res.end = function(chunk?: any) {
|
||||
if (chunk) chunks.push(Buffer.from(chunk));
|
||||
end.apply(res, arguments);
|
||||
|
||||
process.send({
|
||||
__sapper__: true,
|
||||
event: 'file',
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
status: res.statusCode,
|
||||
type: headers['content-type'],
|
||||
body: Buffer.concat(chunks).toString()
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const handle_next = (err?: Error) => {
|
||||
if (err) {
|
||||
res.statusCode = 500;
|
||||
res.end(err.message);
|
||||
} else {
|
||||
process.nextTick(next);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
handle_method(req, res, handle_next);
|
||||
} catch (err) {
|
||||
handle_next(err);
|
||||
}
|
||||
} else {
|
||||
// no matching handler for method
|
||||
process.nextTick(next);
|
||||
}
|
||||
}
|
||||
|
||||
return function find_route(req: Req, res: Res, next: () => void) {
|
||||
if (req[IGNORE]) return next();
|
||||
|
||||
for (const route of routes) {
|
||||
if (route.pattern.test(req.path)) {
|
||||
handle_route(route, req, res, next);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
143
templates/src/server/middleware/index.ts
Normal file
143
templates/src/server/middleware/index.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { build_dir, dev, manifest, IGNORE } from '../placeholders';
|
||||
import { Handler, Req, Res, Store } from './types';
|
||||
import { get_server_route_handler } from './get_server_route_handler';
|
||||
import { get_page_handler } from './get_page_handler';
|
||||
import { lookup } from './mime';
|
||||
|
||||
export default function middleware(opts: {
|
||||
store?: (req: Req, res: Res) => Store,
|
||||
ignore?: any
|
||||
} = {}) {
|
||||
const { store, ignore } = opts;
|
||||
|
||||
let emitted_basepath = false;
|
||||
|
||||
return compose_handlers([
|
||||
ignore && ((req: Req, res: Res, next: () => void) => {
|
||||
req[IGNORE] = should_ignore(req.path, ignore);
|
||||
next();
|
||||
}),
|
||||
|
||||
(req: Req, res: Res, next: () => void) => {
|
||||
if (req[IGNORE]) return next();
|
||||
|
||||
if (req.baseUrl === undefined) {
|
||||
let { originalUrl } = req;
|
||||
if (req.url === '/' && originalUrl[originalUrl.length - 1] !== '/') {
|
||||
originalUrl += '/';
|
||||
}
|
||||
|
||||
req.baseUrl = originalUrl
|
||||
? originalUrl.slice(0, -req.url.length)
|
||||
: '';
|
||||
}
|
||||
|
||||
if (!emitted_basepath && process.send) {
|
||||
process.send({
|
||||
__sapper__: true,
|
||||
event: 'basepath',
|
||||
basepath: req.baseUrl
|
||||
});
|
||||
|
||||
emitted_basepath = true;
|
||||
}
|
||||
|
||||
if (req.path === undefined) {
|
||||
req.path = req.url.replace(/\?.*/, '');
|
||||
}
|
||||
|
||||
next();
|
||||
},
|
||||
|
||||
fs.existsSync(path.join(build_dir, 'index.html')) && serve({
|
||||
pathname: '/index.html',
|
||||
cache_control: dev ? 'no-cache' : 'max-age=600'
|
||||
}),
|
||||
|
||||
fs.existsSync(path.join(build_dir, 'service-worker.js')) && serve({
|
||||
pathname: '/service-worker.js',
|
||||
cache_control: 'no-cache, no-store, must-revalidate'
|
||||
}),
|
||||
|
||||
fs.existsSync(path.join(build_dir, 'service-worker.js.map')) && serve({
|
||||
pathname: '/service-worker.js.map',
|
||||
cache_control: 'no-cache, no-store, must-revalidate'
|
||||
}),
|
||||
|
||||
serve({
|
||||
prefix: '/client/',
|
||||
cache_control: dev ? 'no-cache' : 'max-age=31536000, immutable'
|
||||
}),
|
||||
|
||||
get_server_route_handler(manifest.server_routes),
|
||||
|
||||
get_page_handler(manifest, store)
|
||||
].filter(Boolean));
|
||||
}
|
||||
|
||||
export function compose_handlers(handlers: Handler[]) {
|
||||
return (req: Req, res: Res, next: () => void) => {
|
||||
let i = 0;
|
||||
function go() {
|
||||
const handler = handlers[i];
|
||||
|
||||
if (handler) {
|
||||
handler(req, res, () => {
|
||||
i += 1;
|
||||
go();
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
go();
|
||||
};
|
||||
}
|
||||
|
||||
export function should_ignore(uri: string, val: any) {
|
||||
if (Array.isArray(val)) return val.some(x => should_ignore(uri, x));
|
||||
if (val instanceof RegExp) return val.test(uri);
|
||||
if (typeof val === 'function') return val(uri);
|
||||
return uri.startsWith(val.charCodeAt(0) === 47 ? val : `/${val}`);
|
||||
}
|
||||
|
||||
export function serve({ prefix, pathname, cache_control }: {
|
||||
prefix?: string,
|
||||
pathname?: string,
|
||||
cache_control: string
|
||||
}) {
|
||||
const filter = pathname
|
||||
? (req: Req) => req.path === pathname
|
||||
: (req: Req) => req.path.startsWith(prefix);
|
||||
|
||||
const cache: Map<string, Buffer> = new Map();
|
||||
|
||||
const read = dev
|
||||
? (file: string) => fs.readFileSync(path.resolve(build_dir, file))
|
||||
: (file: string) => (cache.has(file) ? cache : cache.set(file, fs.readFileSync(path.resolve(build_dir, file)))).get(file)
|
||||
|
||||
return (req: Req, res: Res, next: () => void) => {
|
||||
if (req[IGNORE]) return next();
|
||||
|
||||
if (filter(req)) {
|
||||
const type = lookup(req.path);
|
||||
|
||||
try {
|
||||
const file = decodeURIComponent(req.path.slice(1));
|
||||
const data = read(file);
|
||||
|
||||
res.setHeader('Content-Type', type);
|
||||
res.setHeader('Cache-Control', cache_control);
|
||||
res.end(data);
|
||||
} catch (err) {
|
||||
res.statusCode = 404;
|
||||
res.end('not found');
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
}
|
||||
767
templates/src/server/middleware/mime-types.md
Normal file
767
templates/src/server/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
templates/src/server/middleware/mime.ts
Normal file
20
templates/src/server/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]);
|
||||
}
|
||||
69
templates/src/server/middleware/types.ts
Normal file
69
templates/src/server/middleware/types.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { ClientRequest, ServerResponse } from 'http';
|
||||
|
||||
export type ServerRoute = {
|
||||
pattern: RegExp;
|
||||
handlers: Record<string, Handler>;
|
||||
params: (match: RegExpMatchArray) => Record<string, string>;
|
||||
};
|
||||
|
||||
export type Page = {
|
||||
pattern: RegExp;
|
||||
parts: Array<{
|
||||
name: string;
|
||||
component: Component;
|
||||
params?: (match: RegExpMatchArray) => Record<string, string>;
|
||||
}>
|
||||
};
|
||||
|
||||
export type Manifest = {
|
||||
server_routes: ServerRoute[];
|
||||
pages: Page[];
|
||||
root: Component;
|
||||
error: Component;
|
||||
}
|
||||
|
||||
export type Handler = (req: Req, res: Res, next: () => void) => void;
|
||||
|
||||
export type Store = {
|
||||
get: () => any
|
||||
};
|
||||
|
||||
export 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;
|
||||
};
|
||||
|
||||
export interface Req extends ClientRequest {
|
||||
url: string;
|
||||
baseUrl: string;
|
||||
originalUrl: string;
|
||||
method: string;
|
||||
path: string;
|
||||
params: Record<string, string>;
|
||||
query: Record<string, string>;
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface Res extends ServerResponse {
|
||||
write: (data: any) => void;
|
||||
}
|
||||
|
||||
export { ServerResponse };
|
||||
|
||||
interface Component {
|
||||
render: (data: any, opts: { store: Store }) => {
|
||||
head: string;
|
||||
css: { code: string, map: any };
|
||||
html: string
|
||||
},
|
||||
preload: (data: any) => any | Promise<any>
|
||||
}
|
||||
11
templates/src/server/placeholders.ts
Normal file
11
templates/src/server/placeholders.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Manifest } from './types';
|
||||
|
||||
export const manifest: Manifest = __MANIFEST__;
|
||||
|
||||
export const build_dir = __BUILD__DIR__;
|
||||
|
||||
export const src_dir = __SRC__DIR__;
|
||||
|
||||
export const dev = __DEV__;
|
||||
|
||||
export const IGNORE = '__SAPPER__IGNORE__';
|
||||
7
test/app/.gitignore
vendored
Normal file
7
test/app/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
.sapper
|
||||
yarn.lock
|
||||
cypress/screenshots
|
||||
templates/.*
|
||||
dist
|
||||
81
test/app/README.md
Normal file
81
test/app/README.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# sapper-template
|
||||
|
||||
The default [Sapper](https://github.com/sveltejs/sapper) template. To clone it and get started:
|
||||
|
||||
```bash
|
||||
npx degit sveltejs/sapper-template my-app
|
||||
cd my-app
|
||||
npm install # or yarn!
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open up [localhost:3000](http://localhost:3000) and start clicking around.
|
||||
|
||||
|
||||
## Structure
|
||||
|
||||
Sapper expects to find three directories in the root of your project — `assets`, `routes` and `templates`.
|
||||
|
||||
|
||||
### assets
|
||||
|
||||
The [assets](assets) directory contains any static assets that should be available. These are served using [serve-static](https://github.com/expressjs/serve-static).
|
||||
|
||||
In your [service-worker.js](templates/service-worker.js) file, Sapper makes these files available as `__assets__` so that you can cache them (though you can choose not to, for example if you don't want to cache very large files).
|
||||
|
||||
|
||||
### routes
|
||||
|
||||
This is the heart of your Sapper app. There are two kinds of routes — *pages*, and *server routes*.
|
||||
|
||||
**Pages** are Svelte components written in `.html` files. When a user first visits the application, they will be served a server-rendered version of the route in question, plus some JavaScript that 'hydrates' the page and initialises a client-side router. From that point forward, navigating to other pages is handled entirely on the client for a fast, app-like feel. (Sapper will preload and cache the code for these subsequent pages, so that navigation is instantaneous.)
|
||||
|
||||
**Server routes** are modules written in `.js` files, that export functions corresponding to HTTP methods. Each function receives Express `request` and `response` objects as arguments, plus a `next` function. This is useful for creating a JSON API, for example.
|
||||
|
||||
There are three simple rules for naming the files that define your routes:
|
||||
|
||||
* A file called `routes/about.html` corresponds to the `/about` route. A file called `routes/blog/[slug].html` corresponds to the `/blog/:slug` route, in which case `params.slug` is available to the route
|
||||
* The file `routes/index.html` (or `routes/index.js`) corresponds to the root of your app. `routes/about/index.html` is treated the same as `routes/about.html`.
|
||||
* Files and directories with a leading underscore do *not* create routes. This allows you to colocate helper modules and components with the routes that depend on them — for example you could have a file called `routes/_helpers/datetime.js` and it would *not* create a `/_helpers/datetime` route
|
||||
|
||||
|
||||
### templates
|
||||
|
||||
This directory should contain the following files at a minimum:
|
||||
|
||||
* [2xx.html](templates/2xx.html) — a template for the page to serve for valid requests
|
||||
* [4xx.html](templates/4xx.html) — a template for 4xx-range errors (such as 404 Not Found)
|
||||
* [5xx.html](templates/5xx.html) — a template for 5xx-range errors (such as 500 Internal Server Error)
|
||||
* [main.js](templates/main.js) — this module initialises Sapper
|
||||
* [service-worker.js](templates/service-worker.js) — your app's service worker
|
||||
|
||||
Inside the HTML templates, Sapper will inject various values as indicated by `%sapper.xxxx%` tags. Inside JavaScript files, Sapper will replace strings like `__dev__` with the appropriate value.
|
||||
|
||||
In lieu of documentation (bear with us), consult the files to see what variables are available and how they're used.
|
||||
|
||||
|
||||
## Webpack config
|
||||
|
||||
Sapper uses webpack to provide code-splitting, dynamic imports and hot module reloading, as well as compiling your Svelte components. As long as you don't do anything daft, you can edit the configuration files to add whatever loaders and plugins you'd like.
|
||||
|
||||
|
||||
## Production mode and deployment
|
||||
|
||||
To start a production version of your app, run `npm start`. This will disable hot module replacement, and activate the appropriate webpack plugins.
|
||||
|
||||
You can deploy your application to any environment that supports Node 8 or above. As an example, to deploy to [Now](https://zeit.co/now), run these commands:
|
||||
|
||||
```bash
|
||||
npm install -g now
|
||||
now
|
||||
```
|
||||
|
||||
|
||||
## Bugs and feedback
|
||||
|
||||
Sapper is in early development, and may have the odd rough edge here and there. Please be vocal over on the [Sapper issue tracker](https://github.com/sveltejs/sapper/issues).
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[LIL](LICENSE)
|
||||
12
test/app/src/client.js
Normal file
12
test/app/src/client.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Store } from 'svelte/store.js';
|
||||
import * as sapper from '../__sapper__/client.js';
|
||||
|
||||
window.init = () => {
|
||||
return sapper.start({
|
||||
target: document.querySelector('#sapper'),
|
||||
store: data => new Store(data)
|
||||
});
|
||||
};
|
||||
|
||||
window.prefetchRoutes = sapper.prefetchRoutes;
|
||||
window.goto = sapper.goto;
|
||||
20
test/app/src/routes/[x]/[y]/[z].html
Normal file
20
test/app/src/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/src/routes/[x]/[y]/_layout.html
Normal file
22
test/app/src/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/src/routes/[x]/_counts.js
Normal file
5
test/app/src/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
|
||||
};
|
||||
6
test/app/src/routes/_error.html
Normal file
6
test/app/src/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/src/routes/_layout.html
Normal file
15
test/app/src/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>
|
||||
19
test/app/src/routes/about.html
Normal file
19
test/app/src/routes/about.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<svelte:head>
|
||||
<title>About</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>About this site</h1>
|
||||
|
||||
<p>This is the 'about' page. There's not much here.</p>
|
||||
|
||||
<button class='prefetch' on:click='prefetch("blog/why-the-name")'>Why the name?</button>
|
||||
|
||||
<script>
|
||||
import { prefetch } from '../../__sapper__/client.js';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
prefetch
|
||||
}
|
||||
};
|
||||
</script>
|
||||
9
test/app/src/routes/api/delete/[id].js
Normal file
9
test/app/src/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
|
||||
}));
|
||||
}
|
||||
36
test/app/src/routes/blog/[slug].html
Normal file
36
test/app/src/routes/blog/[slug].html
Normal file
@@ -0,0 +1,36 @@
|
||||
<svelte:head>
|
||||
<title>{post.title}</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>{post.title}</h1>
|
||||
|
||||
<div class='content'>
|
||||
{@html post.html}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload({ params, query }) {
|
||||
// the `slug` parameter is available because this file
|
||||
// is called [slug].html
|
||||
const { slug } = params;
|
||||
|
||||
if (slug === 'throw-an-error') {
|
||||
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');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
23
test/app/src/routes/blog/[slug].json.js
Normal file
23
test/app/src/routes/blog/[slug].json.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import posts from './_posts.js';
|
||||
|
||||
const lookup = {};
|
||||
posts.forEach(post => {
|
||||
lookup[post.slug] = JSON.stringify(post);
|
||||
});
|
||||
|
||||
export function get(req, res, next) {
|
||||
// the `slug` parameter is available because this file
|
||||
// is called [slug].js
|
||||
const { slug } = req.params;
|
||||
|
||||
if (slug in lookup) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': `no-cache`
|
||||
});
|
||||
|
||||
res.end(lookup[slug]);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
124
test/app/src/routes/blog/_posts.js
Normal file
124
test/app/src/routes/blog/_posts.js
Normal file
@@ -0,0 +1,124 @@
|
||||
// Ordinarily, you'd generate this data from markdown files in your
|
||||
// repo, or fetch them from a database of some kind. But in order to
|
||||
// avoid unnecessary dependencies in the starter template, and in the
|
||||
// service of obviousness, we're just going to leave it here.
|
||||
|
||||
// This file is called `_posts.js` rather than `posts.js`, because
|
||||
// we don't want to create an `/api/blog/posts` route — the leading
|
||||
// underscore tells Sapper not to do that.
|
||||
|
||||
const posts = [
|
||||
{
|
||||
title: 'What is Sapper?',
|
||||
slug: 'what-is-sapper',
|
||||
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>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>
|
||||
<li>Code-splitting, dynamic imports and hot module replacement, powered by webpack</li>
|
||||
<li>Server-side rendering (SSR) with client-side hydration</li>
|
||||
<li>Service worker for offline support, and all the PWA bells and whistles</li>
|
||||
<li>The nicest development experience you've ever had, or your money back</li>
|
||||
</ul>
|
||||
|
||||
<p>It's implemented as Express middleware. Everything is set up and waiting for you to get started, but you keep complete control over the server, service worker, webpack config and everything else, so it's as flexible as you need it to be.</p>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
title: 'How to use Sapper',
|
||||
slug: 'how-to-use-sapper',
|
||||
html: `
|
||||
<h2>Step one</h2>
|
||||
<p>Create a new project, using <a href='https://github.com/Rich-Harris/degit'>degit</a>:</p>
|
||||
|
||||
<pre><code>npx degit sveltejs/sapper-template my-app
|
||||
cd my-app
|
||||
npm install # or yarn!
|
||||
npm run dev
|
||||
</code></pre>
|
||||
|
||||
<h2>Step two</h2>
|
||||
<p>Go to <a href='http://localhost:3000'>localhost:3000</a>. Open <code>my-app</code> in your editor. Edit the files in the <code>routes</code> directory or add new ones.</p>
|
||||
|
||||
<h2>Step three</h2>
|
||||
<p>...</p>
|
||||
|
||||
<h2>Step four</h2>
|
||||
<p>Resist overdone joke formats.</p>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Why the name?',
|
||||
slug: 'why-the-name',
|
||||
html: `
|
||||
<p>In war, the soldiers who build bridges, repair roads, clear minefields and conduct demolitions — all under combat conditions — are known as <em>sappers</em>.</p>
|
||||
|
||||
<p>For web developers, the stakes are generally lower than those for combat engineers. But we face our own hostile environment: underpowered devices, poor network connections, and the complexity inherent in front-end engineering. Sapper, which is short for <strong>S</strong>velte <strong>app</strong> mak<strong>er</strong>, is your courageous and dutiful ally.</p>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
title: 'How is Sapper different from Next.js?',
|
||||
slug: 'how-is-sapper-different-from-next',
|
||||
html: `
|
||||
<p><a href='https://github.com/zeit/next.js/'>Next.js</a> is a React framework from <a href='https://zeit.co'>Zeit</a>, and is the inspiration for Sapper. There are a few notable differences, however:</p>
|
||||
|
||||
<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>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 <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>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
title: 'How can I get involved?',
|
||||
slug: 'how-can-i-get-involved',
|
||||
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>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
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>
|
||||
`
|
||||
}
|
||||
];
|
||||
|
||||
posts.forEach(post => {
|
||||
post.html = post.html.replace(/^\t{3}/gm, '');
|
||||
});
|
||||
|
||||
export default posts;
|
||||
25
test/app/src/routes/blog/index.html
Normal file
25
test/app/src/routes/blog/index.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<svelte:head>
|
||||
<title>Blog</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>Recent posts</h1>
|
||||
|
||||
<ul>
|
||||
{#each posts as post}
|
||||
<!-- we're using the non-standard `rel=prefetch` attribute to
|
||||
tell Sapper to load the data for the page as soon as
|
||||
the user hovers over the link or taps it, instead of
|
||||
waiting for the 'click' event -->
|
||||
<li><a rel='prefetch' href='blog/{post.slug}'>{post.title}</a></li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload({ params, query }) {
|
||||
return fetch(`blog.json`).then(r => r.json()).then(posts => {
|
||||
return { posts };
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
17
test/app/src/routes/blog/index.json.js
Normal file
17
test/app/src/routes/blog/index.json.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import posts from './_posts.js';
|
||||
|
||||
const contents = JSON.stringify(posts.map(post => {
|
||||
return {
|
||||
title: post.title,
|
||||
slug: post.slug
|
||||
};
|
||||
}));
|
||||
|
||||
export function get(req, res) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': `max-age=${30 * 60 * 1e3}` // cache for 30 minutes
|
||||
});
|
||||
|
||||
res.end(contents);
|
||||
}
|
||||
1
test/app/src/routes/const.html
Normal file
1
test/app/src/routes/const.html
Normal file
@@ -0,0 +1 @@
|
||||
<h1>reserved words are okay as routes</h1>
|
||||
11
test/app/src/routes/credentials/index.html
Normal file
11
test/app/src/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/src/routes/credentials/test.json.js
Normal file
28
test/app/src/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/src/routes/delete-test.html
Normal file
15
test/app/src/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/src/routes/echo/page/[slug].html
Normal file
12
test/app/src/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/src/routes/echo/server-route/[slug].js
Normal file
15
test/app/src/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/src/routes/fünke.html
Normal file
11
test/app/src/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/src/routes/fünke.json.js
Normal file
9
test/app/src/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"
|
||||
));
|
||||
}
|
||||
31
test/app/src/routes/index.html
Normal file
31
test/app/src/routes/index.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<svelte:head>
|
||||
<title>Sapper project template</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>Great success!</h1>
|
||||
|
||||
<a href='.'>home</a>
|
||||
<a href='about'>about</a>
|
||||
<a href='slow-preload'>slow preload</a>
|
||||
<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>
|
||||
|
||||
<div class='hydrate-test'></div>
|
||||
|
||||
<style>
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 2.8em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
</style>
|
||||
1
test/app/src/routes/missing-index/ok.html
Normal file
1
test/app/src/routes/missing-index/ok.html
Normal file
@@ -0,0 +1 @@
|
||||
<h1>it works</h1>
|
||||
1
test/app/src/routes/non-sapper-redirect-to.html
Normal file
1
test/app/src/routes/non-sapper-redirect-to.html
Normal file
@@ -0,0 +1 @@
|
||||
<h1>redirected</h1>
|
||||
1
test/app/src/routes/preload-root/index.html
Normal file
1
test/app/src/routes/preload-root/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<h1>root preload function ran: {rootPreloadFunctionRan}</h1>
|
||||
17
test/app/src/routes/preload-values/custom-class.html
Normal file
17
test/app/src/routes/preload-values/custom-class.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<h1>{foo.bar()}</h1>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload() {
|
||||
class Foo {
|
||||
bar() {
|
||||
return 42;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
foo: new Foo()
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
1
test/app/src/routes/preload-values/index.html
Normal file
1
test/app/src/routes/preload-values/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<svelte:component this={child.component} {...child.props}/>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user