mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-13 19:45:26 +00:00
Compare commits
442 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"rules": {
|
||||
"semi": [ 2, "always" ],
|
||||
"space-before-blocks": [ 2, "always" ],
|
||||
"no-mixed-spaces-and-tabs": [ 2, "smart-tabs" ],
|
||||
"no-cond-assign": 0,
|
||||
"no-unused-vars": 2,
|
||||
"object-shorthand": [ 2, "always" ],
|
||||
"no-const-assign": 2,
|
||||
"no-class-assign": 2,
|
||||
"no-this-before-super": 2,
|
||||
"no-var": 2,
|
||||
"no-unreachable": 2,
|
||||
"valid-typeof": 2,
|
||||
"quote-props": [ 2, "as-needed" ],
|
||||
"one-var": [ 2, "never" ],
|
||||
"prefer-arrow-callback": 2,
|
||||
"prefer-const": [ 2, { "destructuring": "all" } ],
|
||||
"arrow-spacing": 2,
|
||||
"no-inner-declarations": 0
|
||||
},
|
||||
"env": {
|
||||
"es6": true,
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"mocha": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 8,
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,4 +1,13 @@
|
||||
.DS_Store
|
||||
yarn.lock
|
||||
yarn-error.log
|
||||
node_modules
|
||||
cypress/screenshots
|
||||
test/app/.sapper
|
||||
test/app/.sapper
|
||||
test/app/app/manifest
|
||||
test/app/export
|
||||
test/app/build
|
||||
sapper
|
||||
runtime.js
|
||||
dist
|
||||
!rollup.config.js
|
||||
@@ -3,6 +3,7 @@ sudo: false
|
||||
language: node_js
|
||||
|
||||
node_js:
|
||||
- "6"
|
||||
- "stable"
|
||||
|
||||
env:
|
||||
@@ -18,4 +19,3 @@ install:
|
||||
- export DISPLAY=':99.0'
|
||||
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||
- npm install
|
||||
- (cd test/app && npm install)
|
||||
|
||||
270
CHANGELOG.md
270
CHANGELOG.md
@@ -1,5 +1,275 @@
|
||||
# sapper changelog
|
||||
|
||||
## 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))
|
||||
|
||||
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.
|
||||
151
README.md
151
README.md
@@ -1,150 +1,37 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
|
||||
## Client-side app
|
||||
## License
|
||||
|
||||
Sapper will create (and in development mode, update) a barebones `main.js` file that dynamically imports individual routes and renders them — something like this:
|
||||
|
||||
```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);
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
|
||||
## Things to figure out
|
||||
|
||||
* 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.
|
||||
[LIL](LICENSE)
|
||||
@@ -10,8 +10,12 @@ build: off
|
||||
environment:
|
||||
matrix:
|
||||
# node.js
|
||||
- nodejs_version: stable
|
||||
- nodejs_version: 10.5
|
||||
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version
|
||||
- npm install
|
||||
|
||||
test_script:
|
||||
- node --version && npm --version
|
||||
- npm test
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const cmd = process.argv[2];
|
||||
|
||||
if (cmd === 'build') {
|
||||
process.env.NODE_ENV = 'production';
|
||||
require('../lib/build.js')();
|
||||
}
|
||||
1
components/default-layout.html
Normal file
1
components/default-layout.html
Normal file
@@ -0,0 +1 @@
|
||||
<svelte:component this={child.component} {...child.props}/>
|
||||
42
lib/build.js
42
lib/build.js
@@ -1,42 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const mkdirp = require('mkdirp');
|
||||
const rimraf = require('rimraf');
|
||||
const { client, server } = require('./utils/compilers.js');
|
||||
const create_app = require('./utils/create_app.js');
|
||||
const generate_asset_cache = require('./utils/generate_asset_cache.js');
|
||||
const { dest } = require('./config.js');
|
||||
|
||||
module.exports = () => {
|
||||
mkdirp.sync(dest);
|
||||
rimraf.sync(path.join(dest, '**/*'));
|
||||
|
||||
// create main.js and server-routes.js
|
||||
create_app();
|
||||
|
||||
function handleErrors(err, stats) {
|
||||
if (err) {
|
||||
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (stats.hasErrors()) {
|
||||
console.log(stats.toString({ colors: true }));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
client.run((err, clientStats) => {
|
||||
handleErrors(err, clientStats);
|
||||
const clientInfo = clientStats.toJson();
|
||||
fs.writeFileSync(path.join(dest, 'stats.client.json'), JSON.stringify(clientInfo, null, ' '));
|
||||
|
||||
server.run((err, serverStats) => {
|
||||
handleErrors(err, serverStats);
|
||||
const serverInfo = serverStats.toJson();
|
||||
fs.writeFileSync(path.join(dest, 'stats.server.json'), JSON.stringify(serverInfo, null, ' '));
|
||||
|
||||
generate_asset_cache(clientInfo, serverInfo);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
const path = require('path');
|
||||
const mkdirp = require('mkdirp');
|
||||
const rimraf = require('rimraf');
|
||||
|
||||
exports.dev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
exports.templates = path.resolve(process.env.SAPPER_TEMPLATES || 'templates');
|
||||
|
||||
exports.src = path.resolve(process.env.SAPPER_ROUTES || 'routes');
|
||||
|
||||
exports.dest = path.resolve(process.env.SAPPER_DEST || '.sapper');
|
||||
|
||||
if (exports.dev) {
|
||||
mkdirp.sync(exports.dest);
|
||||
rimraf.sync(path.join(exports.dest, '**/*'));
|
||||
}
|
||||
|
||||
exports.entry = {
|
||||
client: path.resolve(exports.templates, '.main.rendered.js'),
|
||||
server: path.resolve(exports.dest, 'server-entry.js')
|
||||
};
|
||||
238
lib/index.js
238
lib/index.js
@@ -1,238 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const route_manager = require('./route_manager.js');
|
||||
const templates = require('./templates.js');
|
||||
const create_app = require('./utils/create_app.js');
|
||||
const create_watcher = require('./utils/create_watcher.js');
|
||||
const compilers = require('./utils/compilers.js');
|
||||
const generate_asset_cache = require('./utils/generate_asset_cache.js');
|
||||
const escape_html = require('escape-html');
|
||||
const { dest, dev } = require('./config.js');
|
||||
|
||||
function connect_dev() {
|
||||
create_app();
|
||||
|
||||
const watcher = create_watcher();
|
||||
|
||||
let asset_cache;
|
||||
|
||||
const middleware = compose_handlers([
|
||||
require('webpack-hot-middleware')(compilers.client, {
|
||||
reload: true,
|
||||
path: '/__webpack_hmr',
|
||||
heartbeat: 10 * 1000
|
||||
}),
|
||||
|
||||
async (req, res, next) => {
|
||||
asset_cache = await watcher.ready;
|
||||
next();
|
||||
},
|
||||
|
||||
set_req_pathname,
|
||||
|
||||
get_asset_handler({
|
||||
filter: pathname => pathname === '/index.html',
|
||||
type: 'text/html',
|
||||
cache: 'max-age=600',
|
||||
fn: () => asset_cache.client.index
|
||||
}),
|
||||
|
||||
get_asset_handler({
|
||||
filter: pathname => pathname === '/service-worker.js',
|
||||
type: 'application/javascript',
|
||||
cache: 'max-age=600',
|
||||
fn: () => asset_cache.client.service_worker
|
||||
}),
|
||||
|
||||
get_asset_handler({
|
||||
filter: pathname => pathname.startsWith('/client/'),
|
||||
type: 'application/javascript',
|
||||
cache: 'max-age=31536000',
|
||||
fn: pathname => asset_cache.client.chunks[pathname]
|
||||
}),
|
||||
|
||||
get_route_handler(() => asset_cache),
|
||||
|
||||
get_not_found_handler(() => asset_cache)
|
||||
]);
|
||||
|
||||
middleware.close = () => {
|
||||
watcher.close();
|
||||
// TODO shut down chokidar
|
||||
};
|
||||
|
||||
return middleware;
|
||||
}
|
||||
|
||||
function connect_prod() {
|
||||
const asset_cache = generate_asset_cache(
|
||||
read_json(path.join(dest, 'stats.client.json')),
|
||||
read_json(path.join(dest, 'stats.server.json'))
|
||||
);
|
||||
|
||||
const middleware = compose_handlers([
|
||||
set_req_pathname,
|
||||
|
||||
get_asset_handler({
|
||||
filter: pathname => pathname === '/index.html',
|
||||
type: 'text/html',
|
||||
cache: 'max-age=600',
|
||||
fn: () => asset_cache.client.index
|
||||
}),
|
||||
|
||||
get_asset_handler({
|
||||
filter: pathname => pathname === '/service-worker.js',
|
||||
type: 'application/javascript',
|
||||
cache: 'max-age=600',
|
||||
fn: () => asset_cache.client.service_worker
|
||||
}),
|
||||
|
||||
get_asset_handler({
|
||||
filter: pathname => pathname.startsWith('/client/'),
|
||||
type: 'application/javascript',
|
||||
cache: 'max-age=31536000',
|
||||
fn: pathname => asset_cache.client.chunks[pathname]
|
||||
}),
|
||||
|
||||
get_route_handler(() => asset_cache),
|
||||
|
||||
get_not_found_handler(() => asset_cache)
|
||||
]);
|
||||
|
||||
// here for API consistency between dev, and prod, but
|
||||
// doesn't actually need to do anything
|
||||
middleware.close = () => {};
|
||||
|
||||
return middleware;
|
||||
}
|
||||
|
||||
module.exports = dev ? connect_dev : connect_prod;
|
||||
|
||||
function set_req_pathname(req, res, next) {
|
||||
req.pathname = req.url.replace(/\?.+/, '');
|
||||
next();
|
||||
}
|
||||
|
||||
function get_asset_handler(opts) {
|
||||
return (req, res, next) => {
|
||||
if (!opts.filter(req.pathname)) return next();
|
||||
|
||||
res.set({
|
||||
'Content-Type': opts.type,
|
||||
'Cache-Control': opts.cache
|
||||
});
|
||||
res.end(opts.fn(req.pathname));
|
||||
};
|
||||
}
|
||||
|
||||
function get_route_handler(fn) {
|
||||
return async function handle_route(req, res, next) {
|
||||
const url = req.pathname;
|
||||
|
||||
const { client, server } = fn();
|
||||
|
||||
// whatever happens, we're going to serve some HTML
|
||||
res.set({
|
||||
'Content-Type': 'text/html'
|
||||
});
|
||||
|
||||
try {
|
||||
for (const route of route_manager.routes) {
|
||||
if (route.test(url)) {
|
||||
req.params = route.exec(url);
|
||||
|
||||
const mod = require(server.entry)[route.id];
|
||||
|
||||
if (route.type === 'page') {
|
||||
// preload main.js and current route
|
||||
// TODO detect other stuff we can preload? images, CSS, fonts?
|
||||
res.set('Link', `<${client.main_file}>;rel="preload";as="script", <${client.routes[route.id]}>;rel="preload";as="script"`);
|
||||
|
||||
const data = { params: req.params, query: req.query };
|
||||
|
||||
if (mod.preload) {
|
||||
const promise = Promise.resolve(mod.preload(data)).then(preloaded => {
|
||||
Object.assign(data, preloaded);
|
||||
return mod.render(data);
|
||||
});
|
||||
|
||||
await templates.stream(res, 200, {
|
||||
main: client.main_file,
|
||||
html: promise.then(rendered => rendered.html),
|
||||
head: promise.then(({ head }) => `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`),
|
||||
styles: promise.then(({ css }) => (css && css.code ? `<style>${css.code}</style>` : ''))
|
||||
});
|
||||
} else {
|
||||
const { html, head, css } = mod.render(data);
|
||||
|
||||
const page = templates.render(200, {
|
||||
main: client.main_file,
|
||||
html,
|
||||
head: `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`,
|
||||
styles: (css && css.code ? `<style>${css.code}</style>` : '')
|
||||
});
|
||||
|
||||
res.end(page);
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
const handler = mod[req.method.toLowerCase()];
|
||||
if (handler) handler(req, res, next);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
} catch(err) {
|
||||
res.status(500);
|
||||
res.end(templates.render(500, {
|
||||
title: (err && err.name) || 'Internal server error',
|
||||
url,
|
||||
error: escape_html(err && (err.details || err.message || err) || 'Unknown error'),
|
||||
stack: err && err.stack.split('\n').slice(1).join('\n')
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function get_not_found_handler(fn) {
|
||||
return function handle_not_found(req, res) {
|
||||
const asset_cache = fn();
|
||||
|
||||
res.status(404);
|
||||
res.end(templates.render(404, {
|
||||
title: 'Not found',
|
||||
status: 404,
|
||||
method: req.method,
|
||||
main: asset_cache.client.main_file,
|
||||
url: req.url
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
function compose_handlers(handlers) {
|
||||
return (req, res, next) => {
|
||||
let i = 0;
|
||||
function go() {
|
||||
const handler = handlers[i];
|
||||
|
||||
if (handler) {
|
||||
handler(req, res, () => {
|
||||
i += 1;
|
||||
go();
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
go();
|
||||
};
|
||||
}
|
||||
|
||||
function read_json(file) {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
const glob = require('glob');
|
||||
const chokidar = require('chokidar');
|
||||
const create_routes = require('./utils/create_routes.js');
|
||||
const { src, dev } = require('./config.js');
|
||||
|
||||
const callbacks = [];
|
||||
|
||||
exports.onchange = fn => {
|
||||
callbacks.push(fn);
|
||||
};
|
||||
|
||||
function update() {
|
||||
exports.routes = create_routes(
|
||||
glob.sync('**/*.+(html|js|mjs)', { cwd: src })
|
||||
);
|
||||
|
||||
callbacks.forEach(fn => fn());
|
||||
}
|
||||
|
||||
update();
|
||||
|
||||
if (dev) {
|
||||
const watcher = chokidar.watch(`${src}/**/*.+(html|js|mjs)`, {
|
||||
ignoreInitial: true,
|
||||
persistent: false
|
||||
});
|
||||
|
||||
watcher.on('add', update);
|
||||
watcher.on('change', update);
|
||||
watcher.on('unlink', update);
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const glob = require('glob');
|
||||
const chokidar = require('chokidar');
|
||||
const { dev } = require('./config.js');
|
||||
|
||||
let templates;
|
||||
|
||||
function create_templates() {
|
||||
templates = glob.sync('*.html', { cwd: 'templates' })
|
||||
.map(file => {
|
||||
const template = fs.readFileSync(`templates/${file}`, 'utf-8');
|
||||
const status = file.replace('.html', '').toLowerCase();
|
||||
|
||||
if (!/^[0-9x]{3}$/.test(status)) {
|
||||
throw new Error(`Bad template — should be a valid status code like 404.html, or a wildcard like 2xx.html`);
|
||||
}
|
||||
|
||||
const specificity = (
|
||||
(status[0] === 'x' ? 0 : 4) +
|
||||
(status[1] === 'x' ? 0 : 2) +
|
||||
(status[2] === 'x' ? 0 : 1)
|
||||
);
|
||||
|
||||
const pattern = new RegExp(`^${status.split('').map(d => d === 'x' ? '\\d' : d).join('')}$`);
|
||||
|
||||
return {
|
||||
test: status => pattern.test(status),
|
||||
specificity,
|
||||
render: data => {
|
||||
return template.replace(/%sapper\.(\w+)%/g, (match, key) => {
|
||||
return key in data ? data[key] : '';
|
||||
});
|
||||
},
|
||||
stream: async (res, data) => {
|
||||
let i = 0;
|
||||
|
||||
do {
|
||||
const start = template.indexOf('%sapper', i);
|
||||
|
||||
if (start === -1) {
|
||||
res.end(template.slice(i));
|
||||
return;
|
||||
}
|
||||
|
||||
res.write(template.slice(i, start));
|
||||
|
||||
const end = template.indexOf('%', start + 1);
|
||||
if (end === -1) {
|
||||
throw new Error(`Bad template`); // TODO validate ahead of time
|
||||
}
|
||||
|
||||
const tag = template.slice(start + 1, end);
|
||||
const match = /sapper\.(\w+)/.exec(tag);
|
||||
if (!match || !(match[1] in data)) throw new Error(`Bad template`); // TODO ditto
|
||||
|
||||
res.write(await data[match[1]]);
|
||||
i = end + 1;
|
||||
} while (i < template.length);
|
||||
}
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.specificity - a.specificity);
|
||||
}
|
||||
|
||||
create_templates();
|
||||
|
||||
if (dev) {
|
||||
const watcher = chokidar.watch('templates/**.html', {
|
||||
ignoreInitial: true,
|
||||
persistent: false
|
||||
});
|
||||
|
||||
watcher.on('add', create_templates);
|
||||
watcher.on('change', create_templates);
|
||||
watcher.on('unlink', create_templates);
|
||||
}
|
||||
|
||||
exports.render = (status, data) => {
|
||||
const template = templates.find(template => template.test(status));
|
||||
if (template) return template.render(data);
|
||||
|
||||
return `Missing template for status code ${status}`;
|
||||
};
|
||||
|
||||
exports.stream = (res, status, data) => {
|
||||
const template = templates.find(template => template.test(status));
|
||||
if (template) return template.stream(res, data);
|
||||
|
||||
return `Missing template for status code ${status}`;
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
const path = require('path');
|
||||
const relative = require('require-relative');
|
||||
const webpack = relative('webpack', process.cwd());
|
||||
|
||||
exports.client = webpack(
|
||||
require(path.resolve('webpack.client.config.js'))
|
||||
);
|
||||
|
||||
exports.server = webpack(
|
||||
require(path.resolve('webpack.server.config.js'))
|
||||
);
|
||||
@@ -1,83 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const chokidar = require('chokidar');
|
||||
const route_manager = require('../route_manager.js');
|
||||
const { src, entry, dev } = require('../config.js');
|
||||
|
||||
function posixify(file) {
|
||||
return file.replace(/[/\\]/g, '/');
|
||||
}
|
||||
|
||||
function create_app() {
|
||||
const { routes } = route_manager;
|
||||
|
||||
function create_client_main() {
|
||||
const template = fs.readFileSync('templates/main.js', 'utf-8');
|
||||
|
||||
const code = `[${
|
||||
routes
|
||||
.filter(route => route.type === 'page')
|
||||
.map(route => {
|
||||
const params = route.dynamic.length === 0 ?
|
||||
'{}' :
|
||||
`{ ${route.dynamic.map((part, i) => `${part}: match[${i + 1}]`).join(', ') } }`;
|
||||
|
||||
const file = posixify(`${src}/${route.file}`);
|
||||
return `{ pattern: ${route.pattern}, params: match => (${params}), load: () => import(/* webpackChunkName: "${route.id}" */ '${file}') }`
|
||||
})
|
||||
.join(', ')
|
||||
}]`;
|
||||
|
||||
let main = template
|
||||
.replace(/__app__/g, posixify(path.resolve(__dirname, '../../runtime/app.js')))
|
||||
.replace(/__routes__/g, code)
|
||||
.replace(/__dev__/g, String(dev));
|
||||
|
||||
if (dev) {
|
||||
const hmr_client = posixify(require.resolve(`webpack-hot-middleware/client`));
|
||||
main += `\n\nimport('${hmr_client}?path=/__webpack_hmr&timeout=20000'); if (module.hot) module.hot.accept();`
|
||||
}
|
||||
|
||||
fs.writeFileSync(entry.client, main);
|
||||
|
||||
// need to fudge the mtime, because webpack is soft in the head
|
||||
const { atime, mtime } = fs.statSync(entry.client);
|
||||
fs.utimesSync(entry.client, new Date(atime.getTime() - 999999), new Date(mtime.getTime() - 999999));
|
||||
}
|
||||
|
||||
function create_server_routes() {
|
||||
const imports = routes
|
||||
.map(route => {
|
||||
const file = posixify(`${src}/${route.file}`);
|
||||
return route.type === 'page' ?
|
||||
`import ${route.id} from '${file}';` :
|
||||
`import * as ${route.id} from '${file}';`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
const exports = `export { ${routes.map(route => route.id)} };`;
|
||||
|
||||
fs.writeFileSync(entry.server, `${imports}\n\n${exports}`);
|
||||
|
||||
const { atime, mtime } = fs.statSync(entry.server);
|
||||
fs.utimesSync(entry.server, new Date(atime.getTime() - 999999), new Date(mtime.getTime() - 999999));
|
||||
}
|
||||
|
||||
create_client_main();
|
||||
create_server_routes();
|
||||
}
|
||||
|
||||
if (dev) {
|
||||
route_manager.onchange(create_app);
|
||||
|
||||
const watcher = chokidar.watch(`templates/main.js`, {
|
||||
ignoreInitial: true,
|
||||
persistent: false
|
||||
});
|
||||
|
||||
watcher.on('add', create_app);
|
||||
watcher.on('change', create_app);
|
||||
watcher.on('unlink', create_app);
|
||||
}
|
||||
|
||||
module.exports = create_app;
|
||||
@@ -1,77 +0,0 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = function create_matchers(files) {
|
||||
const routes = files
|
||||
.map(file => {
|
||||
if (/(^|\/|\\)_/.test(file)) return;
|
||||
|
||||
const parts = file.replace(/\.(html|js|mjs)$/, '').split('/'); // glob output is always posix-style
|
||||
if (parts[parts.length - 1] === 'index') parts.pop();
|
||||
|
||||
const id = (
|
||||
parts.join('_').replace(/[[\]]/g, '$').replace(/^\d/, '_$&').replace(/[^a-zA-Z0-9_$]/g, '_')
|
||||
) || '_';
|
||||
|
||||
const dynamic = parts
|
||||
.filter(part => part[0] === '[')
|
||||
.map(part => part.slice(1, -1));
|
||||
|
||||
const pattern = new RegExp(
|
||||
`^\\/${parts.map(p => p[0] === '[' ? '([^/]+)' : p).join('\\/')}$`
|
||||
);
|
||||
|
||||
const test = url => pattern.test(url);
|
||||
|
||||
const exec = url => {
|
||||
const match = pattern.exec(url);
|
||||
if (!match) return;
|
||||
|
||||
const params = {};
|
||||
dynamic.forEach((param, i) => {
|
||||
params[param] = match[i + 1];
|
||||
});
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
return {
|
||||
id,
|
||||
type: path.extname(file) === '.html' ? 'page' : 'route',
|
||||
file,
|
||||
pattern,
|
||||
test,
|
||||
exec,
|
||||
parts,
|
||||
dynamic
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => {
|
||||
let same = true;
|
||||
|
||||
for (let i = 0; true; i += 1) {
|
||||
const a_part = a.parts[i];
|
||||
const b_part = b.parts[i];
|
||||
|
||||
if (!a_part && !b_part) {
|
||||
if (same) throw new Error(`The ${a.file} and ${b.file} routes clash`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!a_part) return 1;
|
||||
if (!b_part) return -1;
|
||||
|
||||
const a_is_dynamic = a_part[0] === '[';
|
||||
const b_is_dynamic = b_part[0] === '[';
|
||||
|
||||
if (a_is_dynamic === b_is_dynamic) {
|
||||
if (!a_is_dynamic) same = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
return a_is_dynamic ? 1 : -1;
|
||||
}
|
||||
});
|
||||
|
||||
return routes;
|
||||
};
|
||||
@@ -1,75 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const chalk = require('chalk');
|
||||
const compilers = require('./compilers.js');
|
||||
const generate_asset_cache = require('./generate_asset_cache.js');
|
||||
const { dest } = require('../config.js');
|
||||
|
||||
function deferred() {
|
||||
const d = {};
|
||||
|
||||
d.promise = new Promise((fulfil, reject) => {
|
||||
d.fulfil = fulfil;
|
||||
d.reject = reject;
|
||||
});
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
module.exports = function create_watcher() {
|
||||
const deferreds = {
|
||||
client: deferred(),
|
||||
server: deferred()
|
||||
};
|
||||
|
||||
const invalidate = () => Promise.all([
|
||||
deferreds.client.promise,
|
||||
deferreds.server.promise
|
||||
]).then(([client_stats, server_stats]) => {
|
||||
const client_info = client_stats.toJson();
|
||||
fs.writeFileSync(path.join(dest, 'stats.client.json'), JSON.stringify(client_info, null, ' '));
|
||||
|
||||
const server_info = server_stats.toJson();
|
||||
fs.writeFileSync(path.join(dest, 'stats.server.json'), JSON.stringify(server_info, null, ' '));
|
||||
|
||||
return generate_asset_cache(
|
||||
client_stats.toJson(),
|
||||
server_stats.toJson()
|
||||
);
|
||||
});
|
||||
|
||||
function watch_compiler(type) {
|
||||
const compiler = compilers[type];
|
||||
|
||||
compiler.plugin('invalid', filename => {
|
||||
console.log(chalk.cyan(`${type} bundle invalidated, file changed: ${chalk.bold(filename)}`));
|
||||
deferreds[type] = deferred();
|
||||
watcher.ready = invalidate();
|
||||
});
|
||||
|
||||
compiler.plugin('failed', err => {
|
||||
deferreds[type].reject(err);
|
||||
});
|
||||
|
||||
return compiler.watch({}, (err, stats) => {
|
||||
if (stats.hasErrors()) {
|
||||
deferreds[type].reject(stats.toJson().errors[0]);
|
||||
} else {
|
||||
deferreds[type].fulfil(stats);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const watcher = {
|
||||
ready: invalidate(),
|
||||
client: watch_compiler('client'),
|
||||
server: watch_compiler('server'),
|
||||
|
||||
close: () => {
|
||||
watcher.client.close();
|
||||
watcher.server.close();
|
||||
}
|
||||
};
|
||||
|
||||
return watcher;
|
||||
};
|
||||
@@ -1,79 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const glob = require('glob');
|
||||
const templates = require('../templates.js');
|
||||
const route_manager = require('../route_manager.js');
|
||||
const { dest, dev } = require('../config.js');
|
||||
|
||||
function ensure_array(thing) {
|
||||
return Array.isArray(thing) ? thing : [thing]; // omg webpack what the HELL are you doing
|
||||
}
|
||||
|
||||
module.exports = function generate_asset_cache(clientInfo, serverInfo) {
|
||||
const main_file = `/client/${ensure_array(clientInfo.assetsByChunkName.main)[0]}`;
|
||||
|
||||
const chunk_files = clientInfo.assets.map(chunk => `/client/${chunk.name}`);
|
||||
|
||||
const service_worker = generate_service_worker(chunk_files);
|
||||
const index = generate_index(main_file);
|
||||
|
||||
if (dev) {
|
||||
fs.writeFileSync(path.join(dest, 'service-worker.js'), service_worker);
|
||||
fs.writeFileSync(path.join(dest, 'index.html'), index);
|
||||
}
|
||||
|
||||
return {
|
||||
client: {
|
||||
main_file,
|
||||
chunk_files,
|
||||
|
||||
main: read(`${dest}${main_file}`),
|
||||
chunks: chunk_files.reduce((lookup, file) => {
|
||||
lookup[file] = read(`${dest}${file}`);
|
||||
return lookup;
|
||||
}, {}),
|
||||
|
||||
routes: route_manager.routes.reduce((lookup, route) => {
|
||||
lookup[route.id] = `/client/${ensure_array(clientInfo.assetsByChunkName[route.id])[0]}`;
|
||||
return lookup;
|
||||
}, {}),
|
||||
|
||||
index,
|
||||
service_worker
|
||||
},
|
||||
|
||||
server: {
|
||||
entry: path.resolve(dest, 'server', serverInfo.assetsByChunkName.main)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function generate_service_worker(chunk_files) {
|
||||
const assets = glob.sync('**', { cwd: 'assets', nodir: true });
|
||||
|
||||
const route_code = `[${
|
||||
route_manager.routes
|
||||
.filter(route => route.type === 'page')
|
||||
.map(route => `{ pattern: ${route.pattern} }`)
|
||||
.join(', ')
|
||||
}]`;
|
||||
|
||||
return read('templates/service-worker.js')
|
||||
.replace('__timestamp__', Date.now())
|
||||
.replace('__assets__', JSON.stringify(assets))
|
||||
.replace('__shell__', JSON.stringify(chunk_files.concat('/index.html')))
|
||||
.replace('__routes__', route_code);
|
||||
}
|
||||
|
||||
function generate_index(main_file) {
|
||||
return templates.render(200, {
|
||||
styles: '',
|
||||
head: '',
|
||||
html: '<noscript>Please enable JavaScript!</noscript>',
|
||||
main: main_file
|
||||
});
|
||||
}
|
||||
|
||||
function read(file) {
|
||||
return fs.readFileSync(file, 'utf-8');
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
--require source-map-support/register
|
||||
--recursive
|
||||
test/unit/**/*.js
|
||||
test/unit/*/*.js
|
||||
test/common/test.js
|
||||
6672
package-lock.json
generated
6672
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
78
package.json
78
package.json
@@ -1,43 +1,77 @@
|
||||
{
|
||||
"name": "sapper",
|
||||
"version": "0.2.10",
|
||||
"version": "0.15.0",
|
||||
"description": "Military-grade apps, engineered by Svelte",
|
||||
"main": "lib/index.js",
|
||||
"main": "dist/middleware.ts.js",
|
||||
"bin": {
|
||||
"sapper": "cli/index.js"
|
||||
"sapper": "./sapper"
|
||||
},
|
||||
"files": [
|
||||
"*.js",
|
||||
"*.ts.js",
|
||||
"runtime",
|
||||
"webpack",
|
||||
"sapper",
|
||||
"components",
|
||||
"dist"
|
||||
],
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^2.3.0",
|
||||
"chokidar": "^1.7.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"ansi-colors": "^2.0.1",
|
||||
"cheerio": "^1.0.0-rc.2",
|
||||
"chokidar": "^2.0.3",
|
||||
"cookie": "^0.3.1",
|
||||
"devalue": "^1.0.1",
|
||||
"glob": "^7.1.2",
|
||||
"html-minifier": "^3.5.16",
|
||||
"mkdirp": "^0.5.1",
|
||||
"relative": "^3.0.2",
|
||||
"node-fetch": "^2.1.1",
|
||||
"port-authority": "^1.0.2",
|
||||
"pretty-bytes": "^5.0.0",
|
||||
"pretty-ms": "^3.1.0",
|
||||
"require-relative": "^0.8.7",
|
||||
"rimraf": "^2.6.2",
|
||||
"webpack": "^3.10.0",
|
||||
"webpack-hot-middleware": "^2.21.0"
|
||||
"sade": "^1.4.1",
|
||||
"sander": "^0.6.0",
|
||||
"source-map-support": "^0.5.6",
|
||||
"tslib": "^1.9.1",
|
||||
"url-parse": "^1.2.0",
|
||||
"webpack-format-messages": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"css-loader": "^0.28.7",
|
||||
"@types/glob": "^5.0.34",
|
||||
"@types/mkdirp": "^0.5.2",
|
||||
"@types/rimraf": "^2.0.2",
|
||||
"compression": "^1.7.1",
|
||||
"eslint": "^4.13.1",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"express": "^4.16.2",
|
||||
"get-port": "^3.2.0",
|
||||
"mocha": "^4.0.1",
|
||||
"nightmare": "^2.10.0",
|
||||
"node-fetch": "^1.7.3",
|
||||
"npm-run-all": "^4.1.2",
|
||||
"style-loader": "^0.19.1",
|
||||
"svelte": "^1.49.1",
|
||||
"svelte-loader": "^2.3.2",
|
||||
"wait-on": "^2.0.2"
|
||||
"eslint-plugin-import": "^2.12.0",
|
||||
"express": "^4.16.3",
|
||||
"mocha": "^5.2.0",
|
||||
"nightmare": "^3.0.0",
|
||||
"npm-run-all": "^4.1.3",
|
||||
"polka": "^0.4.0",
|
||||
"rollup": "^0.59.2",
|
||||
"rollup-plugin-commonjs": "^9.1.3",
|
||||
"rollup-plugin-json": "^3.0.0",
|
||||
"rollup-plugin-string": "^2.0.2",
|
||||
"rollup-plugin-typescript": "^0.8.1",
|
||||
"serve-static": "^1.13.2",
|
||||
"svelte": "^2.6.3",
|
||||
"svelte-loader": "^2.9.0",
|
||||
"typescript": "^2.8.3",
|
||||
"walk-sync": "^0.3.2",
|
||||
"webpack": "^4.8.3"
|
||||
},
|
||||
"scripts": {
|
||||
"cy:open": "cypress open",
|
||||
"test": "mocha --opts mocha.opts"
|
||||
"test": "mocha --opts mocha.opts",
|
||||
"pretest": "npm run build",
|
||||
"build": "rm -rf dist && rollup -c",
|
||||
"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": [
|
||||
|
||||
55
rollup.config.js
Normal file
55
rollup.config.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import typescript from 'rollup-plugin-typescript';
|
||||
import string from 'rollup-plugin-string';
|
||||
import json from 'rollup-plugin-json';
|
||||
import commonjs from 'rollup-plugin-commonjs';
|
||||
import pkg from './package.json';
|
||||
|
||||
const external = [].concat(
|
||||
Object.keys(pkg.dependencies),
|
||||
Object.keys(process.binding('natives')),
|
||||
'sapper/core.js'
|
||||
);
|
||||
|
||||
export default [
|
||||
{
|
||||
input: `src/runtime/index.ts`,
|
||||
output: {
|
||||
file: `runtime.js`,
|
||||
format: 'es'
|
||||
},
|
||||
plugins: [
|
||||
typescript({
|
||||
typescript: require('typescript'),
|
||||
target: "ES2017"
|
||||
})
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
input: [
|
||||
`src/api.ts`,
|
||||
`src/cli.ts`,
|
||||
`src/core.ts`,
|
||||
`src/middleware.ts`,
|
||||
`src/webpack.ts`
|
||||
],
|
||||
output: {
|
||||
dir: 'dist',
|
||||
format: 'cjs',
|
||||
sourcemap: true
|
||||
},
|
||||
external,
|
||||
plugins: [
|
||||
string({
|
||||
include: '**/*.md'
|
||||
}),
|
||||
json(),
|
||||
commonjs(),
|
||||
typescript({
|
||||
typescript: require('typescript')
|
||||
})
|
||||
],
|
||||
experimentalCodeSplitting: true,
|
||||
experimentalDynamicImport: true
|
||||
}
|
||||
];
|
||||
1
runtime/README.md
Normal file
1
runtime/README.md
Normal file
@@ -0,0 +1 @@
|
||||
This directory exists for legacy reasons and should be deleted before releasing version 1.
|
||||
213
runtime/app.js
213
runtime/app.js
@@ -1,211 +1,2 @@
|
||||
const detach = node => {
|
||||
node.parentNode.removeChild(node);
|
||||
};
|
||||
|
||||
export let component;
|
||||
let target;
|
||||
let routes;
|
||||
|
||||
const history = typeof window !== 'undefined' ? window.history : {
|
||||
pushState: () => {},
|
||||
replaceState: () => {},
|
||||
};
|
||||
|
||||
const scroll_history = {};
|
||||
let uid = 1;
|
||||
let cid;
|
||||
|
||||
if ('scrollRestoration' in history) {
|
||||
history.scrollRestoration = 'manual';
|
||||
}
|
||||
|
||||
function select_route(url) {
|
||||
if (url.origin !== window.location.origin) return null;
|
||||
|
||||
for (const route of routes) {
|
||||
const match = route.pattern.exec(url.pathname);
|
||||
if (match) {
|
||||
const params = route.params(match);
|
||||
|
||||
const query = {};
|
||||
for (const [key, value] of url.searchParams) query[key] = value || true;
|
||||
|
||||
return { route, data: { params, query } };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function render(Component, data, scroll) {
|
||||
Promise.resolve(
|
||||
Component.preload ? Component.preload(data) : {}
|
||||
).then(preloaded => {
|
||||
if (component) {
|
||||
component.destroy();
|
||||
} else {
|
||||
// first load — remove SSR'd <head> contents
|
||||
const start = document.querySelector('#sapper-head-start');
|
||||
const end = document.querySelector('#sapper-head-end');
|
||||
|
||||
if (start && end) {
|
||||
while (start.nextSibling !== end) detach(start.nextSibling);
|
||||
detach(start);
|
||||
detach(end);
|
||||
}
|
||||
|
||||
// preload additional routes
|
||||
routes.reduce((promise, route) => promise.then(route.load), Promise.resolve());
|
||||
}
|
||||
|
||||
component = new Component({
|
||||
target,
|
||||
data: Object.assign(data, preloaded),
|
||||
hydrate: !!component
|
||||
});
|
||||
|
||||
if (scroll) {
|
||||
window.scrollTo(scroll.x, scroll.y);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function navigate(url, id) {
|
||||
const selected = select_route(url);
|
||||
if (selected) {
|
||||
if (id) {
|
||||
// popstate or initial navigation
|
||||
cid = id;
|
||||
} else {
|
||||
// clicked on a link. preserve scroll state
|
||||
scroll_history[cid] = scroll_state();
|
||||
|
||||
id = cid = ++uid;
|
||||
scroll_history[cid] = { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
selected.route.load().then(mod => {
|
||||
render(mod.default, selected.data, scroll_history[id]);
|
||||
});
|
||||
|
||||
cid = id;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function handle_click(event) {
|
||||
// Adapted from https://github.com/visionmedia/page.js
|
||||
// MIT license https://github.com/visionmedia/page.js#license
|
||||
if (which(event) !== 1) return;
|
||||
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
|
||||
if (event.defaultPrevented) return;
|
||||
|
||||
const a = findAnchor(event.target);
|
||||
if (!a) return;
|
||||
|
||||
// check if link is inside an svg
|
||||
// in this case, both href and target are always inside an object
|
||||
const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
|
||||
const href = svg ? a.href.baseVal : a.href;
|
||||
|
||||
if (href === window.location.href) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore if tag has
|
||||
// 1. 'download' attribute
|
||||
// 2. rel='external' attribute
|
||||
if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
|
||||
|
||||
// Ignore if <a> has a target
|
||||
if (svg ? a.target.baseVal : a.target) return;
|
||||
|
||||
const url = new URL(href);
|
||||
|
||||
// Don't handle hash changes
|
||||
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
|
||||
|
||||
if (navigate(url, null)) {
|
||||
event.preventDefault();
|
||||
history.pushState({ id: cid }, '', url.href);
|
||||
}
|
||||
}
|
||||
|
||||
function handle_popstate(event) {
|
||||
scroll_history[cid] = scroll_state();
|
||||
|
||||
if (event.state) {
|
||||
navigate(new URL(window.location), event.state.id);
|
||||
} else {
|
||||
// hashchange
|
||||
cid = ++uid;
|
||||
history.replaceState({ id: cid }, '', window.location.href);
|
||||
}
|
||||
}
|
||||
|
||||
function prefetch(event) {
|
||||
const a = findAnchor(event.target);
|
||||
if (!a || a.rel !== 'prefetch') return;
|
||||
|
||||
const selected = select_route(new URL(a.href));
|
||||
|
||||
if (selected) {
|
||||
selected.route.load().then(mod => {
|
||||
if (mod.default.preload) mod.default.preload(selected.data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function findAnchor(node) {
|
||||
while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG <a> elements have a lowercase name
|
||||
return node;
|
||||
}
|
||||
|
||||
let inited;
|
||||
|
||||
export function init(_target, _routes) {
|
||||
target = _target;
|
||||
routes = _routes;
|
||||
|
||||
if (!inited) { // this check makes HMR possible
|
||||
window.addEventListener('click', handle_click);
|
||||
window.addEventListener('popstate', handle_popstate);
|
||||
|
||||
// prefetch
|
||||
window.addEventListener('touchstart', prefetch);
|
||||
window.addEventListener('mouseover', prefetch);
|
||||
|
||||
inited = true;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const { hash, href } = window.location;
|
||||
|
||||
const deep_linked = hash && document.querySelector(hash);
|
||||
scroll_history[uid] = deep_linked ?
|
||||
{ x: 0, y: deep_linked.getBoundingClientRect().top } :
|
||||
scroll_state();
|
||||
|
||||
history.replaceState({ id: uid }, '', href);
|
||||
navigate(new URL(window.location), uid);
|
||||
});
|
||||
}
|
||||
|
||||
function which(event) {
|
||||
event = event || window.event;
|
||||
return event.which === null ? event.button : event.which;
|
||||
}
|
||||
|
||||
function scroll_state() {
|
||||
return {
|
||||
x: window.scrollX,
|
||||
y: window.scrollY
|
||||
};
|
||||
}
|
||||
|
||||
export function goto(href, opts = {}) {
|
||||
if (navigate(new URL(href, window.location.href))) {
|
||||
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
|
||||
} else {
|
||||
window.location.href = href;
|
||||
}
|
||||
}
|
||||
console.error('sapper/runtime/app.js has been deprecated in favour of sapper/runtime.js');
|
||||
export * from '../runtime.js';
|
||||
38
sapper-dev-client.js
Normal file
38
sapper-dev-client.js
Normal file
@@ -0,0 +1,38 @@
|
||||
let source;
|
||||
|
||||
function check() {
|
||||
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_routes, create_serviceworker_manifest } from '../core'
|
||||
import { locations } from '../config';
|
||||
import * as events from './interfaces';
|
||||
|
||||
export function build(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;
|
||||
}
|
||||
|
||||
async function execute(emitter: EventEmitter, {
|
||||
dest = 'build',
|
||||
app = 'app',
|
||||
webpack = 'webpack',
|
||||
routes = 'routes'
|
||||
} = {}) {
|
||||
mkdirp.sync(dest);
|
||||
rimraf.sync(path.join(dest, '**/*'));
|
||||
|
||||
// minify app/template.html
|
||||
// TODO compile this to a function? could be quicker than str.replace(...).replace(...).replace(...)
|
||||
const template = fs.readFileSync(`${app}/template.html`, 'utf-8');
|
||||
|
||||
// remove this in a future version
|
||||
if (template.indexOf('%sapper.base%') === -1) {
|
||||
const error = new Error(`As of Sapper v0.10, your template.html file must include %sapper.base% in the <head>`);
|
||||
error.code = `missing-sapper-base`;
|
||||
throw error;
|
||||
}
|
||||
|
||||
fs.writeFileSync(`${dest}/template.html`, minify_html(template));
|
||||
|
||||
const route_objects = create_routes();
|
||||
|
||||
// create app/manifest/client.js and app/manifest/server.js
|
||||
create_main_manifests({ routes: route_objects });
|
||||
|
||||
const { client, server, serviceworker } = create_compilers({ webpack });
|
||||
|
||||
const client_stats = await compile(client);
|
||||
emitter.emit('build', <events.BuildEvent>{
|
||||
type: 'client',
|
||||
// TODO duration/warnings
|
||||
webpack_stats: client_stats
|
||||
});
|
||||
|
||||
const client_info = client_stats.toJson();
|
||||
fs.writeFileSync(path.join(dest, 'client_info.json'), JSON.stringify(client_info));
|
||||
fs.writeFileSync(path.join(dest, 'client_assets.json'), JSON.stringify(client_info.assetsByChunkName));
|
||||
|
||||
const server_stats = await compile(server);
|
||||
emitter.emit('build', <events.BuildEvent>{
|
||||
type: 'server',
|
||||
// TODO duration/warnings
|
||||
webpack_stats: server_stats
|
||||
});
|
||||
|
||||
let serviceworker_stats;
|
||||
|
||||
if (serviceworker) {
|
||||
create_serviceworker_manifest({
|
||||
routes: route_objects,
|
||||
client_files: client_stats.toJson().assets.map((chunk: { name: string }) => `client/${chunk.name}`)
|
||||
});
|
||||
|
||||
serviceworker_stats = await compile(serviceworker);
|
||||
|
||||
emitter.emit('build', <events.BuildEvent>{
|
||||
type: 'serviceworker',
|
||||
// TODO duration/warnings
|
||||
webpack_stats: serviceworker_stats
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function compile(compiler: any) {
|
||||
return new Promise((fulfil, reject) => {
|
||||
compiler.run((err: Error, stats: any) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (stats.hasErrors()) {
|
||||
console.error(stats.toString({ colors: true }));
|
||||
reject(new Error(`Encountered errors while building app`));
|
||||
}
|
||||
|
||||
else {
|
||||
fulfil(stats);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
487
src/api/dev.ts
Normal file
487
src/api/dev.ts
Normal file
@@ -0,0 +1,487 @@
|
||||
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 format_messages from 'webpack-format-messages';
|
||||
import { locations } from '../config';
|
||||
import { EventEmitter } from 'events';
|
||||
import { create_routes, create_main_manifests, create_compilers, create_serviceworker_manifest } from '../core';
|
||||
import * as events from './interfaces';
|
||||
|
||||
export function dev(opts) {
|
||||
return new Watcher(opts);
|
||||
}
|
||||
|
||||
class Watcher extends EventEmitter {
|
||||
dirs: {
|
||||
app: string;
|
||||
dest: string;
|
||||
routes: string;
|
||||
webpack: string;
|
||||
}
|
||||
port: number;
|
||||
closed: boolean;
|
||||
|
||||
dev_server: DevServer;
|
||||
proc: child_process.ChildProcess;
|
||||
filewatchers: Array<{ close: () => void }>;
|
||||
deferreds: {
|
||||
client: Deferred;
|
||||
server: Deferred;
|
||||
};
|
||||
|
||||
crashed: boolean;
|
||||
restarting: boolean;
|
||||
current_build: {
|
||||
changed: Set<string>;
|
||||
rebuilding: Set<string>;
|
||||
unique_warnings: Set<string>;
|
||||
unique_errors: Set<string>;
|
||||
}
|
||||
|
||||
constructor({
|
||||
app = locations.app(),
|
||||
dest = locations.dest(),
|
||||
routes = locations.routes(),
|
||||
webpack = 'webpack',
|
||||
port = +process.env.PORT
|
||||
}: {
|
||||
app: string,
|
||||
dest: string,
|
||||
routes: string,
|
||||
webpack: string,
|
||||
port: number
|
||||
}) {
|
||||
super();
|
||||
|
||||
this.dirs = { app, dest, routes, webpack };
|
||||
this.port = port;
|
||||
this.closed = false;
|
||||
|
||||
this.filewatchers = [];
|
||||
|
||||
this.current_build = {
|
||||
changed: new Set(),
|
||||
rebuilding: new Set(),
|
||||
unique_errors: new Set(),
|
||||
unique_warnings: new Set()
|
||||
};
|
||||
|
||||
// remove this in a future version
|
||||
const template = fs.readFileSync(path.join(app, 'template.html'), 'utf-8');
|
||||
if (template.indexOf('%sapper.base%') === -1) {
|
||||
const error = new Error(`As of Sapper v0.10, your template.html file must include %sapper.base% in the <head>`);
|
||||
error.code = `missing-sapper-base`;
|
||||
throw error;
|
||||
}
|
||||
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
process.on('exit', () => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this.port) {
|
||||
if (!await ports.check(this.port)) {
|
||||
this.emit('fatal', <events.FatalEvent>{
|
||||
message: `Port ${this.port} is unavailable`
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.port = await ports.find(3000);
|
||||
}
|
||||
|
||||
const { dest } = this.dirs;
|
||||
rimraf.sync(dest);
|
||||
mkdirp.sync(dest);
|
||||
|
||||
const dev_port = await ports.find(10000);
|
||||
|
||||
try {
|
||||
const routes = create_routes();
|
||||
create_main_manifests({ routes, dev_port });
|
||||
} catch (err) {
|
||||
this.emit('fatal', <events.FatalEvent>{
|
||||
message: err.message
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.dev_server = new DevServer(dev_port);
|
||||
|
||||
this.filewatchers.push(
|
||||
watch_files(locations.routes(), ['add', 'unlink'], () => {
|
||||
const routes = create_routes();
|
||||
create_main_manifests({ routes, dev_port });
|
||||
|
||||
try {
|
||||
const routes = create_routes();
|
||||
create_main_manifests({ routes, dev_port });
|
||||
} catch (err) {
|
||||
this.emit('error', <events.ErrorEvent>{
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
watch_files(`${locations.app()}/template.html`, ['change'], () => {
|
||||
this.dev_server.send({
|
||||
action: 'reload'
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
this.deferreds = {
|
||||
server: new Deferred(),
|
||||
client: new Deferred()
|
||||
};
|
||||
|
||||
// TODO watch the configs themselves?
|
||||
const compilers = create_compilers({ webpack: this.dirs.webpack });
|
||||
|
||||
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');
|
||||
this.deferreds.server = new Deferred();
|
||||
},
|
||||
|
||||
result: info => {
|
||||
fs.writeFileSync(path.join(dest, 'server_info.json'), JSON.stringify(info, null, ' '));
|
||||
|
||||
this.deferreds.client.promise.then(() => {
|
||||
const restart = () => {
|
||||
log = '';
|
||||
this.crashed = false;
|
||||
|
||||
ports.wait(this.port)
|
||||
.then((() => {
|
||||
this.emit('ready', <events.ReadyEvent>{
|
||||
port: this.port,
|
||||
process: this.proc
|
||||
});
|
||||
|
||||
this.deferreds.server.fulfil();
|
||||
|
||||
this.dev_server.send({
|
||||
status: 'completed'
|
||||
});
|
||||
}))
|
||||
.catch(err => {
|
||||
if (this.crashed) return;
|
||||
|
||||
this.emit('fatal', <events.FatalEvent>{
|
||||
message: `Server is not listening on port ${this.port}`,
|
||||
log
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (this.proc) {
|
||||
this.proc.removeListener('exit', emitFatal);
|
||||
this.proc.kill();
|
||||
this.proc.on('exit', restart);
|
||||
} else {
|
||||
restart();
|
||||
}
|
||||
|
||||
this.proc = child_process.fork(`${dest}/server.js`, [], {
|
||||
cwd: process.cwd(),
|
||||
env: Object.assign({
|
||||
PORT: this.port
|
||||
}, process.env),
|
||||
stdio: ['ipc']
|
||||
});
|
||||
|
||||
this.proc.stdout.on('data', chunk => {
|
||||
log += chunk;
|
||||
this.emit('stdout', chunk);
|
||||
});
|
||||
|
||||
this.proc.stderr.on('data', chunk => {
|
||||
log += chunk;
|
||||
this.emit('stderr', chunk);
|
||||
});
|
||||
|
||||
this.proc.on('message', message => {
|
||||
if (message.__sapper__ && message.event === 'basepath') {
|
||||
this.emit('basepath', {
|
||||
basepath: message.basepath
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.proc.on('exit', emitFatal);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let first = true;
|
||||
|
||||
this.watch(compilers.client, {
|
||||
name: 'client',
|
||||
|
||||
invalid: filename => {
|
||||
this.restart(filename, 'client');
|
||||
this.deferreds.client = 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
|
||||
},
|
||||
|
||||
result: info => {
|
||||
fs.writeFileSync(path.join(dest, 'client_info.json'), JSON.stringify(info));
|
||||
fs.writeFileSync(path.join(dest, 'client_assets.json'), JSON.stringify(info.assetsByChunkName, null, ' '));
|
||||
this.deferreds.client.fulfil();
|
||||
|
||||
const client_files = info.assets.map((chunk: { name: string }) => `client/${chunk.name}`);
|
||||
|
||||
create_serviceworker_manifest({
|
||||
routes: create_routes(),
|
||||
client_files
|
||||
});
|
||||
|
||||
// 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',
|
||||
|
||||
result: info => {
|
||||
fs.writeFileSync(path.join(dest, 'serviceworker_info.json'), JSON.stringify(info, null, ' '));
|
||||
}
|
||||
});
|
||||
}
|
||||
: 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: any, { name, invalid = noop, result }: {
|
||||
name: string,
|
||||
invalid?: (filename: string) => void;
|
||||
result: (stats: any) => void;
|
||||
}) {
|
||||
compiler.hooks.invalid.tap('sapper', (filename: string) => {
|
||||
invalid(filename);
|
||||
});
|
||||
|
||||
compiler.watch({}, (err: Error, stats: any) => {
|
||||
if (err) {
|
||||
this.emit('error', <events.ErrorEvent>{
|
||||
type: name,
|
||||
message: err.message
|
||||
});
|
||||
} else {
|
||||
const messages = format_messages(stats);
|
||||
const info = stats.toJson();
|
||||
|
||||
this.emit('build', {
|
||||
type: name,
|
||||
|
||||
duration: info.time,
|
||||
|
||||
errors: messages.errors.map((message: string) => {
|
||||
const duplicate = this.current_build.unique_errors.has(message);
|
||||
this.current_build.unique_errors.add(message);
|
||||
|
||||
return mungeWebpackError(message, duplicate);
|
||||
}),
|
||||
|
||||
warnings: messages.warnings.map((message: string) => {
|
||||
const duplicate = this.current_build.unique_warnings.has(message);
|
||||
this.current_build.unique_warnings.add(message);
|
||||
|
||||
return mungeWebpackError(message, duplicate);
|
||||
}),
|
||||
});
|
||||
|
||||
result(info);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const locPattern = /\((\d+):(\d+)\)$/;
|
||||
|
||||
function mungeWebpackError(message: string, duplicate: boolean) {
|
||||
// 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,
|
||||
line,
|
||||
column,
|
||||
message: lines.join('\n'),
|
||||
originalMessage: message,
|
||||
duplicate
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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_files(pattern: string, events: string[], callback: () => void) {
|
||||
const chokidar = require('chokidar');
|
||||
|
||||
const watcher = chokidar.watch(pattern, {
|
||||
persistent: true,
|
||||
ignoreInitial: true,
|
||||
disableGlobbing: true
|
||||
});
|
||||
|
||||
events.forEach(event => {
|
||||
watcher.on(event, callback);
|
||||
});
|
||||
|
||||
return {
|
||||
close: () => watcher.close()
|
||||
};
|
||||
}
|
||||
131
src/api/export.ts
Normal file
131
src/api/export.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import * as child_process from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as sander from 'sander';
|
||||
import cheerio from 'cheerio';
|
||||
import URL from 'url-parse';
|
||||
import fetch from 'node-fetch';
|
||||
import * as ports from 'port-authority';
|
||||
import { EventEmitter } from 'events';
|
||||
import { minify_html } from './utils/minify_html';
|
||||
import { locations } from '../config';
|
||||
import * as events from './interfaces';
|
||||
|
||||
export function exporter(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;
|
||||
}
|
||||
|
||||
async function execute(emitter: EventEmitter, {
|
||||
build = 'build',
|
||||
dest = 'export',
|
||||
basepath = ''
|
||||
} = {}) {
|
||||
const export_dir = path.join(dest, basepath);
|
||||
|
||||
// Prep output directory
|
||||
sander.rimrafSync(export_dir);
|
||||
|
||||
sander.copydirSync('assets').to(export_dir);
|
||||
sander.copydirSync(build, 'client').to(export_dir, 'client');
|
||||
|
||||
if (sander.existsSync(build, 'service-worker.js')) {
|
||||
sander.copyFileSync(build, 'service-worker.js').to(export_dir, 'service-worker.js');
|
||||
}
|
||||
|
||||
if (sander.existsSync(build, 'service-worker.js.map')) {
|
||||
sander.copyFileSync(build, 'service-worker.js.map').to(export_dir, 'service-worker.js.map');
|
||||
}
|
||||
|
||||
const port = await ports.find(3000);
|
||||
|
||||
const origin = `http://localhost:${port}`;
|
||||
|
||||
const proc = child_process.fork(path.resolve(`${build}/server.js`), [], {
|
||||
cwd: process.cwd(),
|
||||
env: Object.assign({
|
||||
PORT: port,
|
||||
NODE_ENV: 'production',
|
||||
SAPPER_DEST: build,
|
||||
SAPPER_EXPORT: 'true'
|
||||
}, process.env)
|
||||
});
|
||||
|
||||
const seen = new Set();
|
||||
const saved = new Set();
|
||||
|
||||
proc.on('message', message => {
|
||||
if (!message.__sapper__ || message.event !== 'file') return;
|
||||
|
||||
let file = new URL(message.url, origin).pathname.slice(1);
|
||||
let { body } = message;
|
||||
|
||||
if (saved.has(file)) return;
|
||||
saved.add(file);
|
||||
|
||||
const is_html = message.type === 'text/html';
|
||||
|
||||
if (is_html) {
|
||||
file = file === '' ? 'index.html' : `${file}/index.html`;
|
||||
body = minify_html(body);
|
||||
}
|
||||
|
||||
emitter.emit('file', <events.FileEvent>{
|
||||
file,
|
||||
size: body.length
|
||||
});
|
||||
|
||||
sander.writeFileSync(export_dir, file, body);
|
||||
});
|
||||
|
||||
async function handle(url: URL) {
|
||||
const r = await fetch(url.href);
|
||||
const range = ~~(r.status / 100);
|
||||
|
||||
if (range >= 4) {
|
||||
emitter.emit('failure', <events.FailureEvent>{
|
||||
status: r.status,
|
||||
pathname: url.pathname
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (range === 2) {
|
||||
if (r.headers.get('Content-Type') === 'text/html') {
|
||||
const body = await r.text();
|
||||
const $ = cheerio.load(body);
|
||||
const urls: URL[] = [];
|
||||
|
||||
const base = new URL($('base').attr('href') || '/', url.href);
|
||||
|
||||
$('a[href]').each((i: number, $a) => {
|
||||
const url = new URL($a.attribs.href, base.href);
|
||||
|
||||
if (url.origin === origin && !seen.has(url.pathname)) {
|
||||
seen.add(url.pathname);
|
||||
urls.push(url);
|
||||
}
|
||||
});
|
||||
|
||||
for (const url of urls) {
|
||||
await handle(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ports.wait(port)
|
||||
.then(() => handle(new URL(`/${basepath}`, origin))) // TODO all static routes
|
||||
.then(() => proc.kill());
|
||||
}
|
||||
15
src/api/find_page.ts
Normal file
15
src/api/find_page.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as glob from 'glob';
|
||||
import { locations } from '../config';
|
||||
import { create_routes } from '../core';
|
||||
|
||||
export function find_page(pathname: string, cwd = locations.routes()) {
|
||||
const { pages } = create_routes(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/api/interfaces.ts
Normal file
44
src/api/interfaces.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import * as child_process from 'child_process';
|
||||
|
||||
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<{ message: string, duplicate: boolean }>;
|
||||
warnings: Array<{ message: string, duplicate: boolean }>;
|
||||
duration: number;
|
||||
webpack_stats: any;
|
||||
}
|
||||
|
||||
export type FileEvent = {
|
||||
file: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export type FailureEvent = {
|
||||
|
||||
}
|
||||
|
||||
export type DoneEvent = {}
|
||||
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 function minify_html(html: string) {
|
||||
return minify(html, {
|
||||
collapseBooleanAttributes: true,
|
||||
collapseWhitespace: true,
|
||||
conservativeCollapse: true,
|
||||
decodeEntities: true,
|
||||
html5: true,
|
||||
minifyCSS: true,
|
||||
minifyJS: true,
|
||||
removeAttributeQuotes: true,
|
||||
removeComments: true,
|
||||
removeOptionalTags: true,
|
||||
removeRedundantAttributes: true,
|
||||
removeScriptTypeAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
sortAttributes: true,
|
||||
sortClassName: true
|
||||
});
|
||||
}
|
||||
97
src/cli.ts
Executable file
97
src/cli.ts
Executable file
@@ -0,0 +1,97 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as child_process from 'child_process';
|
||||
import sade from 'sade';
|
||||
import * as colors from 'ansi-colors';
|
||||
import prettyMs from 'pretty-ms';
|
||||
// import upgrade from './cli/upgrade';
|
||||
import * as ports from 'port-authority';
|
||||
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')
|
||||
.action(async (opts: { port: number, open: boolean }) => {
|
||||
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')
|
||||
.example(`build custom-dir -p 4567`)
|
||||
.action(async (dest = 'build', opts: { port: 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();
|
||||
|
||||
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.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
||||
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('--basepath', 'Specify a base path')
|
||||
.action(async (dest = 'export', opts: { basepath?: string }) => {
|
||||
console.log(`> Building...`);
|
||||
|
||||
process.env.NODE_ENV = 'production';
|
||||
process.env.SAPPER_DEST = '.sapper/.export';
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
try {
|
||||
const { build } = await import('./cli/build');
|
||||
await build();
|
||||
console.error(`\n> Built in ${elapsed(start)}. Crawling site...`);
|
||||
|
||||
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(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO upgrade
|
||||
|
||||
prog.parse(process.argv);
|
||||
|
||||
function elapsed(start: number) {
|
||||
return prettyMs(Date.now() - start);
|
||||
}
|
||||
32
src/cli/build.ts
Normal file
32
src/cli/build.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { build as _build } from '../api/build';
|
||||
import * as colors from 'ansi-colors';
|
||||
import { locations } from '../config';
|
||||
|
||||
export function build() {
|
||||
return new Promise((fulfil, reject) => {
|
||||
try {
|
||||
const emitter = _build({
|
||||
dest: locations.dest(),
|
||||
app: locations.app(),
|
||||
routes: locations.routes(),
|
||||
webpack: 'webpack'
|
||||
});
|
||||
|
||||
emitter.on('build', event => {
|
||||
console.log(colors.inverse(`\nbuilt ${event.type}`));
|
||||
console.log(event.webpack_stats.toString({ colors: true }));
|
||||
});
|
||||
|
||||
emitter.on('error', event => {
|
||||
reject(event.error);
|
||||
});
|
||||
|
||||
emitter.on('done', event => {
|
||||
fulfil();
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(`${colors.bold.red(`> ${err.message}`)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
78
src/cli/dev.ts
Normal file
78
src/cli/dev.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import * as path from 'path';
|
||||
import * as colors from 'ansi-colors';
|
||||
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 }) {
|
||||
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 => {
|
||||
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 => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
35
src/cli/export.ts
Normal file
35
src/cli/export.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { exporter as _exporter } from '../api/export';
|
||||
import * as colors from 'ansi-colors';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import { locations } from '../config';
|
||||
|
||||
export function exporter(export_dir: string, { basepath = '' }) {
|
||||
return new Promise((fulfil, reject) => {
|
||||
try {
|
||||
const emitter = _exporter({
|
||||
build: locations.dest(),
|
||||
dest: export_dir,
|
||||
basepath
|
||||
});
|
||||
|
||||
emitter.on('file', event => {
|
||||
console.log(`${colors.bold.cyan(event.file)} ${colors.gray(`(${prettyBytes(event.size)})`)}`);
|
||||
});
|
||||
|
||||
emitter.on('failure', event => {
|
||||
console.log(`${colors.red(`> Received ${event.status} response when fetching ${event.pathname}`)}`);
|
||||
});
|
||||
|
||||
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 * as colors from 'ansi-colors';
|
||||
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 * as colors from 'ansi-colors';
|
||||
|
||||
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);
|
||||
}
|
||||
10
src/config.ts
Normal file
10
src/config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import * as path from 'path';
|
||||
|
||||
export const dev = () => process.env.NODE_ENV !== 'production';
|
||||
|
||||
export const locations = {
|
||||
base: () => path.resolve(process.env.SAPPER_BASE || ''),
|
||||
app: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_APP || 'app'),
|
||||
routes: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_ROUTES || 'routes'),
|
||||
dest: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_DEST || '.sapper')
|
||||
};
|
||||
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';
|
||||
export { default as create_routes } from './core/create_routes';
|
||||
29
src/core/create_compilers.ts
Normal file
29
src/core/create_compilers.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as path from 'path';
|
||||
import relative from 'require-relative';
|
||||
|
||||
export default function create_compilers({ webpack }: { webpack: string }) {
|
||||
const wp = relative('webpack', process.cwd());
|
||||
|
||||
const serviceworker_config = try_require(path.resolve(`${webpack}/service-worker.config.js`));
|
||||
|
||||
return {
|
||||
client: wp(
|
||||
require(path.resolve(`${webpack}/client.config.js`))
|
||||
),
|
||||
|
||||
server: wp(
|
||||
require(path.resolve(`${webpack}/server.config.js`))
|
||||
),
|
||||
|
||||
serviceworker: serviceworker_config && wp(serviceworker_config)
|
||||
};
|
||||
}
|
||||
|
||||
function try_require(specifier: string) {
|
||||
try {
|
||||
return require(specifier);
|
||||
} catch (err) {
|
||||
if (err.code === 'MODULE_NOT_FOUND') return null;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
186
src/core/create_manifests.ts
Normal file
186
src/core/create_manifests.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as glob from 'glob';
|
||||
import { posixify, write_if_changed } from './utils';
|
||||
import { dev, locations } from '../config';
|
||||
import { Page, PageComponent, ServerRoute } from '../interfaces';
|
||||
|
||||
export function create_main_manifests({ routes, dev_port }: {
|
||||
routes: { components: PageComponent[], pages: Page[], server_routes: ServerRoute[] };
|
||||
dev_port?: number;
|
||||
}) {
|
||||
const path_to_routes = path.relative(`${locations.app()}/manifest`, locations.routes());
|
||||
|
||||
const client_manifest = generate_client(routes, path_to_routes, dev_port);
|
||||
const server_manifest = generate_server(routes, path_to_routes);
|
||||
|
||||
write_if_changed(
|
||||
`${locations.app()}/manifest/default-layout.html`,
|
||||
`<svelte:component this={child.component} {...child.props}/>`
|
||||
);
|
||||
write_if_changed(`${locations.app()}/manifest/client.js`, client_manifest);
|
||||
write_if_changed(`${locations.app()}/manifest/server.js`, server_manifest);
|
||||
}
|
||||
|
||||
export function create_serviceworker_manifest({ routes, client_files }: {
|
||||
routes: { components: PageComponent[], pages: Page[], server_routes: ServerRoute[] };
|
||||
client_files: string[];
|
||||
}) {
|
||||
const assets = glob.sync('**', { cwd: 'assets', nodir: true });
|
||||
|
||||
let code = `
|
||||
// This file is generated by Sapper — do not edit it!
|
||||
export const timestamp = ${Date.now()};
|
||||
|
||||
export const assets = [\n\t${assets.map((x: string) => `"${x}"`).join(',\n\t')}\n];
|
||||
|
||||
export const shell = [\n\t${client_files.map((x: string) => `"${x}"`).join(',\n\t')}\n];
|
||||
|
||||
export const routes = [\n\t${routes.pages.map((r: Page) => `{ pattern: ${r.pattern} }`).join(',\n\t')}\n];
|
||||
`.replace(/^\t\t/gm, '').trim();
|
||||
|
||||
write_if_changed(`${locations.app()}/manifest/service-worker.js`, code);
|
||||
}
|
||||
|
||||
function right_pad(str: string, len: number) {
|
||||
while (str.length < len) str += ' ';
|
||||
return str;
|
||||
}
|
||||
|
||||
function generate_client(
|
||||
routes: { root: PageComponent, components: PageComponent[], pages: Page[], server_routes: ServerRoute[] },
|
||||
path_to_routes: string,
|
||||
dev_port?: number
|
||||
) {
|
||||
const page_ids = new Set(routes.pages.map(page =>
|
||||
page.pattern.toString()));
|
||||
|
||||
const server_routes_to_ignore = routes.server_routes.filter(route =>
|
||||
!page_ids.has(route.pattern.toString()));
|
||||
|
||||
const len = Math.max(...routes.components.map(c => c.name.length));
|
||||
|
||||
let code = `
|
||||
// This file is generated by Sapper — do not edit it!
|
||||
import root from '${posixify(`${path_to_routes}/${routes.root.file}`)}';
|
||||
import error from '${posixify(`${path_to_routes}/_error.html`)}';
|
||||
|
||||
${routes.components.map(component =>
|
||||
`const ${component.name} = () =>
|
||||
import(/* webpackChunkName: "${component.name}" */ '${get_file(path_to_routes, component)}');`)
|
||||
.join('\n')}
|
||||
|
||||
export const manifest = {
|
||||
ignore: [${server_routes_to_ignore.map(route => route.pattern).join(', ')}],
|
||||
|
||||
pages: [
|
||||
${routes.pages.map(page => `{
|
||||
// ${page.parts[page.parts.length - 1].component.file}
|
||||
pattern: ${page.pattern},
|
||||
parts: [
|
||||
${page.parts.map(part => {
|
||||
if (part.params.length > 0) {
|
||||
const props = part.params.map((param, i) => `${param}: match[${i + 1}]`);
|
||||
return `{ component: ${part.component.name}, params: match => ({ ${props.join(', ')} }) }`;
|
||||
}
|
||||
|
||||
return `{ component: ${part.component.name} }`;
|
||||
}).join(',\n\t\t\t\t\t\t')}
|
||||
]
|
||||
}`).join(',\n\n\t\t\t\t')}
|
||||
],
|
||||
|
||||
root,
|
||||
|
||||
error
|
||||
};
|
||||
|
||||
// this is included for legacy reasons
|
||||
export const routes = {};`.replace(/^\t\t/gm, '').trim();
|
||||
|
||||
if (dev()) {
|
||||
const sapper_dev_client = posixify(
|
||||
path.resolve(__dirname, '../sapper-dev-client.js')
|
||||
);
|
||||
|
||||
code += `
|
||||
|
||||
if (module.hot) {
|
||||
import('${sapper_dev_client}').then(client => {
|
||||
client.connect(${dev_port});
|
||||
});
|
||||
}`.replace(/^\t{3}/gm, '');
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
function generate_server(
|
||||
routes: { root: PageComponent, components: PageComponent[], pages: Page[], server_routes: ServerRoute[] },
|
||||
path_to_routes: string
|
||||
) {
|
||||
const imports = [].concat(
|
||||
routes.server_routes.map(route =>
|
||||
`import * as ${route.name} from '${posixify(`${path_to_routes}/${route.file}`)}';`),
|
||||
routes.components.map(component =>
|
||||
`import ${component.name} from '${get_file(path_to_routes, component)}';`),
|
||||
`import root from '${posixify(`${path_to_routes}/${routes.root.file}`)}';`,
|
||||
`import error from '${posixify(`${path_to_routes}/_error.html`)}';`
|
||||
);
|
||||
|
||||
let code = `
|
||||
// This file is generated by Sapper — do not edit it!
|
||||
${imports.join('\n')}
|
||||
|
||||
export const manifest = {
|
||||
server_routes: [
|
||||
${routes.server_routes.map(route => `{
|
||||
// ${route.file}
|
||||
pattern: ${route.pattern},
|
||||
handlers: ${route.name},
|
||||
params: ${route.params.length > 0
|
||||
? `match => ({ ${route.params.map((param, i) => `${param}: match[${i + 1}]`).join(', ')} })`
|
||||
: `() => ({})`}
|
||||
}`).join(',\n\n\t\t\t\t')}
|
||||
],
|
||||
|
||||
pages: [
|
||||
${routes.pages.map(page => `{
|
||||
// ${page.parts[page.parts.length - 1].component.file}
|
||||
pattern: ${page.pattern},
|
||||
parts: [
|
||||
${page.parts.map(part => {
|
||||
const props = [
|
||||
`name: "${part.component.name}"`,
|
||||
`component: ${part.component.name}`
|
||||
];
|
||||
|
||||
if (part.params.length > 0) {
|
||||
const params = part.params.map((param, i) => `${param}: match[${i + 1}]`);
|
||||
props.push(`params: match => ({ ${params.join(', ')} })`);
|
||||
}
|
||||
|
||||
return `{ ${props.join(', ')} }`;
|
||||
}).join(',\n\t\t\t\t\t\t')}
|
||||
]
|
||||
}`).join(',\n\n\t\t\t\t')}
|
||||
],
|
||||
|
||||
root,
|
||||
|
||||
error
|
||||
};
|
||||
|
||||
// this is included for legacy reasons
|
||||
export const routes = {};`.replace(/^\t\t/gm, '').trim();
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
function get_file(path_to_routes: string, component: PageComponent) {
|
||||
if (component.default) {
|
||||
return `./default-layout.html`;
|
||||
}
|
||||
|
||||
return posixify(`${path_to_routes}/${component.file}`);
|
||||
}
|
||||
306
src/core/create_routes.ts
Normal file
306
src/core/create_routes.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { locations } from '../config';
|
||||
import { Page, PageComponent, ServerRoute } from '../interfaces';
|
||||
import { posixify } from './utils';
|
||||
|
||||
const default_layout_file = posixify(path.resolve(
|
||||
__dirname,
|
||||
'../components/default-layout.html'
|
||||
));
|
||||
|
||||
export default function create_routes(cwd = locations.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 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 = path.extname(basename) === '.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
|
||||
};
|
||||
})
|
||||
.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 layout = fs.existsSync(index)
|
||||
? {
|
||||
name: `${get_slug(item.file)}__layout`,
|
||||
file: `${item.file}/_layout.html`
|
||||
}
|
||||
: null;
|
||||
|
||||
if (layout) {
|
||||
components.push(layout);
|
||||
} else if (components.indexOf(default_layout) === -1) {
|
||||
components.push(default_layout);
|
||||
}
|
||||
|
||||
walk(
|
||||
path.join(dir, item.basename),
|
||||
segments,
|
||||
params,
|
||||
stack.concat({
|
||||
component: layout || default_layout,
|
||||
params
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
parts
|
||||
});
|
||||
} else {
|
||||
pages.push({
|
||||
pattern: get_pattern(segments),
|
||||
parts
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
server_routes.push({
|
||||
name: `route_${get_slug(item.file)}`,
|
||||
pattern: get_pattern(segments),
|
||||
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) {
|
||||
return 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)}`
|
||||
});
|
||||
}
|
||||
|
||||
function get_pattern(segments: Part[][]) {
|
||||
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('') +
|
||||
'\\\/?$'
|
||||
);
|
||||
}
|
||||
25
src/core/utils.ts
Normal file
25
src/core/utils.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as sander from 'sander';
|
||||
|
||||
const previous_contents = new Map();
|
||||
|
||||
export function write_if_changed(file: string, code: string) {
|
||||
if (code !== previous_contents.get(file)) {
|
||||
previous_contents.set(file, code);
|
||||
sander.writeFileSync(file, code);
|
||||
fudge_mtime(file);
|
||||
}
|
||||
}
|
||||
|
||||
export function posixify(file: string) {
|
||||
return file.replace(/[/\\]/g, '/');
|
||||
}
|
||||
|
||||
export function fudge_mtime(file: string) {
|
||||
// need to fudge the mtime so that webpack doesn't go doolally
|
||||
const { atime, mtime } = sander.statSync(file);
|
||||
sander.utimesSync(
|
||||
file,
|
||||
new Date(atime.getTime() - 999999),
|
||||
new Date(mtime.getTime() - 999999)
|
||||
);
|
||||
}
|
||||
42
src/interfaces.ts
Normal file
42
src/interfaces.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
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[];
|
||||
};
|
||||
540
src/middleware.ts
Normal file
540
src/middleware.ts
Normal file
@@ -0,0 +1,540 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { URL } from 'url';
|
||||
import { ClientRequest, ServerResponse } from 'http';
|
||||
import cookie from 'cookie';
|
||||
import devalue from 'devalue';
|
||||
import fetch from 'node-fetch';
|
||||
import { lookup } from './middleware/mime';
|
||||
import { locations, dev } from './config';
|
||||
import sourceMapSupport from 'source-map-support';
|
||||
|
||||
sourceMapSupport.install();
|
||||
|
||||
type ServerRoute = {
|
||||
pattern: RegExp;
|
||||
handlers: Record<string, Handler>;
|
||||
params: (match: RegExpMatchArray) => Record<string, string>;
|
||||
};
|
||||
|
||||
type Page = {
|
||||
pattern: RegExp;
|
||||
parts: Array<{
|
||||
name: string;
|
||||
component: Component;
|
||||
params?: (match: RegExpMatchArray) => Record<string, string>;
|
||||
}>
|
||||
};
|
||||
|
||||
type Manifest = {
|
||||
server_routes: ServerRoute[];
|
||||
pages: Page[];
|
||||
root: Component;
|
||||
error: Component;
|
||||
}
|
||||
|
||||
type Handler = (req: Req, res: ServerResponse, next: () => void) => void;
|
||||
|
||||
type Store = {
|
||||
get: () => any
|
||||
};
|
||||
|
||||
type Props = {
|
||||
path: string;
|
||||
query: Record<string, string>;
|
||||
params: Record<string, string>;
|
||||
error?: { message: string };
|
||||
status?: number;
|
||||
child: {
|
||||
segment: string;
|
||||
component: Component;
|
||||
props: Props;
|
||||
};
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
interface Req extends ClientRequest {
|
||||
url: string;
|
||||
baseUrl: string;
|
||||
originalUrl: string;
|
||||
method: string;
|
||||
path: string;
|
||||
params: Record<string, string>;
|
||||
query: Record<string, string>;
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
|
||||
interface Component {
|
||||
render: (data: any, opts: { store: Store }) => {
|
||||
head: string;
|
||||
css: { code: string, map: any };
|
||||
html: string
|
||||
},
|
||||
preload: (data: any) => any | Promise<any>
|
||||
}
|
||||
|
||||
export default function middleware(opts: {
|
||||
manifest: Manifest,
|
||||
store: (req: Req) => Store,
|
||||
routes?: any // legacy
|
||||
}) {
|
||||
if (opts.routes) {
|
||||
throw new Error(`As of Sapper 0.15, opts.routes should be opts.manifest`);
|
||||
}
|
||||
|
||||
const output = locations.dest();
|
||||
|
||||
const { manifest, store } = opts;
|
||||
|
||||
let emitted_basepath = false;
|
||||
|
||||
const middleware = compose_handlers([
|
||||
(req: Req, res: ServerResponse, next: () => void) => {
|
||||
if (req.baseUrl === undefined) {
|
||||
let { originalUrl } = req;
|
||||
if (req.url === '/' && originalUrl[originalUrl.length - 1] !== '/') {
|
||||
originalUrl += '/';
|
||||
}
|
||||
|
||||
req.baseUrl = originalUrl
|
||||
? originalUrl.slice(0, -req.url.length)
|
||||
: '';
|
||||
}
|
||||
|
||||
if (!emitted_basepath && process.send) {
|
||||
process.send({
|
||||
__sapper__: true,
|
||||
event: 'basepath',
|
||||
basepath: req.baseUrl
|
||||
});
|
||||
|
||||
emitted_basepath = true;
|
||||
}
|
||||
|
||||
if (req.path === undefined) {
|
||||
req.path = req.url.replace(/\?.*/, '');
|
||||
}
|
||||
|
||||
next();
|
||||
},
|
||||
|
||||
fs.existsSync(path.join(output, 'index.html')) && serve({
|
||||
pathname: '/index.html',
|
||||
cache_control: 'max-age=600'
|
||||
}),
|
||||
|
||||
fs.existsSync(path.join(output, 'service-worker.js')) && serve({
|
||||
pathname: '/service-worker.js',
|
||||
cache_control: 'max-age=600'
|
||||
}),
|
||||
|
||||
fs.existsSync(path.join(output, 'service-worker.js.map')) && serve({
|
||||
pathname: '/service-worker.js.map',
|
||||
cache_control: 'max-age=600'
|
||||
}),
|
||||
|
||||
serve({
|
||||
prefix: '/client/',
|
||||
cache_control: 'max-age=31536000'
|
||||
}),
|
||||
|
||||
get_server_route_handler(manifest.server_routes),
|
||||
get_page_handler(manifest, store)
|
||||
].filter(Boolean));
|
||||
|
||||
return middleware;
|
||||
}
|
||||
|
||||
function serve({ prefix, pathname, cache_control }: {
|
||||
prefix?: string,
|
||||
pathname?: string,
|
||||
cache_control: string
|
||||
}) {
|
||||
const filter = pathname
|
||||
? (req: Req) => req.path === pathname
|
||||
: (req: Req) => req.path.startsWith(prefix);
|
||||
|
||||
const output = locations.dest();
|
||||
|
||||
const cache: Map<string, Buffer> = new Map();
|
||||
|
||||
const read = dev()
|
||||
? (file: string) => fs.readFileSync(path.resolve(output, file))
|
||||
: (file: string) => (cache.has(file) ? cache : cache.set(file, fs.readFileSync(path.resolve(output, file)))).get(file)
|
||||
|
||||
return (req: Req, res: ServerResponse, next: () => void) => {
|
||||
if (filter(req)) {
|
||||
const type = lookup(req.path);
|
||||
|
||||
try {
|
||||
const data = read(req.path.slice(1));
|
||||
|
||||
res.setHeader('Content-Type', type);
|
||||
res.setHeader('Cache-Control', cache_control);
|
||||
res.end(data);
|
||||
} catch (err) {
|
||||
res.statusCode = 404;
|
||||
res.end('not found');
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function get_server_route_handler(routes: ServerRoute[]) {
|
||||
function handle_route(route: ServerRoute, req: Req, res: ServerResponse, next: () => void) {
|
||||
req.params = route.params(route.pattern.exec(req.path));
|
||||
|
||||
const method = req.method.toLowerCase();
|
||||
// 'delete' cannot be exported from a module because it is a keyword,
|
||||
// so check for 'del' instead
|
||||
const method_export = method === 'delete' ? 'del' : method;
|
||||
const handle_method = route.handlers[method_export];
|
||||
if (handle_method) {
|
||||
if (process.env.SAPPER_EXPORT) {
|
||||
const { write, end, setHeader } = res;
|
||||
const chunks: any[] = [];
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
// intercept data so that it can be exported
|
||||
res.write = function(chunk: any) {
|
||||
chunks.push(new Buffer(chunk));
|
||||
write.apply(res, arguments);
|
||||
};
|
||||
|
||||
res.setHeader = function(name: string, value: string) {
|
||||
headers[name.toLowerCase()] = value;
|
||||
setHeader.apply(res, arguments);
|
||||
};
|
||||
|
||||
res.end = function(chunk?: any) {
|
||||
if (chunk) chunks.push(new Buffer(chunk));
|
||||
end.apply(res, arguments);
|
||||
|
||||
process.send({
|
||||
__sapper__: true,
|
||||
event: 'file',
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
status: res.statusCode,
|
||||
type: headers['content-type'],
|
||||
body: Buffer.concat(chunks).toString()
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const handle_next = (err?: Error) => {
|
||||
if (err) {
|
||||
res.statusCode = 500;
|
||||
res.end(err.message);
|
||||
} else {
|
||||
process.nextTick(next);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
handle_method(req, res, handle_next);
|
||||
} catch (err) {
|
||||
handle_next(err);
|
||||
}
|
||||
} else {
|
||||
// no matching handler for method
|
||||
process.nextTick(next);
|
||||
}
|
||||
}
|
||||
|
||||
return function find_route(req: Req, res: ServerResponse, next: () => void) {
|
||||
for (const route of routes) {
|
||||
if (route.pattern.test(req.path)) {
|
||||
handle_route(route, req, res, next);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store) {
|
||||
const output = locations.dest();
|
||||
|
||||
const get_chunks = dev()
|
||||
? () => JSON.parse(fs.readFileSync(path.join(output, 'client_assets.json'), 'utf-8'))
|
||||
: (assets => () => assets)(JSON.parse(fs.readFileSync(path.join(output, 'client_assets.json'), 'utf-8')));
|
||||
|
||||
const template = dev()
|
||||
? () => fs.readFileSync(`${locations.app()}/template.html`, 'utf-8')
|
||||
: (str => () => str)(fs.readFileSync(`${locations.dest()}/template.html`, 'utf-8'));
|
||||
|
||||
const { server_routes, pages } = manifest;
|
||||
const error_route = manifest.error;
|
||||
|
||||
function handle_error(req: Req, res: ServerResponse, statusCode: number, error: Error | string) {
|
||||
handle_page({
|
||||
pattern: null,
|
||||
parts: [
|
||||
{ name: null, component: error_route }
|
||||
]
|
||||
}, req, res, statusCode, error);
|
||||
}
|
||||
|
||||
function handle_page(page: Page, req: Req, res: ServerResponse, status = 200, error: Error | string = null) {
|
||||
const get_params = page.parts[page.parts.length - 1].params || (() => ({}));
|
||||
const match = error ? null : page.pattern.exec(req.path);
|
||||
|
||||
const chunks: Record<string, string | string[]> = get_chunks();
|
||||
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
|
||||
// preload main.js and current route
|
||||
// TODO detect other stuff we can preload? images, CSS, fonts?
|
||||
let preloaded_chunks = Array.isArray(chunks.main) ? chunks.main : [chunks.main];
|
||||
if (!error) {
|
||||
page.parts.forEach(part => {
|
||||
// using concat because it could be a string or an array. thanks webpack!
|
||||
preloaded_chunks = preloaded_chunks.concat(chunks[part.name]);
|
||||
});
|
||||
}
|
||||
|
||||
const link = preloaded_chunks
|
||||
.filter(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) : 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`);
|
||||
}
|
||||
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) {
|
||||
const cookies: Record<string, string> = {};
|
||||
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: {}
|
||||
})
|
||||
: {};
|
||||
|
||||
Promise.all([root_preloaded].concat(page.parts.map(part => {
|
||||
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) {
|
||||
res.statusCode = redirect.statusCode;
|
||||
res.setHeader('Location', `${req.baseUrl}/${redirect.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: {}
|
||||
});
|
||||
|
||||
let level = data.child;
|
||||
for (let i = 0; i < page.parts.length; i += 1) {
|
||||
const part = page.parts[i];
|
||||
const get_params = part.params || (() => ({}));
|
||||
|
||||
Object.assign(level, {
|
||||
segment: segments[i],
|
||||
component: part.component,
|
||||
props: Object.assign({}, props, {
|
||||
params: get_params(match)
|
||||
}, preloaded[i + 1])
|
||||
});
|
||||
|
||||
level.props.child = <Props["child"]>{};
|
||||
level = level.props.child;
|
||||
}
|
||||
|
||||
const { html, head, css } = manifest.root.render(data, {
|
||||
store
|
||||
});
|
||||
|
||||
let scripts = []
|
||||
.concat(chunks.main) // chunks main might be an array. it might not! thanks, webpack
|
||||
.filter(file => !file.match(/\.map$/))
|
||||
.map(file => `<script src='${req.baseUrl}/client/${file}'></script>`)
|
||||
.join('');
|
||||
|
||||
let inline_script = `__SAPPER__={${[
|
||||
error && `error:1`,
|
||||
`baseUrl: "${req.baseUrl}"`,
|
||||
serialized.preloaded && `preloaded: ${serialized.preloaded}`,
|
||||
serialized.store && `store: ${serialized.store}`
|
||||
].filter(Boolean).join(',')}};`;
|
||||
|
||||
const has_service_worker = fs.existsSync(path.join(locations.dest(), 'service-worker.js'));
|
||||
if (has_service_worker) {
|
||||
inline_script += `if ('serviceWorker' in navigator) navigator.serviceWorker.register('${req.baseUrl}/service-worker.js');`;
|
||||
}
|
||||
|
||||
const body = template()
|
||||
.replace('%sapper.base%', () => `<base href="${req.baseUrl}/">`)
|
||||
.replace('%sapper.scripts%', () => `<script>${inline_script}</script>${scripts}`)
|
||||
.replace('%sapper.html%', () => html)
|
||||
.replace('%sapper.head%', () => `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`)
|
||||
.replace('%sapper.styles%', () => (css && css.code ? `<style>${css.code}</style>` : ''));
|
||||
|
||||
res.statusCode = status;
|
||||
res.end(body);
|
||||
|
||||
if (process.send) {
|
||||
process.send({
|
||||
__sapper__: true,
|
||||
event: 'file',
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
status: 200,
|
||||
type: 'text/html',
|
||||
body
|
||||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
if (error) {
|
||||
// we encountered an error while rendering the error page — oops
|
||||
res.statusCode = 500;
|
||||
res.end(`<pre>${escape_html(err.message)}</pre>`);
|
||||
} else {
|
||||
handle_error(req, res, 500, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return function find_route(req: Req, res: ServerResponse) {
|
||||
if (!server_routes.some(route => route.pattern.test(req.path))) {
|
||||
for (const page of pages) {
|
||||
if (page.pattern.test(req.path)) {
|
||||
handle_page(page, req, res);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handle_error(req, res, 404, 'Not found');
|
||||
};
|
||||
}
|
||||
|
||||
function compose_handlers(handlers: Handler[]) {
|
||||
return (req: Req, res: ServerResponse, next: () => void) => {
|
||||
let i = 0;
|
||||
function go() {
|
||||
const handler = handlers[i];
|
||||
|
||||
if (handler) {
|
||||
handler(req, res, () => {
|
||||
i += 1;
|
||||
go();
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
go();
|
||||
};
|
||||
}
|
||||
|
||||
function try_serialize(data: any) {
|
||||
try {
|
||||
return devalue(data);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function escape_html(html: string) {
|
||||
const chars: Record<string, string> = {
|
||||
'"' : 'quot',
|
||||
"'": '#39',
|
||||
'&': 'amp',
|
||||
'<' : 'lt',
|
||||
'>' : 'gt'
|
||||
};
|
||||
|
||||
return html.replace(/["'&<>]/g, c => `&${chars[c]};`);
|
||||
}
|
||||
767
src/middleware/mime-types.md
Normal file
767
src/middleware/mime-types.md
Normal file
@@ -0,0 +1,767 @@
|
||||
application/andrew-inset ez
|
||||
application/applixware aw
|
||||
application/atom+xml atom
|
||||
application/atomcat+xml atomcat
|
||||
application/atomsvc+xml atomsvc
|
||||
application/ccxml+xml ccxml
|
||||
application/cdmi-capability cdmia
|
||||
application/cdmi-container cdmic
|
||||
application/cdmi-domain cdmid
|
||||
application/cdmi-object cdmio
|
||||
application/cdmi-queue cdmiq
|
||||
application/cu-seeme cu
|
||||
application/davmount+xml davmount
|
||||
application/docbook+xml dbk
|
||||
application/dssc+der dssc
|
||||
application/dssc+xml xdssc
|
||||
application/ecmascript ecma
|
||||
application/emma+xml emma
|
||||
application/epub+zip epub
|
||||
application/exi exi
|
||||
application/font-tdpfr pfr
|
||||
application/gml+xml gml
|
||||
application/gpx+xml gpx
|
||||
application/gxf gxf
|
||||
application/hyperstudio stk
|
||||
application/inkml+xml ink inkml
|
||||
application/ipfix ipfix
|
||||
application/java-archive jar
|
||||
application/java-serialized-object ser
|
||||
application/java-vm class
|
||||
application/javascript js
|
||||
application/json json map
|
||||
application/jsonml+json jsonml
|
||||
application/lost+xml lostxml
|
||||
application/mac-binhex40 hqx
|
||||
application/mac-compactpro cpt
|
||||
application/mads+xml mads
|
||||
application/marc mrc
|
||||
application/marcxml+xml mrcx
|
||||
application/mathematica ma nb mb
|
||||
application/mathml+xml mathml
|
||||
application/mbox mbox
|
||||
application/mediaservercontrol+xml mscml
|
||||
application/metalink+xml metalink
|
||||
application/metalink4+xml meta4
|
||||
application/mets+xml mets
|
||||
application/mods+xml mods
|
||||
application/mp21 m21 mp21
|
||||
application/mp4 mp4s
|
||||
application/msword doc dot
|
||||
application/mxf mxf
|
||||
application/octet-stream bin dms lrf mar so dist distz pkg bpk dump elc deploy
|
||||
application/oda oda
|
||||
application/oebps-package+xml opf
|
||||
application/ogg ogx
|
||||
application/omdoc+xml omdoc
|
||||
application/onenote onetoc onetoc2 onetmp onepkg
|
||||
application/oxps oxps
|
||||
application/patch-ops-error+xml xer
|
||||
application/pdf pdf
|
||||
application/pgp-encrypted pgp
|
||||
application/pgp-signature asc sig
|
||||
application/pics-rules prf
|
||||
application/pkcs10 p10
|
||||
application/pkcs7-mime p7m p7c
|
||||
application/pkcs7-signature p7s
|
||||
application/pkcs8 p8
|
||||
application/pkix-attr-cert ac
|
||||
application/pkix-cert cer
|
||||
application/pkix-crl crl
|
||||
application/pkix-pkipath pkipath
|
||||
application/pkixcmp pki
|
||||
application/pls+xml pls
|
||||
application/postscript ai eps ps
|
||||
application/prs.cww cww
|
||||
application/pskc+xml pskcxml
|
||||
application/rdf+xml rdf
|
||||
application/reginfo+xml rif
|
||||
application/relax-ng-compact-syntax rnc
|
||||
application/resource-lists+xml rl
|
||||
application/resource-lists-diff+xml rld
|
||||
application/rls-services+xml rs
|
||||
application/rpki-ghostbusters gbr
|
||||
application/rpki-manifest mft
|
||||
application/rpki-roa roa
|
||||
application/rsd+xml rsd
|
||||
application/rss+xml rss
|
||||
application/rtf rtf
|
||||
application/sbml+xml sbml
|
||||
application/scvp-cv-request scq
|
||||
application/scvp-cv-response scs
|
||||
application/scvp-vp-request spq
|
||||
application/scvp-vp-response spp
|
||||
application/sdp sdp
|
||||
application/set-payment-initiation setpay
|
||||
application/set-registration-initiation setreg
|
||||
application/shf+xml shf
|
||||
application/smil+xml smi smil
|
||||
application/sparql-query rq
|
||||
application/sparql-results+xml srx
|
||||
application/srgs gram
|
||||
application/srgs+xml grxml
|
||||
application/sru+xml sru
|
||||
application/ssdl+xml ssdl
|
||||
application/ssml+xml ssml
|
||||
application/tei+xml tei teicorpus
|
||||
application/thraud+xml tfi
|
||||
application/timestamped-data tsd
|
||||
application/vnd.3gpp.pic-bw-large plb
|
||||
application/vnd.3gpp.pic-bw-small psb
|
||||
application/vnd.3gpp.pic-bw-var pvb
|
||||
application/vnd.3gpp2.tcap tcap
|
||||
application/vnd.3m.post-it-notes pwn
|
||||
application/vnd.accpac.simply.aso aso
|
||||
application/vnd.accpac.simply.imp imp
|
||||
application/vnd.acucobol acu
|
||||
application/vnd.acucorp atc acutc
|
||||
application/vnd.adobe.air-application-installer-package+zip air
|
||||
application/vnd.adobe.formscentral.fcdt fcdt
|
||||
application/vnd.adobe.fxp fxp fxpl
|
||||
application/vnd.adobe.xdp+xml xdp
|
||||
application/vnd.adobe.xfdf xfdf
|
||||
application/vnd.ahead.space ahead
|
||||
application/vnd.airzip.filesecure.azf azf
|
||||
application/vnd.airzip.filesecure.azs azs
|
||||
application/vnd.amazon.ebook azw
|
||||
application/vnd.americandynamics.acc acc
|
||||
application/vnd.amiga.ami ami
|
||||
application/vnd.android.package-archive apk
|
||||
application/vnd.anser-web-certificate-issue-initiation cii
|
||||
application/vnd.anser-web-funds-transfer-initiation fti
|
||||
application/vnd.antix.game-component atx
|
||||
application/vnd.apple.installer+xml mpkg
|
||||
application/vnd.apple.mpegurl m3u8
|
||||
application/vnd.aristanetworks.swi swi
|
||||
application/vnd.astraea-software.iota iota
|
||||
application/vnd.audiograph aep
|
||||
application/vnd.blueice.multipass mpm
|
||||
application/vnd.bmi bmi
|
||||
application/vnd.businessobjects rep
|
||||
application/vnd.chemdraw+xml cdxml
|
||||
application/vnd.chipnuts.karaoke-mmd mmd
|
||||
application/vnd.cinderella cdy
|
||||
application/vnd.claymore cla
|
||||
application/vnd.cloanto.rp9 rp9
|
||||
application/vnd.clonk.c4group c4g c4d c4f c4p c4u
|
||||
application/vnd.cluetrust.cartomobile-config c11amc
|
||||
application/vnd.cluetrust.cartomobile-config-pkg c11amz
|
||||
application/vnd.commonspace csp
|
||||
application/vnd.contact.cmsg cdbcmsg
|
||||
application/vnd.cosmocaller cmc
|
||||
application/vnd.crick.clicker clkx
|
||||
application/vnd.crick.clicker.keyboard clkk
|
||||
application/vnd.crick.clicker.palette clkp
|
||||
application/vnd.crick.clicker.template clkt
|
||||
application/vnd.crick.clicker.wordbank clkw
|
||||
application/vnd.criticaltools.wbs+xml wbs
|
||||
application/vnd.ctc-posml pml
|
||||
application/vnd.cups-ppd ppd
|
||||
application/vnd.curl.car car
|
||||
application/vnd.curl.pcurl pcurl
|
||||
application/vnd.dart dart
|
||||
application/vnd.data-vision.rdz rdz
|
||||
application/vnd.dece.data uvf uvvf uvd uvvd
|
||||
application/vnd.dece.ttml+xml uvt uvvt
|
||||
application/vnd.dece.unspecified uvx uvvx
|
||||
application/vnd.dece.zip uvz uvvz
|
||||
application/vnd.denovo.fcselayout-link fe_launch
|
||||
application/vnd.dna dna
|
||||
application/vnd.dolby.mlp mlp
|
||||
application/vnd.dpgraph dpg
|
||||
application/vnd.dreamfactory dfac
|
||||
application/vnd.ds-keypoint kpxx
|
||||
application/vnd.dvb.ait ait
|
||||
application/vnd.dvb.service svc
|
||||
application/vnd.dynageo geo
|
||||
application/vnd.ecowin.chart mag
|
||||
application/vnd.enliven nml
|
||||
application/vnd.epson.esf esf
|
||||
application/vnd.epson.msf msf
|
||||
application/vnd.epson.quickanime qam
|
||||
application/vnd.epson.salt slt
|
||||
application/vnd.epson.ssf ssf
|
||||
application/vnd.eszigno3+xml es3 et3
|
||||
application/vnd.ezpix-album ez2
|
||||
application/vnd.ezpix-package ez3
|
||||
application/vnd.fdf fdf
|
||||
application/vnd.fdsn.mseed mseed
|
||||
application/vnd.fdsn.seed seed dataless
|
||||
application/vnd.flographit gph
|
||||
application/vnd.fluxtime.clip ftc
|
||||
application/vnd.framemaker fm frame maker book
|
||||
application/vnd.frogans.fnc fnc
|
||||
application/vnd.frogans.ltf ltf
|
||||
application/vnd.fsc.weblaunch fsc
|
||||
application/vnd.fujitsu.oasys oas
|
||||
application/vnd.fujitsu.oasys2 oa2
|
||||
application/vnd.fujitsu.oasys3 oa3
|
||||
application/vnd.fujitsu.oasysgp fg5
|
||||
application/vnd.fujitsu.oasysprs bh2
|
||||
application/vnd.fujixerox.ddd ddd
|
||||
application/vnd.fujixerox.docuworks xdw
|
||||
application/vnd.fujixerox.docuworks.binder xbd
|
||||
application/vnd.fuzzysheet fzs
|
||||
application/vnd.genomatix.tuxedo txd
|
||||
application/vnd.geogebra.file ggb
|
||||
application/vnd.geogebra.tool ggt
|
||||
application/vnd.geometry-explorer gex gre
|
||||
application/vnd.geonext gxt
|
||||
application/vnd.geoplan g2w
|
||||
application/vnd.geospace g3w
|
||||
application/vnd.gmx gmx
|
||||
application/vnd.google-earth.kml+xml kml
|
||||
application/vnd.google-earth.kmz kmz
|
||||
application/vnd.grafeq gqf gqs
|
||||
application/vnd.groove-account gac
|
||||
application/vnd.groove-help ghf
|
||||
application/vnd.groove-identity-message gim
|
||||
application/vnd.groove-injector grv
|
||||
application/vnd.groove-tool-message gtm
|
||||
application/vnd.groove-tool-template tpl
|
||||
application/vnd.groove-vcard vcg
|
||||
application/vnd.hal+xml hal
|
||||
application/vnd.handheld-entertainment+xml zmm
|
||||
application/vnd.hbci hbci
|
||||
application/vnd.hhe.lesson-player les
|
||||
application/vnd.hp-hpgl hpgl
|
||||
application/vnd.hp-hpid hpid
|
||||
application/vnd.hp-hps hps
|
||||
application/vnd.hp-jlyt jlt
|
||||
application/vnd.hp-pcl pcl
|
||||
application/vnd.hp-pclxl pclxl
|
||||
application/vnd.hydrostatix.sof-data sfd-hdstx
|
||||
application/vnd.ibm.minipay mpy
|
||||
application/vnd.ibm.modcap afp listafp list3820
|
||||
application/vnd.ibm.rights-management irm
|
||||
application/vnd.ibm.secure-container sc
|
||||
application/vnd.iccprofile icc icm
|
||||
application/vnd.igloader igl
|
||||
application/vnd.immervision-ivp ivp
|
||||
application/vnd.immervision-ivu ivu
|
||||
application/vnd.insors.igm igm
|
||||
application/vnd.intercon.formnet xpw xpx
|
||||
application/vnd.intergeo i2g
|
||||
application/vnd.intu.qbo qbo
|
||||
application/vnd.intu.qfx qfx
|
||||
application/vnd.ipunplugged.rcprofile rcprofile
|
||||
application/vnd.irepository.package+xml irp
|
||||
application/vnd.is-xpr xpr
|
||||
application/vnd.isac.fcs fcs
|
||||
application/vnd.jam jam
|
||||
application/vnd.jcp.javame.midlet-rms rms
|
||||
application/vnd.jisp jisp
|
||||
application/vnd.joost.joda-archive joda
|
||||
application/vnd.kahootz ktz ktr
|
||||
application/vnd.kde.karbon karbon
|
||||
application/vnd.kde.kchart chrt
|
||||
application/vnd.kde.kformula kfo
|
||||
application/vnd.kde.kivio flw
|
||||
application/vnd.kde.kontour kon
|
||||
application/vnd.kde.kpresenter kpr kpt
|
||||
application/vnd.kde.kspread ksp
|
||||
application/vnd.kde.kword kwd kwt
|
||||
application/vnd.kenameaapp htke
|
||||
application/vnd.kidspiration kia
|
||||
application/vnd.kinar kne knp
|
||||
application/vnd.koan skp skd skt skm
|
||||
application/vnd.kodak-descriptor sse
|
||||
application/vnd.las.las+xml lasxml
|
||||
application/vnd.llamagraphics.life-balance.desktop lbd
|
||||
application/vnd.llamagraphics.life-balance.exchange+xml lbe
|
||||
application/vnd.lotus-1-2-3 123
|
||||
application/vnd.lotus-approach apr
|
||||
application/vnd.lotus-freelance pre
|
||||
application/vnd.lotus-notes nsf
|
||||
application/vnd.lotus-organizer org
|
||||
application/vnd.lotus-screencam scm
|
||||
application/vnd.lotus-wordpro lwp
|
||||
application/vnd.macports.portpkg portpkg
|
||||
application/vnd.mcd mcd
|
||||
application/vnd.medcalcdata mc1
|
||||
application/vnd.mediastation.cdkey cdkey
|
||||
application/vnd.mfer mwf
|
||||
application/vnd.mfmp mfm
|
||||
application/vnd.micrografx.flo flo
|
||||
application/vnd.micrografx.igx igx
|
||||
application/vnd.mif mif
|
||||
application/vnd.mobius.daf daf
|
||||
application/vnd.mobius.dis dis
|
||||
application/vnd.mobius.mbk mbk
|
||||
application/vnd.mobius.mqy mqy
|
||||
application/vnd.mobius.msl msl
|
||||
application/vnd.mobius.plc plc
|
||||
application/vnd.mobius.txf txf
|
||||
application/vnd.mophun.application mpn
|
||||
application/vnd.mophun.certificate mpc
|
||||
application/vnd.mozilla.xul+xml xul
|
||||
application/vnd.ms-artgalry cil
|
||||
application/vnd.ms-cab-compressed cab
|
||||
application/vnd.ms-excel xls xlm xla xlc xlt xlw
|
||||
application/vnd.ms-excel.addin.macroenabled.12 xlam
|
||||
application/vnd.ms-excel.sheet.binary.macroenabled.12 xlsb
|
||||
application/vnd.ms-excel.sheet.macroenabled.12 xlsm
|
||||
application/vnd.ms-excel.template.macroenabled.12 xltm
|
||||
application/vnd.ms-fontobject eot
|
||||
application/vnd.ms-htmlhelp chm
|
||||
application/vnd.ms-ims ims
|
||||
application/vnd.ms-lrm lrm
|
||||
application/vnd.ms-officetheme thmx
|
||||
application/vnd.ms-pki.seccat cat
|
||||
application/vnd.ms-pki.stl stl
|
||||
application/vnd.ms-powerpoint ppt pps pot
|
||||
application/vnd.ms-powerpoint.addin.macroenabled.12 ppam
|
||||
application/vnd.ms-powerpoint.presentation.macroenabled.12 pptm
|
||||
application/vnd.ms-powerpoint.slide.macroenabled.12 sldm
|
||||
application/vnd.ms-powerpoint.slideshow.macroenabled.12 ppsm
|
||||
application/vnd.ms-powerpoint.template.macroenabled.12 potm
|
||||
application/vnd.ms-project mpp mpt
|
||||
application/vnd.ms-word.document.macroenabled.12 docm
|
||||
application/vnd.ms-word.template.macroenabled.12 dotm
|
||||
application/vnd.ms-works wps wks wcm wdb
|
||||
application/vnd.ms-wpl wpl
|
||||
application/vnd.ms-xpsdocument xps
|
||||
application/vnd.mseq mseq
|
||||
application/vnd.musician mus
|
||||
application/vnd.muvee.style msty
|
||||
application/vnd.mynfc taglet
|
||||
application/vnd.neurolanguage.nlu nlu
|
||||
application/vnd.nitf ntf nitf
|
||||
application/vnd.noblenet-directory nnd
|
||||
application/vnd.noblenet-sealer nns
|
||||
application/vnd.noblenet-web nnw
|
||||
application/vnd.nokia.n-gage.data ngdat
|
||||
application/vnd.nokia.n-gage.symbian.install n-gage
|
||||
application/vnd.nokia.radio-preset rpst
|
||||
application/vnd.nokia.radio-presets rpss
|
||||
application/vnd.novadigm.edm edm
|
||||
application/vnd.novadigm.edx edx
|
||||
application/vnd.novadigm.ext ext
|
||||
application/vnd.oasis.opendocument.chart odc
|
||||
application/vnd.oasis.opendocument.chart-template otc
|
||||
application/vnd.oasis.opendocument.database odb
|
||||
application/vnd.oasis.opendocument.formula odf
|
||||
application/vnd.oasis.opendocument.formula-template odft
|
||||
application/vnd.oasis.opendocument.graphics odg
|
||||
application/vnd.oasis.opendocument.graphics-template otg
|
||||
application/vnd.oasis.opendocument.image odi
|
||||
application/vnd.oasis.opendocument.image-template oti
|
||||
application/vnd.oasis.opendocument.presentation odp
|
||||
application/vnd.oasis.opendocument.presentation-template otp
|
||||
application/vnd.oasis.opendocument.spreadsheet ods
|
||||
application/vnd.oasis.opendocument.spreadsheet-template ots
|
||||
application/vnd.oasis.opendocument.text odt
|
||||
application/vnd.oasis.opendocument.text-master odm
|
||||
application/vnd.oasis.opendocument.text-template ott
|
||||
application/vnd.oasis.opendocument.text-web oth
|
||||
application/vnd.olpc-sugar xo
|
||||
application/vnd.oma.dd2+xml dd2
|
||||
application/vnd.openofficeorg.extension oxt
|
||||
application/vnd.openxmlformats-officedocument.presentationml.presentation pptx
|
||||
application/vnd.openxmlformats-officedocument.presentationml.slide sldx
|
||||
application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx
|
||||
application/vnd.openxmlformats-officedocument.presentationml.template potx
|
||||
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx
|
||||
application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx
|
||||
application/vnd.openxmlformats-officedocument.wordprocessingml.document docx
|
||||
application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx
|
||||
application/vnd.osgeo.mapguide.package mgp
|
||||
application/vnd.osgi.dp dp
|
||||
application/vnd.osgi.subsystem esa
|
||||
application/vnd.palm pdb pqa oprc
|
||||
application/vnd.pawaafile paw
|
||||
application/vnd.pg.format str
|
||||
application/vnd.pg.osasli ei6
|
||||
application/vnd.picsel efif
|
||||
application/vnd.pmi.widget wg
|
||||
application/vnd.pocketlearn plf
|
||||
application/vnd.powerbuilder6 pbd
|
||||
application/vnd.previewsystems.box box
|
||||
application/vnd.proteus.magazine mgz
|
||||
application/vnd.publishare-delta-tree qps
|
||||
application/vnd.pvi.ptid1 ptid
|
||||
application/vnd.quark.quarkxpress qxd qxt qwd qwt qxl qxb
|
||||
application/vnd.realvnc.bed bed
|
||||
application/vnd.recordare.musicxml mxl
|
||||
application/vnd.recordare.musicxml+xml musicxml
|
||||
application/vnd.rig.cryptonote cryptonote
|
||||
application/vnd.rim.cod cod
|
||||
application/vnd.rn-realmedia rm
|
||||
application/vnd.rn-realmedia-vbr rmvb
|
||||
application/vnd.route66.link66+xml link66
|
||||
application/vnd.sailingtracker.track st
|
||||
application/vnd.seemail see
|
||||
application/vnd.sema sema
|
||||
application/vnd.semd semd
|
||||
application/vnd.semf semf
|
||||
application/vnd.shana.informed.formdata ifm
|
||||
application/vnd.shana.informed.formtemplate itp
|
||||
application/vnd.shana.informed.interchange iif
|
||||
application/vnd.shana.informed.package ipk
|
||||
application/vnd.simtech-mindmapper twd twds
|
||||
application/vnd.smaf mmf
|
||||
application/vnd.smart.teacher teacher
|
||||
application/vnd.solent.sdkm+xml sdkm sdkd
|
||||
application/vnd.spotfire.dxp dxp
|
||||
application/vnd.spotfire.sfs sfs
|
||||
application/vnd.stardivision.calc sdc
|
||||
application/vnd.stardivision.draw sda
|
||||
application/vnd.stardivision.impress sdd
|
||||
application/vnd.stardivision.math smf
|
||||
application/vnd.stardivision.writer sdw vor
|
||||
application/vnd.stardivision.writer-global sgl
|
||||
application/vnd.stepmania.package smzip
|
||||
application/vnd.stepmania.stepchart sm
|
||||
application/vnd.sun.xml.calc sxc
|
||||
application/vnd.sun.xml.calc.template stc
|
||||
application/vnd.sun.xml.draw sxd
|
||||
application/vnd.sun.xml.draw.template std
|
||||
application/vnd.sun.xml.impress sxi
|
||||
application/vnd.sun.xml.impress.template sti
|
||||
application/vnd.sun.xml.math sxm
|
||||
application/vnd.sun.xml.writer sxw
|
||||
application/vnd.sun.xml.writer.global sxg
|
||||
application/vnd.sun.xml.writer.template stw
|
||||
application/vnd.sus-calendar sus susp
|
||||
application/vnd.svd svd
|
||||
application/vnd.symbian.install sis sisx
|
||||
application/vnd.syncml+xml xsm
|
||||
application/vnd.syncml.dm+wbxml bdm
|
||||
application/vnd.syncml.dm+xml xdm
|
||||
application/vnd.tao.intent-module-archive tao
|
||||
application/vnd.tcpdump.pcap pcap cap dmp
|
||||
application/vnd.tmobile-livetv tmo
|
||||
application/vnd.trid.tpt tpt
|
||||
application/vnd.triscape.mxs mxs
|
||||
application/vnd.trueapp tra
|
||||
application/vnd.ufdl ufd ufdl
|
||||
application/vnd.uiq.theme utz
|
||||
application/vnd.umajin umj
|
||||
application/vnd.unity unityweb
|
||||
application/vnd.uoml+xml uoml
|
||||
application/vnd.vcx vcx
|
||||
application/vnd.visio vsd vst vss vsw
|
||||
application/vnd.visionary vis
|
||||
application/vnd.vsf vsf
|
||||
application/vnd.wap.wbxml wbxml
|
||||
application/vnd.wap.wmlc wmlc
|
||||
application/vnd.wap.wmlscriptc wmlsc
|
||||
application/vnd.webturbo wtb
|
||||
application/vnd.wolfram.player nbp
|
||||
application/vnd.wordperfect wpd
|
||||
application/vnd.wqd wqd
|
||||
application/vnd.wt.stf stf
|
||||
application/vnd.xara xar
|
||||
application/vnd.xfdl xfdl
|
||||
application/vnd.yamaha.hv-dic hvd
|
||||
application/vnd.yamaha.hv-script hvs
|
||||
application/vnd.yamaha.hv-voice hvp
|
||||
application/vnd.yamaha.openscoreformat osf
|
||||
application/vnd.yamaha.openscoreformat.osfpvg+xml osfpvg
|
||||
application/vnd.yamaha.smaf-audio saf
|
||||
application/vnd.yamaha.smaf-phrase spf
|
||||
application/vnd.yellowriver-custom-menu cmp
|
||||
application/vnd.zul zir zirz
|
||||
application/vnd.zzazz.deck+xml zaz
|
||||
application/voicexml+xml vxml
|
||||
application/widget wgt
|
||||
application/winhlp hlp
|
||||
application/wsdl+xml wsdl
|
||||
application/wspolicy+xml wspolicy
|
||||
application/x-7z-compressed 7z
|
||||
application/x-abiword abw
|
||||
application/x-ace-compressed ace
|
||||
application/x-apple-diskimage dmg
|
||||
application/x-authorware-bin aab x32 u32 vox
|
||||
application/x-authorware-map aam
|
||||
application/x-authorware-seg aas
|
||||
application/x-bcpio bcpio
|
||||
application/x-bittorrent torrent
|
||||
application/x-blorb blb blorb
|
||||
application/x-bzip bz
|
||||
application/x-bzip2 bz2 boz
|
||||
application/x-cbr cbr cba cbt cbz cb7
|
||||
application/x-cdlink vcd
|
||||
application/x-cfs-compressed cfs
|
||||
application/x-chat chat
|
||||
application/x-chess-pgn pgn
|
||||
application/x-conference nsc
|
||||
application/x-cpio cpio
|
||||
application/x-csh csh
|
||||
application/x-debian-package deb udeb
|
||||
application/x-dgc-compressed dgc
|
||||
application/x-director dir dcr dxr cst cct cxt w3d fgd swa
|
||||
application/x-doom wad
|
||||
application/x-dtbncx+xml ncx
|
||||
application/x-dtbook+xml dtb
|
||||
application/x-dtbresource+xml res
|
||||
application/x-dvi dvi
|
||||
application/x-envoy evy
|
||||
application/x-eva eva
|
||||
application/x-font-bdf bdf
|
||||
application/x-font-ghostscript gsf
|
||||
application/x-font-linux-psf psf
|
||||
application/x-font-pcf pcf
|
||||
application/x-font-snf snf
|
||||
application/x-font-type1 pfa pfb pfm afm
|
||||
application/x-freearc arc
|
||||
application/x-futuresplash spl
|
||||
application/x-gca-compressed gca
|
||||
application/x-glulx ulx
|
||||
application/x-gnumeric gnumeric
|
||||
application/x-gramps-xml gramps
|
||||
application/x-gtar gtar
|
||||
application/x-hdf hdf
|
||||
application/x-install-instructions install
|
||||
application/x-iso9660-image iso
|
||||
application/x-java-jnlp-file jnlp
|
||||
application/x-latex latex
|
||||
application/x-lzh-compressed lzh lha
|
||||
application/x-mie mie
|
||||
application/x-mobipocket-ebook prc mobi
|
||||
application/x-ms-application application
|
||||
application/x-ms-shortcut lnk
|
||||
application/x-ms-wmd wmd
|
||||
application/x-ms-wmz wmz
|
||||
application/x-ms-xbap xbap
|
||||
application/x-msaccess mdb
|
||||
application/x-msbinder obd
|
||||
application/x-mscardfile crd
|
||||
application/x-msclip clp
|
||||
application/x-msdownload exe dll com bat msi
|
||||
application/x-msmediaview mvb m13 m14
|
||||
application/x-msmetafile wmf wmz emf emz
|
||||
application/x-msmoney mny
|
||||
application/x-mspublisher pub
|
||||
application/x-msschedule scd
|
||||
application/x-msterminal trm
|
||||
application/x-mswrite wri
|
||||
application/x-netcdf nc cdf
|
||||
application/x-nzb nzb
|
||||
application/x-pkcs12 p12 pfx
|
||||
application/x-pkcs7-certificates p7b spc
|
||||
application/x-pkcs7-certreqresp p7r
|
||||
application/x-rar-compressed rar
|
||||
application/x-research-info-systems ris
|
||||
application/x-sh sh
|
||||
application/x-shar shar
|
||||
application/x-shockwave-flash swf
|
||||
application/x-silverlight-app xap
|
||||
application/x-sql sql
|
||||
application/x-stuffit sit
|
||||
application/x-stuffitx sitx
|
||||
application/x-subrip srt
|
||||
application/x-sv4cpio sv4cpio
|
||||
application/x-sv4crc sv4crc
|
||||
application/x-t3vm-image t3
|
||||
application/x-tads gam
|
||||
application/x-tar tar
|
||||
application/x-tcl tcl
|
||||
application/x-tex tex
|
||||
application/x-tex-tfm tfm
|
||||
application/x-texinfo texinfo texi
|
||||
application/x-tgif obj
|
||||
application/x-ustar ustar
|
||||
application/x-wais-source src
|
||||
application/x-x509-ca-cert der crt
|
||||
application/x-xfig fig
|
||||
application/x-xliff+xml xlf
|
||||
application/x-xpinstall xpi
|
||||
application/x-xz xz
|
||||
application/x-zmachine z1 z2 z3 z4 z5 z6 z7 z8
|
||||
application/xaml+xml xaml
|
||||
application/xcap-diff+xml xdf
|
||||
application/xenc+xml xenc
|
||||
application/xhtml+xml xhtml xht
|
||||
application/xml xml xsl
|
||||
application/xml-dtd dtd
|
||||
application/xop+xml xop
|
||||
application/xproc+xml xpl
|
||||
application/xslt+xml xslt
|
||||
application/xspf+xml xspf
|
||||
application/xv+xml mxml xhvml xvml xvm
|
||||
application/yang yang
|
||||
application/yin+xml yin
|
||||
application/zip zip
|
||||
audio/adpcm adp
|
||||
audio/basic au snd
|
||||
audio/midi mid midi kar rmi
|
||||
audio/mp4 m4a mp4a
|
||||
audio/mpeg mpga mp2 mp2a mp3 m2a m3a
|
||||
audio/ogg oga ogg spx
|
||||
audio/s3m s3m
|
||||
audio/silk sil
|
||||
audio/vnd.dece.audio uva uvva
|
||||
audio/vnd.digital-winds eol
|
||||
audio/vnd.dra dra
|
||||
audio/vnd.dts dts
|
||||
audio/vnd.dts.hd dtshd
|
||||
audio/vnd.lucent.voice lvp
|
||||
audio/vnd.ms-playready.media.pya pya
|
||||
audio/vnd.nuera.ecelp4800 ecelp4800
|
||||
audio/vnd.nuera.ecelp7470 ecelp7470
|
||||
audio/vnd.nuera.ecelp9600 ecelp9600
|
||||
audio/vnd.rip rip
|
||||
audio/webm weba
|
||||
audio/x-aac aac
|
||||
audio/x-aiff aif aiff aifc
|
||||
audio/x-caf caf
|
||||
audio/x-flac flac
|
||||
audio/x-matroska mka
|
||||
audio/x-mpegurl m3u
|
||||
audio/x-ms-wax wax
|
||||
audio/x-ms-wma wma
|
||||
audio/x-pn-realaudio ram ra
|
||||
audio/x-pn-realaudio-plugin rmp
|
||||
audio/x-wav wav
|
||||
audio/xm xm
|
||||
chemical/x-cdx cdx
|
||||
chemical/x-cif cif
|
||||
chemical/x-cmdf cmdf
|
||||
chemical/x-cml cml
|
||||
chemical/x-csml csml
|
||||
chemical/x-xyz xyz
|
||||
font/collection ttc
|
||||
font/otf otf
|
||||
font/ttf ttf
|
||||
font/woff woff
|
||||
font/woff2 woff2
|
||||
image/bmp bmp
|
||||
image/cgm cgm
|
||||
image/g3fax g3
|
||||
image/gif gif
|
||||
image/ief ief
|
||||
image/jpeg jpeg jpg jpe
|
||||
image/ktx ktx
|
||||
image/png png
|
||||
image/prs.btif btif
|
||||
image/sgi sgi
|
||||
image/svg+xml svg svgz
|
||||
image/tiff tiff tif
|
||||
image/vnd.adobe.photoshop psd
|
||||
image/vnd.dece.graphic uvi uvvi uvg uvvg
|
||||
image/vnd.djvu djvu djv
|
||||
image/vnd.dvb.subtitle sub
|
||||
image/vnd.dwg dwg
|
||||
image/vnd.dxf dxf
|
||||
image/vnd.fastbidsheet fbs
|
||||
image/vnd.fpx fpx
|
||||
image/vnd.fst fst
|
||||
image/vnd.fujixerox.edmics-mmr mmr
|
||||
image/vnd.fujixerox.edmics-rlc rlc
|
||||
image/vnd.ms-modi mdi
|
||||
image/vnd.ms-photo wdp
|
||||
image/vnd.net-fpx npx
|
||||
image/vnd.wap.wbmp wbmp
|
||||
image/vnd.xiff xif
|
||||
image/webp webp
|
||||
image/x-3ds 3ds
|
||||
image/x-cmu-raster ras
|
||||
image/x-cmx cmx
|
||||
image/x-freehand fh fhc fh4 fh5 fh7
|
||||
image/x-icon ico
|
||||
image/x-mrsid-image sid
|
||||
image/x-pcx pcx
|
||||
image/x-pict pic pct
|
||||
image/x-portable-anymap pnm
|
||||
image/x-portable-bitmap pbm
|
||||
image/x-portable-graymap pgm
|
||||
image/x-portable-pixmap ppm
|
||||
image/x-rgb rgb
|
||||
image/x-tga tga
|
||||
image/x-xbitmap xbm
|
||||
image/x-xpixmap xpm
|
||||
image/x-xwindowdump xwd
|
||||
message/rfc822 eml mime
|
||||
model/iges igs iges
|
||||
model/mesh msh mesh silo
|
||||
model/vnd.collada+xml dae
|
||||
model/vnd.dwf dwf
|
||||
model/vnd.gdl gdl
|
||||
model/vnd.gtw gtw
|
||||
model/vnd.mts mts
|
||||
model/vnd.vtu vtu
|
||||
model/vrml wrl vrml
|
||||
model/x3d+binary x3db x3dbz
|
||||
model/x3d+vrml x3dv x3dvz
|
||||
model/x3d+xml x3d x3dz
|
||||
text/cache-manifest appcache
|
||||
text/calendar ics ifb
|
||||
text/css css
|
||||
text/csv csv
|
||||
text/html html htm
|
||||
text/n3 n3
|
||||
text/plain txt text conf def list log in
|
||||
text/prs.lines.tag dsc
|
||||
text/richtext rtx
|
||||
text/sgml sgml sgm
|
||||
text/tab-separated-values tsv
|
||||
text/troff t tr roff man me ms
|
||||
text/turtle ttl
|
||||
text/uri-list uri uris urls
|
||||
text/vcard vcard
|
||||
text/vnd.curl curl
|
||||
text/vnd.curl.dcurl dcurl
|
||||
text/vnd.curl.mcurl mcurl
|
||||
text/vnd.curl.scurl scurl
|
||||
text/vnd.dvb.subtitle sub
|
||||
text/vnd.fly fly
|
||||
text/vnd.fmi.flexstor flx
|
||||
text/vnd.graphviz gv
|
||||
text/vnd.in3d.3dml 3dml
|
||||
text/vnd.in3d.spot spot
|
||||
text/vnd.sun.j2me.app-descriptor jad
|
||||
text/vnd.wap.wml wml
|
||||
text/vnd.wap.wmlscript wmls
|
||||
text/x-asm s asm
|
||||
text/x-c c cc cxx cpp h hh dic
|
||||
text/x-fortran f for f77 f90
|
||||
text/x-java-source java
|
||||
text/x-nfo nfo
|
||||
text/x-opml opml
|
||||
text/x-pascal p pas
|
||||
text/x-setext etx
|
||||
text/x-sfv sfv
|
||||
text/x-uuencode uu
|
||||
text/x-vcalendar vcs
|
||||
text/x-vcard vcf
|
||||
video/3gpp 3gp
|
||||
video/3gpp2 3g2
|
||||
video/h261 h261
|
||||
video/h263 h263
|
||||
video/h264 h264
|
||||
video/jpeg jpgv
|
||||
video/jpm jpm jpgm
|
||||
video/mj2 mj2 mjp2
|
||||
video/mp4 mp4 mp4v mpg4
|
||||
video/mpeg mpeg mpg mpe m1v m2v
|
||||
video/ogg ogv
|
||||
video/quicktime qt mov
|
||||
video/vnd.dece.hd uvh uvvh
|
||||
video/vnd.dece.mobile uvm uvvm
|
||||
video/vnd.dece.pd uvp uvvp
|
||||
video/vnd.dece.sd uvs uvvs
|
||||
video/vnd.dece.video uvv uvvv
|
||||
video/vnd.dvb.file dvb
|
||||
video/vnd.fvt fvt
|
||||
video/vnd.mpegurl mxu m4u
|
||||
video/vnd.ms-playready.media.pyv pyv
|
||||
video/vnd.uvvu.mp4 uvu uvvu
|
||||
video/vnd.vivo viv
|
||||
video/webm webm
|
||||
video/x-f4v f4v
|
||||
video/x-fli fli
|
||||
video/x-flv flv
|
||||
video/x-m4v m4v
|
||||
video/x-matroska mkv mk3d mks
|
||||
video/x-mng mng
|
||||
video/x-ms-asf asf asx
|
||||
video/x-ms-vob vob
|
||||
video/x-ms-wm wm
|
||||
video/x-ms-wmv wmv
|
||||
video/x-ms-wmx wmx
|
||||
video/x-ms-wvx wvx
|
||||
video/x-msvideo avi
|
||||
video/x-sgi-movie movie
|
||||
video/x-smv smv
|
||||
x-conference/x-cooltalk ice
|
||||
20
src/middleware/mime.ts
Normal file
20
src/middleware/mime.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import mime_raw from './mime-types.md';
|
||||
|
||||
const map: Map<string, string> = new Map();
|
||||
|
||||
mime_raw.split('\n').forEach((row: string) => {
|
||||
const match = /(.+?)\t+(.+)/.exec(row);
|
||||
if (!match) return;
|
||||
|
||||
const type = match[1];
|
||||
const extensions = match[2].split(' ');
|
||||
|
||||
extensions.forEach(ext => {
|
||||
map.set(ext, type);
|
||||
});
|
||||
});
|
||||
|
||||
export function lookup(file: string) {
|
||||
const match = /\.([^\.]+)$/.exec(file);
|
||||
return match && map.get(match[1]);
|
||||
}
|
||||
468
src/runtime/index.ts
Normal file
468
src/runtime/index.ts
Normal file
@@ -0,0 +1,468 @@
|
||||
import { detach, findAnchor, scroll_state, which } from './utils';
|
||||
import { Component, ComponentConstructor, Params, Query, Redirect, Manifest, RouteData, ScrollPosition, Store, Target } from './interfaces';
|
||||
|
||||
const initial_data = typeof window !== 'undefined' && window.__SAPPER__;
|
||||
|
||||
export let root: Component;
|
||||
let target: Node;
|
||||
let store: Store;
|
||||
let manifest: Manifest;
|
||||
let segments: string[] = [];
|
||||
|
||||
type RootProps = {
|
||||
path: string;
|
||||
params: Record<string, string>;
|
||||
query: Record<string, string>;
|
||||
child: Child;
|
||||
};
|
||||
|
||||
type Child = {
|
||||
segment?: string;
|
||||
props?: any;
|
||||
component?: Component;
|
||||
};
|
||||
|
||||
const root_props: RootProps = {
|
||||
path: null,
|
||||
params: null,
|
||||
query: null,
|
||||
child: {
|
||||
segment: null,
|
||||
component: null,
|
||||
props: {}
|
||||
}
|
||||
};
|
||||
|
||||
export { root as component }; // legacy reasons — drop in a future version
|
||||
|
||||
const history = typeof window !== 'undefined' ? window.history : {
|
||||
pushState: (state: any, title: string, href: string) => {},
|
||||
replaceState: (state: any, title: string, href: string) => {},
|
||||
scrollRestoration: ''
|
||||
};
|
||||
|
||||
const scroll_history: Record<string, ScrollPosition> = {};
|
||||
let uid = 1;
|
||||
let cid: number;
|
||||
|
||||
if ('scrollRestoration' in history) {
|
||||
history.scrollRestoration = 'manual';
|
||||
}
|
||||
|
||||
function select_route(url: URL): Target {
|
||||
if (url.origin !== window.location.origin) return null;
|
||||
if (!url.pathname.startsWith(initial_data.baseUrl)) return null;
|
||||
|
||||
const path = url.pathname.slice(initial_data.baseUrl.length);
|
||||
|
||||
// avoid accidental clashes between server routes and pages
|
||||
if (manifest.ignore.some(pattern => pattern.test(path))) return;
|
||||
|
||||
for (let i = 0; i < manifest.pages.length; i += 1) {
|
||||
const page = manifest.pages[i];
|
||||
|
||||
const match = page.pattern.exec(path);
|
||||
if (match) {
|
||||
const query: Record<string, string | true> = {};
|
||||
if (url.search.length > 0) {
|
||||
url.search.slice(1).split('&').forEach(searchParam => {
|
||||
const [, key, value] = /([^=]+)=(.*)/.exec(searchParam);
|
||||
query[key] = value || true;
|
||||
});
|
||||
}
|
||||
return { url, path, page, match, query };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let current_token: {};
|
||||
|
||||
function render(data: any, changed_from: number, scroll: ScrollPosition, token: {}) {
|
||||
if (current_token !== token) return;
|
||||
|
||||
if (root) {
|
||||
// first, clear out highest-level root component
|
||||
let level = data.child;
|
||||
for (let i = 0; i < changed_from; i += 1) {
|
||||
if (i === changed_from) break;
|
||||
level = level.props.child;
|
||||
}
|
||||
|
||||
const { component } = level;
|
||||
level.component = null;
|
||||
root.set({ child: data.child });
|
||||
|
||||
// then render new stuff
|
||||
level.component = component;
|
||||
root.set(data);
|
||||
} else {
|
||||
// first load — remove SSR'd <head> contents
|
||||
const start = document.querySelector('#sapper-head-start');
|
||||
const end = document.querySelector('#sapper-head-end');
|
||||
|
||||
if (start && end) {
|
||||
while (start.nextSibling !== end) detach(start.nextSibling);
|
||||
detach(start);
|
||||
detach(end);
|
||||
}
|
||||
|
||||
Object.assign(data, root_data);
|
||||
|
||||
root = new manifest.root({
|
||||
target,
|
||||
data,
|
||||
store,
|
||||
hydrate: true
|
||||
});
|
||||
}
|
||||
|
||||
if (scroll) {
|
||||
window.scrollTo(scroll.x, scroll.y);
|
||||
}
|
||||
|
||||
Object.assign(root_props, data);
|
||||
ready = true;
|
||||
}
|
||||
|
||||
function changed(a: Record<string, string | true>, b: Record<string, string | true>) {
|
||||
return JSON.stringify(a) !== JSON.stringify(b);
|
||||
}
|
||||
|
||||
let root_preload: Promise<any>;
|
||||
let root_data: any;
|
||||
|
||||
function prepare_page(target: Target): Promise<{
|
||||
redirect?: Redirect;
|
||||
data?: any;
|
||||
changed_from?: number;
|
||||
}> {
|
||||
if (root) {
|
||||
root.set({ preloading: true });
|
||||
}
|
||||
|
||||
const { page, path, query } = target;
|
||||
const new_segments = path.split('/').filter(Boolean);
|
||||
let changed_from = 0;
|
||||
|
||||
while (
|
||||
segments[changed_from] &&
|
||||
new_segments[changed_from] &&
|
||||
segments[changed_from] === new_segments[changed_from]
|
||||
) changed_from += 1;
|
||||
|
||||
let redirect: Redirect = null;
|
||||
let error: { statusCode: number, message: Error | string } = null;
|
||||
|
||||
const preload_context = {
|
||||
store,
|
||||
fetch: (url: string, opts?: any) => window.fetch(url, opts),
|
||||
redirect: (statusCode: number, location: string) => {
|
||||
if (redirect && (redirect.statusCode !== statusCode || redirect.location !== location)) {
|
||||
throw new Error(`Conflicting redirects`);
|
||||
}
|
||||
redirect = { statusCode, location };
|
||||
},
|
||||
error: (statusCode: number, message: Error | string) => {
|
||||
error = { statusCode, message };
|
||||
}
|
||||
};
|
||||
|
||||
if (!root_preload) {
|
||||
root_preload = manifest.root.preload
|
||||
? initial_data.preloaded[0] || manifest.root.preload.call(preload_context, {
|
||||
path,
|
||||
query,
|
||||
params: {}
|
||||
})
|
||||
: {};
|
||||
}
|
||||
|
||||
return Promise.all(page.parts.map(async (part, i) => {
|
||||
if (i < changed_from) return null;
|
||||
|
||||
const { default: Component } = await part.component();
|
||||
const req = {
|
||||
path,
|
||||
query,
|
||||
params: part.params ? part.params(target.match) : {}
|
||||
};
|
||||
|
||||
const preloaded = ready || !initial_data.preloaded[i + 1]
|
||||
? Component.preload ? await Component.preload.call(preload_context, req) : {}
|
||||
: initial_data.preloaded[i + 1];
|
||||
|
||||
return { Component, preloaded };
|
||||
})).catch(err => {
|
||||
error = { statusCode: 500, message: err };
|
||||
return [];
|
||||
}).then(async results => {
|
||||
if (!root_data) root_data = await root_preload;
|
||||
|
||||
if (redirect) {
|
||||
return { redirect };
|
||||
}
|
||||
|
||||
segments = new_segments;
|
||||
|
||||
const get_params = page.parts[page.parts.length - 1].params || (() => ({}));
|
||||
const params = get_params(target.match);
|
||||
|
||||
if (error) {
|
||||
const props = {
|
||||
path,
|
||||
query,
|
||||
params,
|
||||
error: typeof error.message === 'string' ? new Error(error.message) : error.message,
|
||||
status: error.statusCode
|
||||
};
|
||||
|
||||
return {
|
||||
data: Object.assign({}, props, {
|
||||
preloading: false,
|
||||
child: {
|
||||
component: manifest.error,
|
||||
props
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
const props = { path, query };
|
||||
const data = {
|
||||
path,
|
||||
preloading: false,
|
||||
child: Object.assign({}, root_props.child)
|
||||
};
|
||||
if (changed(query, root_props.query)) data.query = query;
|
||||
if (changed(params, root_props.params)) data.params = params;
|
||||
|
||||
let level = data.child;
|
||||
for (let i = 0; i < page.parts.length; i += 1) {
|
||||
const part = page.parts[i];
|
||||
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);
|
||||
} else {
|
||||
level.segment = new_segments[i];
|
||||
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;
|
||||
}
|
||||
|
||||
return { data, changed_from };
|
||||
});
|
||||
}
|
||||
|
||||
async function navigate(target: Target, id: number): Promise<any> {
|
||||
if (id) {
|
||||
// popstate or initial navigation
|
||||
cid = id;
|
||||
} else {
|
||||
// clicked on a link. preserve scroll state
|
||||
scroll_history[cid] = scroll_state();
|
||||
|
||||
id = cid = ++uid;
|
||||
scroll_history[cid] = { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
cid = id;
|
||||
|
||||
const loaded = prefetching && prefetching.href === target.url.href ?
|
||||
prefetching.promise :
|
||||
prepare_page(target);
|
||||
|
||||
prefetching = null;
|
||||
|
||||
const token = current_token = {};
|
||||
const { redirect, data, changed_from } = await loaded;
|
||||
|
||||
if (redirect) {
|
||||
await goto(redirect.location, { replaceState: true });
|
||||
} else {
|
||||
render(data, changed_from, scroll_history[id], token);
|
||||
document.activeElement.blur();
|
||||
}
|
||||
}
|
||||
|
||||
function handle_click(event: MouseEvent) {
|
||||
// Adapted from https://github.com/visionmedia/page.js
|
||||
// MIT license https://github.com/visionmedia/page.js#license
|
||||
if (which(event) !== 1) return;
|
||||
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
|
||||
if (event.defaultPrevented) return;
|
||||
|
||||
const a: HTMLAnchorElement | SVGAElement = <HTMLAnchorElement | SVGAElement>findAnchor(<Node>event.target);
|
||||
if (!a) return;
|
||||
|
||||
// check if link is inside an svg
|
||||
// in this case, both href and target are always inside an object
|
||||
const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
|
||||
const href = String(svg ? (<SVGAElement>a).href.baseVal : a.href);
|
||||
|
||||
if (href === window.location.href) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore if tag has
|
||||
// 1. 'download' attribute
|
||||
// 2. rel='external' attribute
|
||||
if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
|
||||
|
||||
// Ignore if <a> has a target
|
||||
if (svg ? (<SVGAElement>a).target.baseVal : a.target) return;
|
||||
|
||||
const url = new URL(href);
|
||||
|
||||
// Don't handle hash changes
|
||||
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
|
||||
|
||||
const target = select_route(url);
|
||||
if (target) {
|
||||
navigate(target, null);
|
||||
event.preventDefault();
|
||||
history.pushState({ id: cid }, '', url.href);
|
||||
}
|
||||
}
|
||||
|
||||
function handle_popstate(event: PopStateEvent) {
|
||||
scroll_history[cid] = scroll_state();
|
||||
|
||||
if (event.state) {
|
||||
const url = new URL(window.location.href);
|
||||
const target = select_route(url);
|
||||
if (target) {
|
||||
navigate(target, event.state.id);
|
||||
} else {
|
||||
window.location.href = window.location.href;
|
||||
}
|
||||
} else {
|
||||
// hashchange
|
||||
cid = ++uid;
|
||||
history.replaceState({ id: cid }, '', window.location.href);
|
||||
}
|
||||
}
|
||||
|
||||
let prefetching: {
|
||||
href: string;
|
||||
promise: Promise<{ redirect?: Redirect, data?: any, changed_from?: number }>;
|
||||
} = null;
|
||||
|
||||
export function prefetch(href: string) {
|
||||
const target: Target = select_route(new URL(href, document.baseURI));
|
||||
|
||||
if (target && (!prefetching || href !== prefetching.href)) {
|
||||
prefetching = {
|
||||
href,
|
||||
promise: prepare_page(target)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let mousemove_timeout: NodeJS.Timer;
|
||||
|
||||
function handle_mousemove(event: MouseEvent) {
|
||||
clearTimeout(mousemove_timeout);
|
||||
mousemove_timeout = setTimeout(() => {
|
||||
trigger_prefetch(event);
|
||||
}, 20);
|
||||
}
|
||||
|
||||
function trigger_prefetch(event: MouseEvent | TouchEvent) {
|
||||
const a: HTMLAnchorElement = <HTMLAnchorElement>findAnchor(<Node>event.target);
|
||||
if (!a || a.rel !== 'prefetch') return;
|
||||
|
||||
prefetch(a.href);
|
||||
}
|
||||
|
||||
let inited: boolean;
|
||||
let ready = false;
|
||||
|
||||
export function init(opts: {
|
||||
App: ComponentConstructor,
|
||||
target: Node,
|
||||
manifest: Manifest,
|
||||
store?: (data: any) => Store,
|
||||
routes?: any // legacy
|
||||
}) {
|
||||
if (opts instanceof HTMLElement) {
|
||||
throw new Error(`The signature of init(...) has changed — see https://sapper.svelte.technology/guide#0-11-to-0-12 for more information`);
|
||||
}
|
||||
|
||||
if (opts.routes) {
|
||||
throw new Error(`As of Sapper 0.15, opts.routes should be opts.manifest`);
|
||||
}
|
||||
|
||||
target = opts.target;
|
||||
manifest = opts.manifest;
|
||||
|
||||
if (opts && opts.store) {
|
||||
store = opts.store(initial_data.store);
|
||||
}
|
||||
|
||||
if (!inited) { // this check makes HMR possible
|
||||
window.addEventListener('click', handle_click);
|
||||
window.addEventListener('popstate', handle_popstate);
|
||||
|
||||
// prefetch
|
||||
window.addEventListener('touchstart', trigger_prefetch);
|
||||
window.addEventListener('mousemove', handle_mousemove);
|
||||
|
||||
inited = true;
|
||||
}
|
||||
|
||||
return Promise.resolve().then(() => {
|
||||
const { hash, href } = window.location;
|
||||
|
||||
const deep_linked = hash && document.getElementById(hash.slice(1));
|
||||
scroll_history[uid] = deep_linked ?
|
||||
{ x: 0, y: deep_linked.getBoundingClientRect().top } :
|
||||
scroll_state();
|
||||
|
||||
history.replaceState({ id: uid }, '', href);
|
||||
|
||||
if (!initial_data.error) {
|
||||
const target = select_route(new URL(window.location.href));
|
||||
if (target) return navigate(target, uid);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function goto(href: string, opts = { replaceState: false }) {
|
||||
const target = select_route(new URL(href, document.baseURI));
|
||||
let promise;
|
||||
|
||||
if (target) {
|
||||
promise = navigate(target, null);
|
||||
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
|
||||
} else {
|
||||
window.location.href = href;
|
||||
promise = new Promise(f => {}); // never resolves
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
export function prefetchRoutes(pathnames: string[]) {
|
||||
if (!manifest) throw new Error(`You must call init() first`);
|
||||
|
||||
return manifest.pages
|
||||
.filter(route => {
|
||||
if (!pathnames) return true;
|
||||
return pathnames.some(pathname => route.pattern.test(pathname));
|
||||
})
|
||||
.reduce((promise: Promise<any>, route) => {
|
||||
return promise.then(route.load);
|
||||
}, Promise.resolve());
|
||||
}
|
||||
|
||||
// remove this in 0.9
|
||||
export { prefetchRoutes as preloadRoutes };
|
||||
49
src/runtime/interfaces.ts
Normal file
49
src/runtime/interfaces.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Store } from '../interfaces';
|
||||
|
||||
export { Store };
|
||||
export type Params = Record<string, string>;
|
||||
export type Query = Record<string, string | true>;
|
||||
export type RouteData = { params: Params, query: Query, path: string };
|
||||
|
||||
export interface ComponentConstructor {
|
||||
new (options: { target: Node, data: any, store: Store, hydrate: boolean }): Component;
|
||||
preload: (props: { params: Params, query: Query }) => Promise<any>;
|
||||
};
|
||||
|
||||
export interface Component {
|
||||
set: (data: any) => void;
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
export type Page = {
|
||||
pattern: RegExp;
|
||||
parts: Array<{
|
||||
component: () => Promise<{ default: ComponentConstructor }>;
|
||||
params?: (match: RegExpExecArray) => Record<string, string>;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type Manifest = {
|
||||
ignore: RegExp[];
|
||||
root: ComponentConstructor;
|
||||
error: () => Promise<{ default: ComponentConstructor }>;
|
||||
pages: Page[]
|
||||
};
|
||||
|
||||
export type ScrollPosition = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type Target = {
|
||||
url: URL;
|
||||
path: string;
|
||||
page: Page;
|
||||
match: RegExpExecArray;
|
||||
query: Record<string, string | true>;
|
||||
};
|
||||
|
||||
export type Redirect = {
|
||||
statusCode: number;
|
||||
location: string;
|
||||
};
|
||||
19
src/runtime/utils.ts
Normal file
19
src/runtime/utils.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export function detach(node: Node) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
|
||||
export function findAnchor(node: Node) {
|
||||
while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG <a> elements have a lowercase name
|
||||
return node;
|
||||
}
|
||||
|
||||
export function which(event: MouseEvent) {
|
||||
return event.which === null ? event.button : event.which;
|
||||
}
|
||||
|
||||
export function scroll_state() {
|
||||
return {
|
||||
x: window.scrollX,
|
||||
y: window.scrollY
|
||||
};
|
||||
}
|
||||
55
src/webpack.ts
Normal file
55
src/webpack.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { locations, dev } from './config';
|
||||
|
||||
export default {
|
||||
dev: dev(),
|
||||
|
||||
client: {
|
||||
entry: () => {
|
||||
return {
|
||||
main: `${locations.app()}/client`
|
||||
};
|
||||
},
|
||||
|
||||
output: () => {
|
||||
return {
|
||||
path: `${locations.dest()}/client`,
|
||||
filename: '[hash]/[name].js',
|
||||
chunkFilename: '[hash]/[name].[id].js',
|
||||
publicPath: `client/`
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
server: {
|
||||
entry: () => {
|
||||
return {
|
||||
server: `${locations.app()}/server`
|
||||
};
|
||||
},
|
||||
|
||||
output: () => {
|
||||
return {
|
||||
path: locations.dest(),
|
||||
filename: '[name].js',
|
||||
chunkFilename: '[hash]/[name].[id].js',
|
||||
libraryTarget: 'commonjs2'
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
serviceworker: {
|
||||
entry: () => {
|
||||
return {
|
||||
'service-worker': `${locations.app()}/service-worker`
|
||||
};
|
||||
},
|
||||
|
||||
output: () => {
|
||||
return {
|
||||
path: locations.dest(),
|
||||
filename: '[name].js',
|
||||
chunkFilename: '[name].[id].[hash].js'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
3
test/app/.gitignore
vendored
3
test/app/.gitignore
vendored
@@ -3,4 +3,5 @@ node_modules
|
||||
.sapper
|
||||
yarn.lock
|
||||
cypress/screenshots
|
||||
templates/.*
|
||||
templates/.*
|
||||
dist
|
||||
|
||||
13
test/app/app/client.js
Normal file
13
test/app/app/client.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { init, prefetchRoutes } from '../../../runtime.js';
|
||||
import { Store } from 'svelte/store.js';
|
||||
import { manifest } from './manifest/client.js';
|
||||
|
||||
window.init = () => {
|
||||
return init({
|
||||
target: document.querySelector('#sapper'),
|
||||
manifest,
|
||||
store: data => new Store(data)
|
||||
});
|
||||
};
|
||||
|
||||
window.prefetchRoutes = prefetchRoutes;
|
||||
104
test/app/app/server.js
Normal file
104
test/app/app/server.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import fs from 'fs';
|
||||
import { resolve } from 'url';
|
||||
import express from 'express';
|
||||
import serve from 'serve-static';
|
||||
import sapper from '../../../dist/middleware.ts.js';
|
||||
import { Store } from 'svelte/store.js';
|
||||
import { manifest } from './manifest/server.js';
|
||||
|
||||
let pending;
|
||||
let ended;
|
||||
|
||||
process.on('message', message => {
|
||||
if (message.action === 'start') {
|
||||
if (pending) {
|
||||
throw new Error(`Already capturing`);
|
||||
}
|
||||
|
||||
pending = new Set();
|
||||
ended = false;
|
||||
process.send({ type: 'ready' });
|
||||
}
|
||||
|
||||
if (message.action === 'end') {
|
||||
ended = true;
|
||||
if (pending.size === 0) {
|
||||
process.send({ type: 'done' });
|
||||
pending = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const app = express();
|
||||
|
||||
const { PORT = 3000, BASEPATH = '' } = process.env;
|
||||
const base = `http://localhost:${PORT}${BASEPATH}/`;
|
||||
|
||||
// this allows us to do e.g. `fetch('/api/blog')` on the server
|
||||
const fetch = require('node-fetch');
|
||||
global.fetch = (url, opts) => {
|
||||
return fetch(resolve(base, url), opts);
|
||||
};
|
||||
|
||||
const middlewares = [
|
||||
serve('assets'),
|
||||
|
||||
// set test cookie
|
||||
(req, res, next) => {
|
||||
res.setHeader('Set-Cookie', 'test=woohoo!; Max-Age=3600');
|
||||
next();
|
||||
},
|
||||
|
||||
// emit messages so we can capture requests
|
||||
(req, res, next) => {
|
||||
if (!pending) return next();
|
||||
|
||||
pending.add(req.url);
|
||||
|
||||
const { write, end } = res;
|
||||
const chunks = [];
|
||||
|
||||
res.write = function(chunk) {
|
||||
chunks.push(new Buffer(chunk));
|
||||
write.apply(res, arguments);
|
||||
};
|
||||
|
||||
res.end = function(chunk) {
|
||||
if (chunk) chunks.push(new Buffer(chunk));
|
||||
end.apply(res, arguments);
|
||||
|
||||
if (pending) pending.delete(req.url);
|
||||
|
||||
process.send({
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
status: res.statusCode,
|
||||
headers: res._headers,
|
||||
body: Buffer.concat(chunks).toString()
|
||||
});
|
||||
|
||||
if (pending && pending.size === 0 && ended) {
|
||||
process.send({ type: 'done' });
|
||||
}
|
||||
};
|
||||
|
||||
next();
|
||||
},
|
||||
|
||||
sapper({
|
||||
manifest,
|
||||
store: () => {
|
||||
return new Store({
|
||||
title: 'Stored title'
|
||||
});
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
if (BASEPATH) {
|
||||
app.use(BASEPATH, ...middlewares);
|
||||
} else {
|
||||
app.use(...middlewares);
|
||||
}
|
||||
|
||||
app.listen(PORT);
|
||||
@@ -1,14 +1,12 @@
|
||||
const ASSETS = `cache__timestamp__`;
|
||||
import { assets, shell, timestamp, routes } from './manifest/service-worker.js';
|
||||
|
||||
const ASSETS = `cachetimestamp`;
|
||||
|
||||
// `shell` is an array of all the files generated by webpack,
|
||||
// `assets` is an array of everything in the `assets` directory
|
||||
const to_cache = __shell__.concat(__assets__);
|
||||
const to_cache = shell.concat(assets);
|
||||
const cached = new Set(to_cache);
|
||||
|
||||
// `routes` is an array of `{ pattern: RegExp }` objects that
|
||||
// match the pages in your app
|
||||
const routes = __routes__;
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
@@ -1,19 +1,15 @@
|
||||
<!doctype>
|
||||
<!doctype html>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width'>
|
||||
<meta name='theme-color' content='#aa1e1e'>
|
||||
|
||||
<link rel='stylesheet' href='/global.css'>
|
||||
<link rel='manifest' href='/manifest.json'>
|
||||
<link rel='icon' type='image/png' href='/favicon.png'>
|
||||
%sapper.base%
|
||||
|
||||
<script>
|
||||
// if ('serviceWorker' in navigator) {
|
||||
// navigator.serviceWorker.register('/service-worker.js');
|
||||
// }
|
||||
</script>
|
||||
<link rel='stylesheet' href='global.css'>
|
||||
<link rel='manifest' href='manifest.json'>
|
||||
<link rel='icon' type='image/png' href='favicon.png'>
|
||||
|
||||
<!-- Sapper generates a <style> tag containing critical CSS
|
||||
for the current page. CSS for the rest of the app is
|
||||
@@ -32,6 +28,6 @@
|
||||
<!-- Sapper creates a <script> tag containing `templates/main.js`
|
||||
and anything else it needs to hydrate the app and
|
||||
initialise the router -->
|
||||
<script src='%sapper.main%'></script>
|
||||
%sapper.scripts%
|
||||
</body>
|
||||
</html>
|
||||
4931
test/app/package-lock.json
generated
4931
test/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"name": "TODO",
|
||||
"description": "TODO",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "node server.js",
|
||||
"build": "sapper build",
|
||||
"start": "cross-env NODE_ENV=production node server.js",
|
||||
"prestart": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"compression": "^1.7.1",
|
||||
"cross-env": "^5.1.1",
|
||||
"css-loader": "^0.28.7",
|
||||
"express": "^4.16.2",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"glob": "^7.1.2",
|
||||
"marked": "^0.3.7",
|
||||
"node-fetch": "^1.7.3",
|
||||
"npm-run-all": "^4.1.2",
|
||||
"serve-static": "^1.13.1",
|
||||
"style-loader": "^0.19.0",
|
||||
"svelte": "^1.49.1",
|
||||
"svelte-loader": "^2.2.1",
|
||||
"uglifyjs-webpack-plugin": "^1.1.2",
|
||||
"webpack": "^3.10.0"
|
||||
}
|
||||
}
|
||||
20
test/app/routes/[x]/[y]/[z].html
Normal file
20
test/app/routes/[x]/[y]/[z].html
Normal file
@@ -0,0 +1,20 @@
|
||||
<span>z: {segment} {count}</span>
|
||||
<a href="foo/bar/qux"></a>
|
||||
|
||||
<script>
|
||||
import counts from '../_counts.js';
|
||||
|
||||
export default {
|
||||
preload() {
|
||||
return {
|
||||
count: counts.z += 1
|
||||
};
|
||||
},
|
||||
|
||||
oncreate() {
|
||||
this.set({
|
||||
segment: this.get().params.z
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
20
test/app/routes/[x]/[y]/_layout.html
Normal file
20
test/app/routes/[x]/[y]/_layout.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<span>y: {segment} {count}</span>
|
||||
<svelte:component this={child.component} {...child.props}/>
|
||||
|
||||
<script>
|
||||
import counts from '../_counts.js';
|
||||
|
||||
export default {
|
||||
preload() {
|
||||
return {
|
||||
count: counts.y += 1
|
||||
};
|
||||
},
|
||||
|
||||
oncreate() {
|
||||
this.set({
|
||||
segment: this.get().params.y
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
5
test/app/routes/[x]/_counts.js
Normal file
5
test/app/routes/[x]/_counts.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
x: process.browser ? 1 : 0,
|
||||
y: process.browser ? 1 : 0,
|
||||
z: process.browser ? 1 : 0
|
||||
};
|
||||
20
test/app/routes/[x]/_layout.html
Normal file
20
test/app/routes/[x]/_layout.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<span>x: {segment} {count}</span>
|
||||
<svelte:component this={child.component} {...child.props}/>
|
||||
|
||||
<script>
|
||||
import counts from './_counts.js';
|
||||
|
||||
export default {
|
||||
preload() {
|
||||
return {
|
||||
count: counts.x += 1
|
||||
};
|
||||
},
|
||||
|
||||
oncreate() {
|
||||
this.set({
|
||||
segment: this.get().params.x
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,15 +0,0 @@
|
||||
<Nav page={{page}}/>
|
||||
|
||||
<main>
|
||||
<slot></slot>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
import Nav from './Nav.html';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Nav
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,56 +0,0 @@
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a class='{{page === "home" ? "selected" : ""}}' href='/'>home</a></li>
|
||||
<li><a class='{{page === "about" ? "selected" : ""}}' href='/about'>about</a></li>
|
||||
|
||||
<!-- for the blog link, we're using rel=prefetch so that Sapper prefetches
|
||||
the blog data when we hover over the link or tap it on a touchscreen -->
|
||||
<li><a rel=prefetch class='{{page === "blog" ? "selected" : ""}}' href='/blog'>blog</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
nav {
|
||||
border-bottom: 1px solid rgba(170,30,30,0.1);
|
||||
font-weight: 300;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* clearfix */
|
||||
ul::after {
|
||||
content: '';
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
li {
|
||||
display: block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.selected {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.selected::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
width: calc(100% - 1em);
|
||||
height: 2px;
|
||||
background-color: rgb(170,30,30);
|
||||
display: block;
|
||||
bottom: -1px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
padding: 1em 0.5em;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
6
test/app/routes/_error.html
Normal file
6
test/app/routes/_error.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<svelte:head>
|
||||
<title>{status}</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>{status}</h1>
|
||||
<p>{error.message}</p>
|
||||
15
test/app/routes/_layout.html
Normal file
15
test/app/routes/_layout.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{#if preloading}
|
||||
<progress class='preloading-progress' value=0.5/>
|
||||
{/if}
|
||||
|
||||
<svelte:component this={child.component} {rootPreloadFunctionRan} {...child.props}/>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload() {
|
||||
return {
|
||||
rootPreloadFunctionRan: true
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,26 +1,27 @@
|
||||
<:Head>
|
||||
<svelte:head>
|
||||
<title>About</title>
|
||||
</:Head>
|
||||
</svelte:head>
|
||||
|
||||
<Layout page='about'>
|
||||
<h1>About this site</h1>
|
||||
<h1>About this site</h1>
|
||||
|
||||
<p>This is the 'about' page. There's not much here.</p>
|
||||
<p>This is the 'about' page. There's not much here.</p>
|
||||
|
||||
<button on:click='goto("/blog/what-is-sapper")'>What is Sapper?</button>
|
||||
</Layout>
|
||||
<button class='prefetch' on:click='prefetch("blog/why-the-name")'>Why the name?</button>
|
||||
|
||||
<script>
|
||||
import Layout from './_components/Layout.html';
|
||||
import { goto } from '../../../runtime/app.js';
|
||||
import { goto, prefetch } from '../../../runtime.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Layout
|
||||
oncreate() {
|
||||
window.goto = goto;
|
||||
},
|
||||
|
||||
ondestroy() {
|
||||
window.goto = null;
|
||||
},
|
||||
|
||||
methods: {
|
||||
goto
|
||||
prefetch
|
||||
}
|
||||
};
|
||||
</script>
|
||||
9
test/app/routes/api/delete/[id].js
Normal file
9
test/app/routes/api/delete/[id].js
Normal file
@@ -0,0 +1,9 @@
|
||||
export function del(req, res) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
res.end(JSON.stringify({
|
||||
id: req.params.id
|
||||
}));
|
||||
}
|
||||
@@ -1,66 +1,35 @@
|
||||
<:Head>
|
||||
<title>{{post.title}}</title>
|
||||
</:Head>
|
||||
<svelte:head>
|
||||
<title>{post.title}</title>
|
||||
</svelte:head>
|
||||
|
||||
<Layout page='blog'>
|
||||
<h1>{{post.title}}</h1>
|
||||
<h1>{post.title}</h1>
|
||||
|
||||
<div class='content'>
|
||||
{{{post.html}}}
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
/*
|
||||
By default, CSS is locally scoped to the component,
|
||||
and any unused styles are dead-code-eliminated.
|
||||
In this page, Svelte can't know which elements are
|
||||
going to appear inside the {{{post.html}}} block,
|
||||
so we have to use the :global(...) modifier to target
|
||||
all elements inside .content
|
||||
*/
|
||||
.content :global(h2) {
|
||||
font-size: 1.4em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.content :global(pre) {
|
||||
background-color: #f9f9f9;
|
||||
box-shadow: inset 1px 1px 5px rgba(0,0,0,0.05);
|
||||
padding: 0.5em;
|
||||
border-radius: 2px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.content :global(pre) :global(code) {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content :global(ul) {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.content :global(li) {
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
</style>
|
||||
<div class='content'>
|
||||
{@html post.html}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import Layout from '../_components/Layout.html';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Layout
|
||||
},
|
||||
|
||||
preload({ params, query }) {
|
||||
// the `slug` parameter is available because this file
|
||||
// is called [slug].html
|
||||
const { slug } = params;
|
||||
|
||||
return fetch(`/api/blog/${slug}`).then(r => r.json()).then(post => {
|
||||
return { post };
|
||||
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');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,9 +11,9 @@ export function get(req, res, next) {
|
||||
const { slug } = req.params;
|
||||
|
||||
if (slug in lookup) {
|
||||
res.set({
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': `max-age=${30 * 60 * 1e3}` // cache for 30 minutes
|
||||
'Cache-Control': `no-cache`
|
||||
});
|
||||
|
||||
res.end(lookup[slug]);
|
||||
@@ -14,7 +14,7 @@ const posts = [
|
||||
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>
|
||||
<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>
|
||||
@@ -70,8 +70,8 @@ const posts = [
|
||||
<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 powering this very page (look in <code>routes/api/blog</code>)</li>
|
||||
<li>Links are just <code><a></code> elements, rather than framework-specific <code><Link></code> components. That means, for example, that <a href='/blog/how-can-i-get-involved'>this link right here</a>, despite being inside a blob of HTML, works with the router as you'd expect.</li>
|
||||
<li>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>
|
||||
`
|
||||
},
|
||||
@@ -1,38 +1,23 @@
|
||||
<:Head>
|
||||
<svelte:head>
|
||||
<title>Blog</title>
|
||||
</:Head>
|
||||
</svelte:head>
|
||||
|
||||
<Layout page='blog'>
|
||||
<h1>Recent posts</h1>
|
||||
<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>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
ul {
|
||||
margin: 0 0 1em 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
<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>
|
||||
import Layout from '../_components/Layout.html';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Layout
|
||||
},
|
||||
|
||||
preload({ params, query }) {
|
||||
return fetch(`/api/blog`).then(r => r.json()).then(posts => {
|
||||
return fetch(`blog.json`).then(r => r.json()).then(posts => {
|
||||
return { posts };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ const contents = JSON.stringify(posts.map(post => {
|
||||
}));
|
||||
|
||||
export function get(req, res) {
|
||||
res.set({
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': `max-age=${30 * 60 * 1e3}` // cache for 30 minutes
|
||||
});
|
||||
11
test/app/routes/credentials/index.html
Normal file
11
test/app/routes/credentials/index.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<h1>{message}</h1>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload({ query }) {
|
||||
return this.fetch(`credentials/test.json`, {
|
||||
credentials: query.creds
|
||||
}).then(r => r.json());
|
||||
}
|
||||
};
|
||||
</script>
|
||||
28
test/app/routes/credentials/test.json.js
Normal file
28
test/app/routes/credentials/test.json.js
Normal file
@@ -0,0 +1,28 @@
|
||||
export function get(req, res) {
|
||||
const cookies = req.headers.cookie
|
||||
? req.headers.cookie.split(/,\s+/).reduce((cookies, cookie) => {
|
||||
const [pair] = cookie.split('; ');
|
||||
const [name, value] = pair.split('=');
|
||||
cookies[name] = value;
|
||||
return cookies;
|
||||
}, {})
|
||||
: {};
|
||||
|
||||
if (cookies.test) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
res.end(JSON.stringify({
|
||||
message: cookies.test
|
||||
}));
|
||||
} else {
|
||||
res.writeHead(403, {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
res.end(JSON.stringify({
|
||||
message: 'unauthorized'
|
||||
}));
|
||||
}
|
||||
}
|
||||
15
test/app/routes/delete-test.html
Normal file
15
test/app/routes/delete-test.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<button class='del' on:click='del()'>delete</button>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
del() {
|
||||
fetch(`api/delete/42`, { method: 'DELETE' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
window.deleted = data;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
1
test/app/routes/fünke.html
Normal file
1
test/app/routes/fünke.html
Normal file
@@ -0,0 +1 @@
|
||||
<h1>I'm afraid I just blue myself</h1>
|
||||
@@ -1,58 +1,26 @@
|
||||
<:Head>
|
||||
<svelte:head>
|
||||
<title>Sapper project template</title>
|
||||
</:Head>
|
||||
</svelte:head>
|
||||
|
||||
<Layout page='home'>
|
||||
<h1>Great success!</h1>
|
||||
<h1>Great success!</h1>
|
||||
|
||||
<figure>
|
||||
<img src='/great-success.png'>
|
||||
<figcaption>HIGH FIVE!</figcaption>
|
||||
</figure>
|
||||
<a href='.'>home</a>
|
||||
<a href='about'>about</a>
|
||||
<a href='slow-preload'>slow preload</a>
|
||||
<a href='redirect-from'>redirect</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>
|
||||
|
||||
<p><strong>Try editing this file (routes/index.html) to test hot module reloading.</strong></p>
|
||||
</Layout>
|
||||
<div class='hydrate-test'></div>
|
||||
|
||||
<style>
|
||||
h1, figure, p {
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 2.8em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
h1 {
|
||||
font-size: 4em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Layout from './_components/Layout.html';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Layout
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</style>
|
||||
1
test/app/routes/missing-index/ok.html
Normal file
1
test/app/routes/missing-index/ok.html
Normal file
@@ -0,0 +1 @@
|
||||
<h1>it works</h1>
|
||||
1
test/app/routes/preload-root.html
Normal file
1
test/app/routes/preload-root.html
Normal file
@@ -0,0 +1 @@
|
||||
<h1>root preload function ran: {rootPreloadFunctionRan}</h1>
|
||||
17
test/app/routes/preload-values/custom-class.html
Normal file
17
test/app/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/routes/preload-values/index.html
Normal file
1
test/app/routes/preload-values/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<svelte:component this={child.component} {...child.props}/>
|
||||
11
test/app/routes/preload-values/set.html
Normal file
11
test/app/routes/preload-values/set.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<h1>{set.has('x')}</h1>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload() {
|
||||
return {
|
||||
set: new Set(['x'])
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
7
test/app/routes/redirect-from.html
Normal file
7
test/app/routes/redirect-from.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<script>
|
||||
export default {
|
||||
preload() {
|
||||
this.redirect(301, 'redirect-to');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
1
test/app/routes/redirect-to.html
Normal file
1
test/app/routes/redirect-to.html
Normal file
@@ -0,0 +1 @@
|
||||
<h1>redirected</h1>
|
||||
15
test/app/routes/slow-preload.html
Normal file
15
test/app/routes/slow-preload.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<h1>This page should never render</h1>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload() {
|
||||
return new Promise(fulfil => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.fulfil = fulfil;
|
||||
} else {
|
||||
fulfil({});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
1
test/app/routes/store.html
Normal file
1
test/app/routes/store.html
Normal file
@@ -0,0 +1 @@
|
||||
<h1>{$title}</h1>
|
||||
3
test/app/routes/throw-an-error.js
Normal file
3
test/app/routes/throw-an-error.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export function get() {
|
||||
throw new Error('nope');
|
||||
}
|
||||
9
test/app/routes/unsafe-replacement.html
Normal file
9
test/app/routes/unsafe-replacement.html
Normal file
@@ -0,0 +1,9 @@
|
||||
$&
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload() {
|
||||
return '$&';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,24 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const app = require('express')();
|
||||
const compression = require('compression');
|
||||
const sapper = require('sapper');
|
||||
const static = require('serve-static');
|
||||
|
||||
const { PORT = 3000 } = process.env;
|
||||
|
||||
// this allows us to do e.g. `fetch('/api/blog')` on the server
|
||||
const fetch = require('node-fetch');
|
||||
global.fetch = (url, opts) => {
|
||||
if (url[0] === '/') url = `http://localhost:${PORT}${url}`;
|
||||
return fetch(url, opts);
|
||||
};
|
||||
|
||||
app.use(compression({ threshold: 0 }));
|
||||
|
||||
app.use(static('assets'));
|
||||
|
||||
app.use(sapper());
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`listening on port ${PORT}`);
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
<!doctype>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width'>
|
||||
<meta name='theme-color' content='#aa1e1e'>
|
||||
|
||||
<link rel='manifest' href='/manifest.json'>
|
||||
<link rel='icon' type='image/png' href='/favicon.png'>
|
||||
|
||||
<!-- %sapper.status% is the HTTP status code, e.g. 404 -->
|
||||
<title>%sapper.status%</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
max-width: 800px;
|
||||
padding: 1em;
|
||||
margin: 0 auto;
|
||||
font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
background-color: #f4f4f4;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: rgb(170,30,30);
|
||||
border-bottom: 1px solid #aaa;
|
||||
padding: 0 0 0.5em 0;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: Menlo, monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>%sapper.title%</h1>
|
||||
<p>Could not %sapper.method% %sapper.url%</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,47 +0,0 @@
|
||||
<!doctype>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width'>
|
||||
<meta name='theme-color' content='#aa1e1e'>
|
||||
|
||||
<link rel='manifest' href='/manifest.json'>
|
||||
<link rel='icon' type='image/png' href='/favicon.png'>
|
||||
|
||||
<title>%sapper.status%</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
max-width: 800px;
|
||||
padding: 1em;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: rgb(170,30,30);
|
||||
border-bottom: 1px solid #aaa;
|
||||
padding: 0 0 0.5em 0;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: Menlo, monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.stack {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>%sapper.title%</h1>
|
||||
<pre>%sapper.error%</pre>
|
||||
<pre class='stack'>%sapper.stack%</pre>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +0,0 @@
|
||||
import { init } from '../../../runtime/app.js';
|
||||
|
||||
// `routes` is an array of route objects injected by Sapper
|
||||
init(document.querySelector('#sapper'), __routes__);
|
||||
|
||||
window.READY = true;
|
||||
@@ -1,6 +1,9 @@
|
||||
const config = require('../../webpack/config.js');
|
||||
const config = require('../../../webpack/config.js');
|
||||
const webpack = require('webpack');
|
||||
|
||||
const mode = process.env.NODE_ENV;
|
||||
const isDev = mode === 'development';
|
||||
|
||||
module.exports = {
|
||||
entry: config.client.entry(),
|
||||
output: config.client.output(),
|
||||
@@ -16,24 +19,16 @@ module.exports = {
|
||||
loader: 'svelte-loader',
|
||||
options: {
|
||||
hydratable: true,
|
||||
emitCss: !config.dev,
|
||||
cascade: false,
|
||||
store: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
{ loader: "style-loader" },
|
||||
{ loader: "css-loader" }
|
||||
]
|
||||
}
|
||||
].filter(Boolean)
|
||||
]
|
||||
},
|
||||
mode,
|
||||
plugins: [
|
||||
config.dev && new webpack.HotModuleReplacementPlugin(),
|
||||
!config.dev && new webpack.optimize.ModuleConcatenationPlugin()
|
||||
isDev && new webpack.HotModuleReplacementPlugin()
|
||||
].filter(Boolean),
|
||||
devtool: config.dev ? 'inline-source-map' : false
|
||||
devtool: isDev && 'inline-source-map'
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
const config = require('../../webpack/config.js');
|
||||
const config = require('../../../webpack/config.js');
|
||||
const sapper_pkg = require('../../../package.json');
|
||||
|
||||
module.exports = {
|
||||
entry: config.server.entry(),
|
||||
@@ -7,6 +8,10 @@ module.exports = {
|
||||
resolve: {
|
||||
extensions: ['.js', '.html']
|
||||
},
|
||||
externals: [].concat(
|
||||
Object.keys(sapper_pkg.dependencies),
|
||||
Object.keys(sapper_pkg.devDependencies)
|
||||
),
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
@@ -23,5 +28,9 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
mode: process.env.NODE_ENV,
|
||||
performance: {
|
||||
hints: false // it doesn't matter if server.js is large
|
||||
}
|
||||
};
|
||||
7
test/app/webpack/service-worker.config.js
Normal file
7
test/app/webpack/service-worker.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const config = require('../../../webpack/config.js');
|
||||
|
||||
module.exports = {
|
||||
entry: config.serviceworker.entry(),
|
||||
output: config.serviceworker.output(),
|
||||
mode: process.env.NODE_ENV
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user