mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-13 19:45:26 +00:00
Compare commits
355 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2758382c68 | ||
|
|
dd7f1ff99c | ||
|
|
45142cd037 | ||
|
|
ceb1caf1de | ||
|
|
7e263a3076 | ||
|
|
ec88d4a430 | ||
|
|
909ea72108 | ||
|
|
cd09d75d99 | ||
|
|
0e3abe489a | ||
|
|
a5d141d2f1 | ||
|
|
87eae6164b | ||
|
|
97e00f5a9c | ||
|
|
bd55558b5e | ||
|
|
25dc4b3a4c | ||
|
|
72c27b78a3 | ||
|
|
25809ec409 | ||
|
|
3220c522d7 | ||
|
|
d5d25f1d30 | ||
|
|
7ccd6ba329 | ||
|
|
35c30ae2c5 | ||
|
|
2c61f6d396 | ||
|
|
86233a8eab | ||
|
|
c140b128ee | ||
|
|
66ac9773c0 | ||
|
|
e60714bb98 | ||
|
|
52dfd6e939 | ||
|
|
fc2312eba6 | ||
|
|
cf90476255 | ||
|
|
1e8d7d10ab | ||
|
|
cf6621b83c | ||
|
|
9812cbd71c | ||
|
|
67a81a3cac | ||
|
|
67463683cc | ||
|
|
b94481b716 | ||
|
|
a95ddee48d | ||
|
|
953694f77f | ||
|
|
2f24cb0429 | ||
|
|
687071902d | ||
|
|
cd3fcfdf3c | ||
|
|
dad48e4abd | ||
|
|
37d3d57694 | ||
|
|
9a5d273590 | ||
|
|
3816fe71ad | ||
|
|
69f5b9cac7 | ||
|
|
ad14320dc3 | ||
|
|
43563bd8e5 | ||
|
|
02d558b97c | ||
|
|
866286c95e | ||
|
|
e1b5e336dc | ||
|
|
1d71b86c0f | ||
|
|
bdc248f09a | ||
|
|
be63ea7c96 | ||
|
|
819ec0b776 | ||
|
|
d22d37fb18 | ||
|
|
8ec433581a | ||
|
|
0d0e4d664e | ||
|
|
4348fad16d | ||
|
|
4314897a78 | ||
|
|
b1c57466c0 | ||
|
|
ef55fc5ddd | ||
|
|
e011fce935 | ||
|
|
ba3d9c85c5 | ||
|
|
cddd7adaad | ||
|
|
d8412f33ba | ||
|
|
254e41b11e | ||
|
|
491c5e3b92 | ||
|
|
4441ceb91d | ||
|
|
77e418cd21 | ||
|
|
4171786953 | ||
|
|
5f7cbadd8d | ||
|
|
9bac32eea4 | ||
|
|
3a19657ad9 | ||
|
|
d1b6d029e9 | ||
|
|
45b1147228 | ||
|
|
c827fda703 | ||
|
|
dd39909371 | ||
|
|
fb24c862f3 | ||
|
|
542115f82e | ||
|
|
61000a4795 | ||
|
|
7f98d50e15 | ||
|
|
c580259c07 | ||
|
|
e8f3aff0da | ||
|
|
c82031a8e5 | ||
|
|
1eed1023aa | ||
|
|
c1a2d93da6 | ||
|
|
504654b58e | ||
|
|
b1067103a4 | ||
|
|
06af8e87da | ||
|
|
8bb0999878 | ||
|
|
b5a8d29c37 | ||
|
|
5925636b16 | ||
|
|
bc232007c3 | ||
|
|
ffaacb4c99 | ||
|
|
47b50f2c0e | ||
|
|
a66ac00d42 | ||
|
|
0f8c04b03d | ||
|
|
d9d93f41c4 | ||
|
|
5289fc11d8 | ||
|
|
dd6c51567a | ||
|
|
01ff84f241 | ||
|
|
329c113723 | ||
|
|
2ad10b380f | ||
|
|
e6314cde96 | ||
|
|
b64e25a177 | ||
|
|
49bc1b00a9 | ||
|
|
24bfcc8d2d | ||
|
|
b405e5878e | ||
|
|
ef0ca58a21 | ||
|
|
854147fa6c | ||
|
|
50ecc5c130 | ||
|
|
7e2f5f8fb6 | ||
|
|
acef0e808f | ||
|
|
248573f510 | ||
|
|
e91955fdad | ||
|
|
368e6d5cb1 | ||
|
|
1984203e87 | ||
|
|
0165c14fd9 | ||
|
|
bdb9d49187 | ||
|
|
4d79cb81ed | ||
|
|
181b0711ec | ||
|
|
1b282e7b0d | ||
|
|
99853c5181 | ||
|
|
ff3b43443e | ||
|
|
2622692f69 | ||
|
|
7625302ec7 | ||
|
|
09422e3c5a | ||
|
|
a96fb93bfb | ||
|
|
17d7ca36f1 | ||
|
|
b73e5eaa8e | ||
|
|
d9cb572271 | ||
|
|
34c28f36cd | ||
|
|
5dd04eb35c | ||
|
|
b1d072d43a | ||
|
|
5ad3f3f1d5 | ||
|
|
58754c6d15 | ||
|
|
c36780fdc8 | ||
|
|
9bebb56bd6 | ||
|
|
f475634d8d | ||
|
|
58c1eb9fa8 | ||
|
|
631afbbfe4 | ||
|
|
1cc9acb4f1 | ||
|
|
19005110f1 | ||
|
|
21ee8ad39d | ||
|
|
906b0c7ad5 | ||
|
|
896fd410d1 | ||
|
|
c0cc877456 | ||
|
|
3ed9ce27a1 | ||
|
|
edba45b809 | ||
|
|
43c1890235 | ||
|
|
605929053c | ||
|
|
2752c73ebb | ||
|
|
2547db39ac | ||
|
|
1285739cc5 | ||
|
|
14d64e854a | ||
|
|
c419c73550 | ||
|
|
835b94175d | ||
|
|
25bdcf9957 | ||
|
|
792ccf5c6a | ||
|
|
4ca8195037 | ||
|
|
cb12231053 | ||
|
|
d55401d45b | ||
|
|
99d4eafb0b | ||
|
|
bff6f550be | ||
|
|
f8ea9ebda1 | ||
|
|
181d7b4a61 | ||
|
|
beb415c65d | ||
|
|
5bbd7ead17 | ||
|
|
e11405d555 | ||
|
|
9fe0ca2c22 | ||
|
|
f2eb95d546 | ||
|
|
ab1ca60363 | ||
|
|
d95f52f8e9 | ||
|
|
b02183af53 | ||
|
|
f9828f9fd2 | ||
|
|
9a760c570f | ||
|
|
0f390920a8 | ||
|
|
9adb6ca7e6 | ||
|
|
24980651c0 | ||
|
|
7c6436a99c | ||
|
|
f6b26f1b07 | ||
|
|
55b60369f9 | ||
|
|
2be9dd1883 | ||
|
|
b29700f725 | ||
|
|
7188ce0d0d | ||
|
|
4f8ce19fe1 | ||
|
|
a85f2921e8 | ||
|
|
7a2ed16884 | ||
|
|
08e575fee0 | ||
|
|
7dbcab74d3 | ||
|
|
9b1b545194 | ||
|
|
7b01242f3e | ||
|
|
15b1fbf8a6 | ||
|
|
8f1d2e0a04 | ||
|
|
dfb8692d78 | ||
|
|
09d3c4d85e | ||
|
|
1e623dde29 | ||
|
|
5104abf329 | ||
|
|
6554fc8616 | ||
|
|
cd01b7e6db | ||
|
|
bfa3da6d3d | ||
|
|
6ee092f8d4 | ||
|
|
ac70004f77 | ||
|
|
3449f1eb37 | ||
|
|
16cb1fccc6 | ||
|
|
b20c1c029f | ||
|
|
7abfb1aab1 | ||
|
|
205c2defe4 | ||
|
|
09a6eec83e | ||
|
|
2cabf61ea7 | ||
|
|
71cfdd2907 | ||
|
|
297f4276de | ||
|
|
422e31e183 | ||
|
|
b53ee061c0 | ||
|
|
8bad37205d | ||
|
|
fd0dd4fe58 | ||
|
|
4940644ae3 | ||
|
|
fb8d952eeb | ||
|
|
fc631c4866 | ||
|
|
03ce2ea998 | ||
|
|
dd8deb2d8a | ||
|
|
7d721abb2a | ||
|
|
39b1fa89ce | ||
|
|
7a3506420f | ||
|
|
72ae4a1c64 | ||
|
|
a09c33d6a5 | ||
|
|
4590aa313c | ||
|
|
d11bd954e0 | ||
|
|
c15959710b | ||
|
|
bb8ff74f68 | ||
|
|
2cbbe91490 | ||
|
|
faeddd8add | ||
|
|
d77722c042 | ||
|
|
61daba7a64 | ||
|
|
54ff8cc2e6 | ||
|
|
e6fcafe09b | ||
|
|
a305d3cea1 | ||
|
|
75e70207b8 | ||
|
|
8a8526d9ed | ||
|
|
9a76229bb6 | ||
|
|
f4e46e6e6c | ||
|
|
90cd347112 | ||
|
|
5adfdd6fe0 | ||
|
|
a6dc61a182 | ||
|
|
96666d05ec | ||
|
|
6390ba692b | ||
|
|
0e131cc81e | ||
|
|
bd3d5713cb | ||
|
|
9ec23c47ad | ||
|
|
b7bb69925e | ||
|
|
25124f6ee7 | ||
|
|
73d491cd19 | ||
|
|
e25fceb4b8 | ||
|
|
3807147c57 | ||
|
|
a523ba58ff | ||
|
|
fe03fd3a52 | ||
|
|
89c430a0cb | ||
|
|
8ef312849c | ||
|
|
4200446684 | ||
|
|
681ed005b8 | ||
|
|
d457af8d51 | ||
|
|
0c158b9e1f | ||
|
|
50011e2077 | ||
|
|
f27b7973e3 | ||
|
|
2af2ab3cb9 | ||
|
|
6a4dc1901c | ||
|
|
fbbc0e9e19 | ||
|
|
1213c3da46 | ||
|
|
4cc2104088 | ||
|
|
d6dda371ca | ||
|
|
304c06085e | ||
|
|
33b6450e34 | ||
|
|
8faa98af6a | ||
|
|
14df138528 | ||
|
|
44285cdb2f | ||
|
|
bd656cfd5b | ||
|
|
c4b4bd587d | ||
|
|
2abfdb03d5 | ||
|
|
a80ac3a8b8 | ||
|
|
887cb09386 | ||
|
|
cfeeafded4 | ||
|
|
2cae674033 | ||
|
|
7c0f32662d | ||
|
|
b4fb1c3268 | ||
|
|
ecd0f673a9 | ||
|
|
40d16852f7 | ||
|
|
133be03791 | ||
|
|
727a76ebb5 | ||
|
|
e3c047831a | ||
|
|
81b5e0d764 | ||
|
|
98e904dcfc | ||
|
|
ca51372150 | ||
|
|
7cef1f1120 | ||
|
|
1b73baabce | ||
|
|
5aa01b922b | ||
|
|
f0bc68be88 | ||
|
|
be7c53becc | ||
|
|
9ea4137b87 | ||
|
|
7588911108 | ||
|
|
fc8280adea | ||
|
|
d08f9eb5a4 | ||
|
|
2b3472b1b1 | ||
|
|
30ddb3dd7e | ||
|
|
0c891ba79e | ||
|
|
ee94f355d5 | ||
|
|
bea9b7965a | ||
|
|
1312aede1f | ||
|
|
50e307e0c0 | ||
|
|
e87ac1f367 | ||
|
|
5da9d0926a | ||
|
|
9538499d51 | ||
|
|
ff1e632057 | ||
|
|
aeeb231477 | ||
|
|
d1940db8c0 | ||
|
|
98f9a64b64 | ||
|
|
b9bef802d3 | ||
|
|
a7024b3806 | ||
|
|
423e02aeae | ||
|
|
12b73ecebf | ||
|
|
e1bc38b5a7 | ||
|
|
b66f624f01 | ||
|
|
502dd547d1 | ||
|
|
4c343490d2 | ||
|
|
b3027c5816 | ||
|
|
c29e8022cc | ||
|
|
e4cd4c9cb0 | ||
|
|
feddad42b2 | ||
|
|
3c4ebcda30 | ||
|
|
75aedf4663 | ||
|
|
c8366dec74 | ||
|
|
9a936669c6 | ||
|
|
0226bd90c6 | ||
|
|
e1926e1bcb | ||
|
|
db1c1f332a | ||
|
|
e8d510b261 | ||
|
|
f8e237b265 | ||
|
|
68c2f2e388 | ||
|
|
0bcb61650b | ||
|
|
43a12a8331 | ||
|
|
f0feab5738 | ||
|
|
e9203b4d71 | ||
|
|
8e79e706e6 | ||
|
|
4b495f44fd | ||
|
|
222a750b7b | ||
|
|
5b214c964c | ||
|
|
95f99fd378 | ||
|
|
1bed4b0670 | ||
|
|
9d4890913a | ||
|
|
f50d3c4262 | ||
|
|
8925e541d5 | ||
|
|
a48afb77d3 | ||
|
|
45e845ee92 | ||
|
|
492f024d2a | ||
|
|
8d40992cf1 | ||
|
|
4232f75b19 | ||
|
|
fefb0d96d7 |
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"rules": {
|
||||
"indent": [ 2, "tab", { "SwitchCase": 1 } ],
|
||||
"semi": [ 2, "always" ],
|
||||
"space-before-blocks": [ 2, "always" ],
|
||||
"no-mixed-spaces-and-tabs": [ 2, "smart-tabs" ],
|
||||
"no-cond-assign": 0,
|
||||
"no-unused-vars": 2,
|
||||
"object-shorthand": [ 2, "always" ],
|
||||
"no-const-assign": 2,
|
||||
"no-class-assign": 2,
|
||||
"no-this-before-super": 2,
|
||||
"no-var": 2,
|
||||
"no-unreachable": 2,
|
||||
"valid-typeof": 2,
|
||||
"quote-props": [ 2, "as-needed" ],
|
||||
"one-var": [ 2, "never" ],
|
||||
"prefer-arrow-callback": 2,
|
||||
"prefer-const": [ 2, { "destructuring": "all" } ],
|
||||
"arrow-spacing": 2,
|
||||
"no-inner-declarations": 0
|
||||
},
|
||||
"env": {
|
||||
"es6": true,
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"mocha": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,4 +1,13 @@
|
||||
.DS_Store
|
||||
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
|
||||
13
.travis.yml
13
.travis.yml
@@ -1,10 +1,21 @@
|
||||
sudo: false
|
||||
|
||||
language: node_js
|
||||
|
||||
node_js:
|
||||
- "6"
|
||||
- "stable"
|
||||
|
||||
env:
|
||||
global:
|
||||
- BUILD_TIMEOUT=10000
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- xvfb
|
||||
|
||||
install:
|
||||
- export DISPLAY=':99.0'
|
||||
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||
- npm install
|
||||
- (cd test/app && npm install)
|
||||
|
||||
220
CHANGELOG.md
220
CHANGELOG.md
@@ -1,5 +1,225 @@
|
||||
# sapper changelog
|
||||
|
||||
## 0.10.5
|
||||
|
||||
* Fix missing service worker ([#231](https://github.com/sveltejs/sapper/pull/231))
|
||||
|
||||
## 0.10.4
|
||||
|
||||
* Upgrade chokidar, this time with a fix ([#227](https://github.com/sveltejs/sapper/pull/227))
|
||||
|
||||
## 0.10.3
|
||||
|
||||
* Downgrade chokidar ([#212](https://github.com/sveltejs/sapper/issues/212))
|
||||
|
||||
## 0.10.2
|
||||
|
||||
* Attach `store` to error pages
|
||||
* Fix sorting edge case ([#215](https://github.com/sveltejs/sapper/pull/215))
|
||||
|
||||
## 0.10.1
|
||||
|
||||
* Fix server-side `fetch` paths ([#207](https://github.com/sveltejs/sapper/pull/207))
|
||||
|
||||
## 0.10.0
|
||||
|
||||
* Support mounting on a path (this requires `app/template.html` to include `%sapper.base%`) ([#180](https://github.com/sveltejs/sapper/issues/180))
|
||||
* Support per-request server-side `Store` with client-side hydration ([#178](https://github.com/sveltejs/sapper/issues/178))
|
||||
* Add `this.fetch` to `preload`, with credentials support ([#178](https://github.com/sveltejs/sapper/issues/178))
|
||||
* Exclude sourcemaps from preload links and `<script>` block ([#204](https://github.com/sveltejs/sapper/pull/204))
|
||||
* Register service worker in `<script>` block
|
||||
|
||||
|
||||
## 0.9.6
|
||||
|
||||
* Whoops — `tslib` is a runtime dependency
|
||||
|
||||
## 0.9.5
|
||||
|
||||
* Stringify clorox output ([#197](https://github.com/sveltejs/sapper/pull/197))
|
||||
|
||||
## 0.9.4
|
||||
|
||||
* Add `SAPPER_BASE` and `SAPPER_APP` environment variables ([#181](https://github.com/sveltejs/sapper/issues/181))
|
||||
* Minify template in `sapper build` ([#15](https://github.com/sveltejs/sapper/issues/15))
|
||||
* Minify all HTML files in `sapper export` ([#172](https://github.com/sveltejs/sapper/issues/172))
|
||||
* Log exported files ([#195](https://github.com/sveltejs/sapper/pull/195))
|
||||
* Add `--open`/`-o` flag to `sapper dev` and `sapper start` ([#186](https://github.com/sveltejs/sapper/issues/186))
|
||||
|
||||
## 0.9.3
|
||||
|
||||
* Fix path to `sapper-dev-client`
|
||||
|
||||
## 0.9.2
|
||||
|
||||
* Include `dist` files in package
|
||||
|
||||
## 0.9.1
|
||||
|
||||
* Include `sapper` bin
|
||||
|
||||
## 0.9.0
|
||||
|
||||
* Use `devalue` instead of `serialize-javascript`, allowing `preload` to return non-POJOs and cyclical/repeated references, but *not* functions ([#112](https://github.com/sveltejs/sapper/issues/112))
|
||||
* Kill child process if webpack crashes ([#177](https://github.com/sveltejs/sapper/issues/177))
|
||||
* Support HMR on remote devices ([#165](https://github.com/sveltejs/sapper/issues/165))
|
||||
* Remove hard-coded port (([#169](https://github.com/sveltejs/sapper/issues/169)))
|
||||
* Allow non-JS files, e.g. TypeScript to be used as entry points and server routes ([#57](https://github.com/sveltejs/sapper/issues/57))
|
||||
* Faster startup ([#173](https://github.com/sveltejs/sapper/issues/173))
|
||||
|
||||
## 0.8.4
|
||||
|
||||
* Fix route sorting ([#175](https://github.com/sveltejs/sapper/pull/175))
|
||||
|
||||
## 0.8.3
|
||||
|
||||
* Automatically select available port, or use `--port` flag for `dev` and `start` ([#169](https://github.com/sveltejs/sapper/issues/169))
|
||||
* Show stats after build/export ([#168](https://github.com/sveltejs/sapper/issues/168))
|
||||
* Various CLI improvements ([#170](https://github.com/sveltejs/sapper/pull/170))
|
||||
|
||||
## 0.8.2
|
||||
|
||||
* Rename `preloadRoutes` to `prefetchRoutes` ([#166](https://github.com/sveltejs/sapper/issues/166))
|
||||
|
||||
## 0.8.1
|
||||
|
||||
* Add `sapper start` command, for running an app built with `sapper build` ([#163](https://github.com/sveltejs/sapper/issues/163))
|
||||
|
||||
## 0.8.0
|
||||
|
||||
* Update to webpack 4
|
||||
* Add `preloadRoutes` function — secondary routes are no longer automatically preloaded ([#160](https://github.com/sveltejs/sapper/issues/160))
|
||||
* `sapper build` outputs to `build`, `sapper build custom-dir` outputs to `custom-dir` ([#150](https://github.com/sveltejs/sapper/pull/150))
|
||||
* `sapper export` outputs to `export`, `sapper export custom-dir` outputs to `custom-dir` ([#150](https://github.com/sveltejs/sapper/pull/150))
|
||||
* Improved logging ([#158](https://github.com/sveltejs/sapper/pull/158))
|
||||
* URI-encode routes ([#103](https://github.com/sveltejs/sapper/issues/103))
|
||||
* Various performance and stability improvements ([#152](https://github.com/sveltejs/sapper/pull/152))
|
||||
|
||||
## 0.7.6
|
||||
|
||||
* Prevent client-side navigation to server route ([#145](https://github.com/sveltejs/sapper/issues/145))
|
||||
* Don't serve error page for server route errors ([#138](https://github.com/sveltejs/sapper/issues/138))
|
||||
|
||||
## 0.7.5
|
||||
|
||||
* Allow dynamic parameters inside route parts ([#139](https://github.com/sveltejs/sapper/issues/139))
|
||||
|
||||
## 0.7.4
|
||||
|
||||
* Force `NODE_ENV='production'` when running `build` or `export` ([#141](https://github.com/sveltejs/sapper/issues/141))
|
||||
* Use source-map-support ([#134](https://github.com/sveltejs/sapper/pull/134))
|
||||
|
||||
## 0.7.3
|
||||
|
||||
* Handle webpack assets that are arrays instead of strings ([#131](https://github.com/sveltejs/sapper/pull/131))
|
||||
* Wait for new server to start before broadcasting HMR update ([#129](https://github.com/sveltejs/sapper/pull/129))
|
||||
|
||||
## 0.7.2
|
||||
|
||||
* Add `hmr-client.js` to package
|
||||
* Wait until first successful client build before creating service-worker.js
|
||||
|
||||
## 0.7.1
|
||||
|
||||
* Add missing `tslib` dependency
|
||||
|
||||
## 0.7.0
|
||||
|
||||
* Restructure app layout (see [migration guide](https://sapper.svelte.technology/guide#0-6-to-0-7)) ([#126](https://github.com/sveltejs/sapper/pull/126))
|
||||
* Support `this.redirect(status, location)` and `this.error(status, error)` in `preload` functions ([#127](https://github.com/sveltejs/sapper/pull/127))
|
||||
* Add `sapper dev` command
|
||||
* Add `sapper --help` command
|
||||
|
||||
## 0.6.4
|
||||
|
||||
* Prevent phantom HMR requests in production mode ([#114](https://github.com/sveltejs/sapper/pull/114))
|
||||
|
||||
## 0.6.3
|
||||
|
||||
* Ignore non-HTML responses when crawling during `export`
|
||||
* Build in prod mode for `export`
|
||||
|
||||
## 0.6.2
|
||||
|
||||
* Handle unspecified type in `sapper export`
|
||||
|
||||
## 0.6.1
|
||||
|
||||
* Fix `pkg.files` and `pkg.bin`
|
||||
|
||||
## 0.6.0
|
||||
|
||||
* Hydrate on first load, and only on first load ([#93](https://github.com/sveltejs/sapper/pull/93))
|
||||
* Identify clashes between page and server routes ([#96](https://github.com/sveltejs/sapper/pull/96))
|
||||
* Remove Express-specific utilities, for compatbility with Polka et al ([#94](https://github.com/sveltejs/sapper/issues/94))
|
||||
* Return a promise from `init` when first page has rendered ([#99](https://github.com/sveltejs/sapper/issues/99))
|
||||
* Handle invalid hash links ([#104](https://github.com/sveltejs/sapper/pull/104))
|
||||
* Avoid `URLSearchParams` ([#107](https://github.com/sveltejs/sapper/pull/107))
|
||||
* Don't automatically set `Content-Type` for server routes ([#111](https://github.com/sveltejs/sapper/pull/111))
|
||||
* Handle empty query string routes, e.g. `/?` ([#105](https://github.com/sveltejs/sapper/pull/105))
|
||||
|
||||
## 0.5.1
|
||||
|
||||
* Only write service-worker.js to filesystem in dev mode ([#90](https://github.com/sveltejs/sapper/issues/90))
|
||||
|
||||
## 0.5.0
|
||||
|
||||
* Experimental support for `sapper export` ([#9](https://github.com/sveltejs/sapper/issues/9))
|
||||
* Lazily load chokidar, for faster startup ([#64](https://github.com/sveltejs/sapper/pull/64))
|
||||
|
||||
## 0.4.0
|
||||
|
||||
* `%sapper.main%` has been replaced with `%sapper.scripts%` ([#86](https://github.com/sveltejs/sapper/issues/86))
|
||||
* Node 6 support ([#67](https://github.com/sveltejs/sapper/pull/67))
|
||||
* Explicitly load css-loader and style-loader ([#72](https://github.com/sveltejs/sapper/pull/72))
|
||||
* DELETE requests are handled with `del` exports ([#77](https://github.com/sveltejs/sapper/issues/77))
|
||||
* Send preloaded data for first route to client, where possible ([#3](https://github.com/sveltejs/sapper/issues/3))
|
||||
|
||||
## 0.3.2
|
||||
|
||||
* Expose `prefetch` function ([#61](https://github.com/sveltejs/sapper/pull/61))
|
||||
|
||||
## 0.3.1
|
||||
|
||||
* Fix missing `runtime.js`
|
||||
|
||||
## 0.3.0
|
||||
|
||||
* Move `sapper/runtime/app.js` to `sapper/runtime.js`
|
||||
* Cancel navigation if overtaken by second navigation ([#48](https://github.com/sveltejs/sapper/issues/48))
|
||||
* Store preloaded data, to avoiding double prefetching ([#49](https://github.com/sveltejs/sapper/issues/49))
|
||||
* Pass server request object to `preload` ([#54](https://github.com/sveltejs/sapper/pull/54))
|
||||
* Nested routes ([#55](https://github.com/sveltejs/sapper/issues/55))
|
||||
|
||||
## 0.2.10
|
||||
|
||||
* Handle deep links correctly ([#44](https://github.com/sveltejs/sapper/issues/44))
|
||||
|
||||
## 0.2.9
|
||||
|
||||
* Don't write files to disk in prod mode
|
||||
|
||||
## 0.2.8
|
||||
|
||||
* Add `goto` function ([#29](https://github.com/sveltejs/sapper/issues/29))
|
||||
* Don't use `/tmp` as destination in Now environments
|
||||
|
||||
## 0.2.7
|
||||
|
||||
* Fix streaming bug
|
||||
|
||||
## 0.2.6
|
||||
|
||||
* Render main.js back to templates, to allow relative imports ([#40](https://github.com/sveltejs/sapper/issues/40))
|
||||
|
||||
## 0.2.5
|
||||
|
||||
* Fix nested routes on Windows ([#39](https://github.com/sveltejs/sapper/pull/39))
|
||||
* Rebundle when routes and main.js change ([#34](https://github.com/sveltejs/sapper/pull/34))
|
||||
* Add `Link...preload` headers for JavaScript assets ([#2](https://github.com/sveltejs/sapper/issues/2))
|
||||
* Stream document up to first dynamic content ([#19](https://github.com/sveltejs/sapper/issues/19))
|
||||
* Error if routes clash ([#33](https://github.com/sveltejs/sapper/issues/33))
|
||||
|
||||
## 0.2.4
|
||||
|
||||
* Posixify path to HMR client
|
||||
|
||||
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)
|
||||
@@ -15,3 +15,7 @@ environment:
|
||||
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')();
|
||||
}
|
||||
41
lib/build.js
41
lib/build.js
@@ -1,41 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const glob = require('glob');
|
||||
const mkdirp = require('mkdirp');
|
||||
const { client, server } = require('./utils/compilers.js');
|
||||
const create_app = require('./utils/create_app.js');
|
||||
const generate_asset_cache = require('./utils/generate_asset_cache.js');
|
||||
const { dest } = require('./config.js');
|
||||
|
||||
module.exports = () => {
|
||||
mkdirp(dest);
|
||||
|
||||
// create main.js and server-routes.js
|
||||
create_app();
|
||||
|
||||
function handleErrors(err, stats) {
|
||||
if (err) {
|
||||
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (stats.hasErrors()) {
|
||||
console.log(stats.toString({ colors: true }));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
client.run((err, clientStats) => {
|
||||
handleErrors(err, clientStats);
|
||||
const clientInfo = clientStats.toJson();
|
||||
fs.writeFileSync(path.join(dest, 'stats.client.json'), JSON.stringify(clientInfo, null, ' '));
|
||||
|
||||
server.run((err, serverStats) => {
|
||||
handleErrors(err, serverStats);
|
||||
const serverInfo = serverStats.toJson();
|
||||
fs.writeFileSync(path.join(dest, 'stats.server.json'), JSON.stringify(serverInfo, null, ' '));
|
||||
|
||||
generate_asset_cache(clientInfo, serverInfo);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
const path = require('path');
|
||||
const mkdirp = require('mkdirp');
|
||||
const rimraf = require('rimraf');
|
||||
|
||||
exports.dev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
exports.templates = path.resolve(process.env.SAPPER_TEMPLATES || 'templates');
|
||||
|
||||
exports.src = path.resolve(process.env.SAPPER_ROUTES || 'routes');
|
||||
|
||||
exports.dest = path.resolve(
|
||||
process.env.NOW ? '/tmp' :
|
||||
process.env.SAPPER_DEST || '.sapper'
|
||||
);
|
||||
|
||||
if (exports.dev) {
|
||||
mkdirp(exports.dest);
|
||||
rimraf.sync(path.join(exports.dest, '**/*'));
|
||||
}
|
||||
|
||||
exports.server_routes = path.resolve(exports.dest, 'server-routes.js');
|
||||
208
lib/index.js
208
lib/index.js
@@ -1,208 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const glob = require('glob');
|
||||
const rimraf = require('rimraf');
|
||||
const mkdirp = require('mkdirp');
|
||||
const webpack = require('webpack');
|
||||
const route_manager = require('./route_manager.js');
|
||||
const templates = require('./templates.js');
|
||||
const create_app = require('./utils/create_app.js');
|
||||
const create_watcher = require('./utils/create_watcher.js');
|
||||
const compilers = require('./utils/compilers.js');
|
||||
const generate_asset_cache = require('./utils/generate_asset_cache.js');
|
||||
const escape_html = require('escape-html');
|
||||
const { src, dest, dev } = require('./config.js');
|
||||
|
||||
function connect_dev() {
|
||||
// create main.js and server-routes.js
|
||||
// TODO update on changes
|
||||
create_app();
|
||||
|
||||
const watcher = create_watcher();
|
||||
|
||||
let asset_cache;
|
||||
|
||||
return compose_handlers([
|
||||
require('webpack-hot-middleware')(compilers.client, {
|
||||
reload: true,
|
||||
path: '/__webpack_hmr',
|
||||
heartbeat: 10 * 1000
|
||||
}),
|
||||
|
||||
async (req, res, next) => {
|
||||
asset_cache = await watcher.ready;
|
||||
next();
|
||||
},
|
||||
|
||||
set_req_pathname,
|
||||
|
||||
get_asset_handler({
|
||||
filter: pathname => pathname === '/index.html',
|
||||
type: 'text/html',
|
||||
cache: 'max-age=600',
|
||||
fn: () => asset_cache.client.index
|
||||
}),
|
||||
|
||||
get_asset_handler({
|
||||
filter: pathname => pathname === '/service-worker.js',
|
||||
type: 'application/javascript',
|
||||
cache: 'max-age=600',
|
||||
fn: () => asset_cache.client.service_worker
|
||||
}),
|
||||
|
||||
get_asset_handler({
|
||||
filter: pathname => pathname.startsWith('/client/'),
|
||||
type: 'application/javascript',
|
||||
cache: 'max-age=31536000',
|
||||
fn: pathname => asset_cache.client.chunks[pathname]
|
||||
}),
|
||||
|
||||
get_route_handler(() => asset_cache),
|
||||
|
||||
not_found
|
||||
]);
|
||||
}
|
||||
|
||||
function connect_prod() {
|
||||
const asset_cache = generate_asset_cache(
|
||||
read_json(path.join(dest, 'stats.client.json')),
|
||||
read_json(path.join(dest, 'stats.server.json'))
|
||||
);
|
||||
|
||||
return compose_handlers([
|
||||
set_req_pathname,
|
||||
|
||||
get_asset_handler({
|
||||
filter: pathname => pathname === '/index.html',
|
||||
type: 'text/html',
|
||||
cache: 'max-age=600',
|
||||
fn: () => asset_cache.client.index
|
||||
}),
|
||||
|
||||
get_asset_handler({
|
||||
filter: pathname => pathname === '/service-worker.js',
|
||||
type: 'application/javascript',
|
||||
cache: 'max-age=600',
|
||||
fn: () => asset_cache.client.service_worker
|
||||
}),
|
||||
|
||||
get_asset_handler({
|
||||
filter: pathname => pathname.startsWith('/client/'),
|
||||
type: 'application/javascript',
|
||||
cache: 'max-age=31536000',
|
||||
fn: pathname => asset_cache.client.chunks[pathname]
|
||||
}),
|
||||
|
||||
get_route_handler(() => asset_cache),
|
||||
|
||||
not_found
|
||||
]);
|
||||
}
|
||||
|
||||
module.exports = dev ? connect_dev : connect_prod;
|
||||
|
||||
function set_req_pathname(req, res, next) {
|
||||
req.pathname = req.url.replace(/\?.+/, '');
|
||||
next();
|
||||
}
|
||||
|
||||
function get_asset_handler(opts) {
|
||||
return (req, res, next) => {
|
||||
if (!opts.filter(req.pathname)) return next();
|
||||
|
||||
res.set({
|
||||
'Content-Type': opts.type,
|
||||
'Cache-Control': opts.cache
|
||||
});
|
||||
res.end(opts.fn(req.pathname));
|
||||
};
|
||||
}
|
||||
|
||||
function get_route_handler(fn) {
|
||||
return async function handle_route(req, res, next) {
|
||||
const url = req.pathname;
|
||||
|
||||
const { client, server } = fn();
|
||||
|
||||
// whatever happens, we're going to serve some HTML
|
||||
res.set({
|
||||
'Content-Type': 'text/html'
|
||||
});
|
||||
|
||||
try {
|
||||
for (const route of route_manager.routes) {
|
||||
if (route.test(url)) {
|
||||
req.params = route.exec(url);
|
||||
|
||||
const mod = require(server.entry)[route.id];
|
||||
|
||||
if (route.type === 'page') {
|
||||
let data = { params: req.params, query: req.query };
|
||||
if (mod.preload) data = Object.assign(data, await mod.preload(data));
|
||||
|
||||
const { html, head, css } = mod.render(data);
|
||||
|
||||
const page = templates.render(200, {
|
||||
main: client.main_file,
|
||||
html,
|
||||
head: `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`,
|
||||
styles: (css && css.code ? `<style>${css.code}</style>` : '')
|
||||
});
|
||||
|
||||
res.status(200);
|
||||
res.end(page);
|
||||
}
|
||||
|
||||
else {
|
||||
const handler = mod[req.method.toLowerCase()];
|
||||
if (handler) handler(req, res, next);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
} catch(err) {
|
||||
res.status(500).end(templates.render(500, {
|
||||
title: (err && err.name) || 'Internal server error',
|
||||
url,
|
||||
error: escape_html(err && (err.details || err.message || err) || 'Unknown error'),
|
||||
stack: err && err.stack.split('\n').slice(1).join('\n')
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function not_found(req, res) {
|
||||
res.status(404).end(templates.render(404, {
|
||||
title: 'Not found',
|
||||
status: 404,
|
||||
method: req.method,
|
||||
url: 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,16 +0,0 @@
|
||||
const glob = require('glob');
|
||||
const create_routes = require('./utils/create_routes.js');
|
||||
const { src } = require('./config.js');
|
||||
|
||||
const route_manager = {
|
||||
routes: create_routes(
|
||||
glob.sync('**/*.+(html|js|mjs)', { cwd: src })
|
||||
),
|
||||
|
||||
onchange(fn) {
|
||||
// TODO in dev mode, keep this updated, and allow
|
||||
// webpack compiler etc to hook into it
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = route_manager;
|
||||
@@ -1,43 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const glob = require('glob');
|
||||
|
||||
const templates = glob.sync('*.html', { cwd: 'templates' })
|
||||
.map(file => {
|
||||
const template = fs.readFileSync(`templates/${file}`, 'utf-8');
|
||||
const status = file.replace('.html', '').toLowerCase();
|
||||
|
||||
if (!/^[0-9x]{3}$/.test(status)) {
|
||||
throw new Error(`Bad template — should be a valid status code like 404.html, or a wildcard like 2xx.html`);
|
||||
}
|
||||
|
||||
const specificity = (
|
||||
(status[0] === 'x' ? 0 : 4) +
|
||||
(status[1] === 'x' ? 0 : 2) +
|
||||
(status[2] === 'x' ? 0 : 1)
|
||||
);
|
||||
|
||||
const pattern = new RegExp(`^${status.split('').map(d => d === 'x' ? '\\d' : d).join('')}$`);
|
||||
|
||||
return {
|
||||
test: status => pattern.test(status),
|
||||
specificity,
|
||||
render(data) {
|
||||
return template.replace(/%sapper\.(\w+)%/g, (match, key) => {
|
||||
return key in data ? data[key] : '';
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.sort((a, b) => b.specificity - a.specificity);
|
||||
|
||||
exports.render = (status, data) => {
|
||||
const template = templates.find(template => template.test(status));
|
||||
if (template) return template.render(data);
|
||||
|
||||
return `Missing template for status code ${status}`;
|
||||
};
|
||||
|
||||
exports.onchange = fn => {
|
||||
// TODO in dev mode, keep this updated, and allow
|
||||
// webpack compiler etc to hook into it
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
const path = require('path');
|
||||
const relative = require('require-relative');
|
||||
const webpack = relative('webpack', process.cwd());
|
||||
|
||||
exports.client = webpack(
|
||||
require(path.resolve('webpack.client.config.js'))
|
||||
);
|
||||
|
||||
exports.server = webpack(
|
||||
require(path.resolve('webpack.server.config.js'))
|
||||
);
|
||||
@@ -1,70 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const route_manager = require('../route_manager.js');
|
||||
const { src, dest, server_routes, dev } = require('../config.js');
|
||||
|
||||
function posixify(file) {
|
||||
return file.replace(/[\/\\]/g, '/');
|
||||
}
|
||||
|
||||
module.exports = function create_app() {
|
||||
const { routes } = route_manager;
|
||||
|
||||
function create_client_main() {
|
||||
const template = fs.readFileSync('templates/main.js', 'utf-8');
|
||||
|
||||
const code = `[${
|
||||
routes
|
||||
.filter(route => route.type === 'page')
|
||||
.map(route => {
|
||||
const params = route.dynamic.length === 0 ?
|
||||
'{}' :
|
||||
`{ ${route.dynamic.map((part, i) => `${part}: match[${i + 1}]`).join(', ') } }`;
|
||||
|
||||
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();`
|
||||
}
|
||||
|
||||
const file = path.resolve(dest, 'main.js');
|
||||
|
||||
fs.writeFileSync(file, main);
|
||||
|
||||
// need to fudge the mtime, because webpack is soft in the head
|
||||
const { atime, mtime } = fs.statSync(file);
|
||||
fs.utimesSync(file, new Date(atime.getTime() - 999999), new Date(mtime.getTime() - 999999));
|
||||
}
|
||||
|
||||
function create_server_routes() {
|
||||
const imports = routes
|
||||
.map(route => {
|
||||
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(server_routes, `${imports}\n\n${exports}`);
|
||||
|
||||
const { atime, mtime } = fs.statSync(server_routes);
|
||||
fs.utimesSync(server_routes, new Date(atime.getTime() - 999999), new Date(mtime.getTime() - 999999));
|
||||
}
|
||||
|
||||
// TODO in dev mode, watch files
|
||||
create_client_main();
|
||||
create_server_routes();
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = function create_matchers(files) {
|
||||
return files
|
||||
.map(file => {
|
||||
if (/(^|\/|\\)_/.test(file)) return;
|
||||
|
||||
const parts = file.replace(/\.(html|js|mjs)$/, '').split(path.sep);
|
||||
if (parts[parts.length - 1] === 'index') parts.pop();
|
||||
|
||||
const id = (
|
||||
parts.join('_').replace(/[[\]]/g, '$').replace(/^\d/, '_$&').replace(/[^a-zA-Z0-9_$]/g, '_')
|
||||
) || '_';
|
||||
|
||||
const dynamic = parts
|
||||
.filter(part => part[0] === '[')
|
||||
.map(part => part.slice(1, -1));
|
||||
|
||||
const pattern = new RegExp(
|
||||
`^\\/${parts.map(p => p[0] === '[' ? '([^/]+)' : p).join('\\/')}$`
|
||||
);
|
||||
|
||||
const test = url => pattern.test(url);
|
||||
|
||||
const exec = url => {
|
||||
const match = pattern.exec(url);
|
||||
if (!match) return;
|
||||
|
||||
const params = {};
|
||||
dynamic.forEach((param, i) => {
|
||||
params[param] = match[i + 1];
|
||||
});
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
return {
|
||||
id,
|
||||
type: path.extname(file) === '.html' ? 'page' : 'route',
|
||||
file,
|
||||
pattern,
|
||||
test,
|
||||
exec,
|
||||
parts,
|
||||
dynamic
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => {
|
||||
return (
|
||||
(a.dynamic.length - b.dynamic.length) || // match static paths first
|
||||
(b.parts.length - a.parts.length) // match longer paths first
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
const path = require('path');
|
||||
const chalk = require('chalk');
|
||||
const compilers = require('./compilers.js');
|
||||
const generate_asset_cache = require('./generate_asset_cache.js');
|
||||
|
||||
function deferred() {
|
||||
const d = {};
|
||||
|
||||
d.promise = new Promise((fulfil, reject) => {
|
||||
d.fulfil = fulfil;
|
||||
d.reject = reject;
|
||||
});
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
module.exports = function create_watcher() {
|
||||
const deferreds = {
|
||||
client: deferred(),
|
||||
server: deferred()
|
||||
};
|
||||
|
||||
const invalidate = () => Promise.all([
|
||||
deferreds.client.promise,
|
||||
deferreds.server.promise
|
||||
]).then(([client_stats, server_stats]) => {
|
||||
return generate_asset_cache(
|
||||
client_stats.toJson(),
|
||||
server_stats.toJson()
|
||||
);
|
||||
});
|
||||
|
||||
watcher = {
|
||||
ready: invalidate()
|
||||
};
|
||||
|
||||
function watch_compiler(type) {
|
||||
const compiler = compilers[type];
|
||||
|
||||
compiler.plugin('invalid', filename => {
|
||||
console.log(chalk.red(`${type} bundle invalidated, file changed: ${chalk.bold(filename)}`));
|
||||
deferreds[type] = deferred();
|
||||
watcher.ready = invalidate();
|
||||
});
|
||||
|
||||
compiler.plugin('failed', err => {
|
||||
deferreds[type].reject(err);
|
||||
});
|
||||
|
||||
compiler.watch({}, (err, stats) => {
|
||||
if (stats.hasErrors()) {
|
||||
deferreds[type].reject(stats.toJson().errors[0]);
|
||||
} else {
|
||||
deferreds[type].fulfil(stats);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
watch_compiler('client');
|
||||
watch_compiler('server');
|
||||
|
||||
return watcher;
|
||||
};
|
||||
@@ -1,67 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const glob = require('glob');
|
||||
const templates = require('../templates.js');
|
||||
const route_manager = require('../route_manager.js');
|
||||
const { dest } = require('../config.js');
|
||||
|
||||
module.exports = function generate_asset_cache(clientInfo, serverInfo) {
|
||||
const main_file = `/client/${clientInfo.assetsByChunkName.main}`;
|
||||
const chunk_files = clientInfo.assets.map(chunk => `/client/${chunk.name}`);
|
||||
|
||||
const service_worker = generate_service_worker(chunk_files);
|
||||
const index = generate_index(main_file);
|
||||
|
||||
fs.writeFileSync(path.join(dest, 'service-worker.js'), service_worker);
|
||||
fs.writeFileSync(path.join(dest, 'index.html'), index);
|
||||
|
||||
return {
|
||||
client: {
|
||||
main_file,
|
||||
chunk_files,
|
||||
|
||||
main: read(`${dest}${main_file}`),
|
||||
chunks: chunk_files.reduce((lookup, file) => {
|
||||
lookup[file] = read(`${dest}${file}`);
|
||||
return lookup;
|
||||
}, {}),
|
||||
|
||||
index,
|
||||
service_worker
|
||||
},
|
||||
|
||||
server: {
|
||||
entry: path.resolve(dest, 'server', serverInfo.assetsByChunkName.server_routes)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function generate_service_worker(chunk_files) {
|
||||
const assets = glob.sync('**', { cwd: 'assets', nodir: true });
|
||||
|
||||
const route_code = `[${
|
||||
route_manager.routes
|
||||
.filter(route => route.type === 'page')
|
||||
.map(route => `{ pattern: ${route.pattern} }`)
|
||||
.join(', ')
|
||||
}]`;
|
||||
|
||||
return read('templates/service-worker.js')
|
||||
.replace('__timestamp__', Date.now())
|
||||
.replace('__assets__', JSON.stringify(assets))
|
||||
.replace('__shell__', JSON.stringify(chunk_files.concat('/index.html')))
|
||||
.replace('__routes__', route_code);
|
||||
}
|
||||
|
||||
function generate_index(main_file) {
|
||||
return templates.render(200, {
|
||||
styles: '',
|
||||
head: '',
|
||||
html: '<noscript>Please enable JavaScript!</noscript>',
|
||||
main: main_file
|
||||
});
|
||||
}
|
||||
|
||||
function read(file) {
|
||||
return fs.readFileSync(file, 'utf-8');
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
--require source-map-support/register
|
||||
--recursive
|
||||
test/unit/**/*.js
|
||||
test/unit/**/*.js
|
||||
test/common/test.js
|
||||
5343
package-lock.json
generated
5343
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
76
package.json
76
package.json
@@ -1,41 +1,79 @@
|
||||
{
|
||||
"name": "sapper",
|
||||
"version": "0.2.4",
|
||||
"version": "0.10.5",
|
||||
"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",
|
||||
"dist"
|
||||
],
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^2.3.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"cheerio": "^1.0.0-rc.2",
|
||||
"chokidar": "^2.0.3",
|
||||
"clorox": "^1.0.3",
|
||||
"cookie": "^0.3.1",
|
||||
"devalue": "^1.0.1",
|
||||
"glob": "^7.1.2",
|
||||
"html-minifier": "^3.5.11",
|
||||
"mkdirp": "^0.5.1",
|
||||
"relative": "^3.0.2",
|
||||
"node-fetch": "^2.1.1",
|
||||
"port-authority": "^1.0.2",
|
||||
"pretty-bytes": "^4.0.2",
|
||||
"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.0",
|
||||
"sander": "^0.6.0",
|
||||
"source-map-support": "^0.5.4",
|
||||
"tslib": "^1.9.0",
|
||||
"url-parse": "^1.2.0",
|
||||
"webpack-format-messages": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cypress": "^1.3.0",
|
||||
"@std/esm": "^0.25.3",
|
||||
"@types/glob": "^5.0.34",
|
||||
"@types/mkdirp": "^0.5.2",
|
||||
"@types/rimraf": "^2.0.2",
|
||||
"compression": "^1.7.1",
|
||||
"eslint": "^4.13.1",
|
||||
"mocha": "^4.0.1",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"express": "^4.16.3",
|
||||
"get-port": "^3.2.0",
|
||||
"mocha": "^5.0.4",
|
||||
"nightmare": "^3.0.0",
|
||||
"npm-run-all": "^4.1.2",
|
||||
"wait-on": "^2.0.2"
|
||||
"polka": "^0.3.4",
|
||||
"rollup": "^0.57.0",
|
||||
"rollup-plugin-commonjs": "^9.1.0",
|
||||
"rollup-plugin-json": "^2.3.0",
|
||||
"rollup-plugin-string": "^2.0.2",
|
||||
"rollup-plugin-typescript": "^0.8.1",
|
||||
"serve-static": "^1.13.2",
|
||||
"svelte": "^1.57.4",
|
||||
"svelte-loader": "^2.5.1",
|
||||
"ts-node": "^5.0.1",
|
||||
"typescript": "^2.6.2",
|
||||
"walk-sync": "^0.3.2",
|
||||
"webpack": "^4.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"cy:open": "cypress open",
|
||||
"test": "run-s test:unit test:dev test:prod",
|
||||
"test:unit": "mocha --opts mocha.opts",
|
||||
"test:dev": "run-p --race test:launch:dev cy:run:dev",
|
||||
"test:launch:dev": "node test/launch.js --dev",
|
||||
"cy:run:dev": "wait-on http://localhost:3000 && cypress run -s test/cypress/integration/dev.js",
|
||||
"test:prod": "run-p --race test:launch:prod cy:run:prod",
|
||||
"test:launch:prod": "node test/launch.js --prod",
|
||||
"cy:run:prod": "wait-on http://localhost:3000 && cypress run -s test/cypress/integration/prod.js"
|
||||
"test": "mocha --opts mocha.opts",
|
||||
"pretest": "npm run build",
|
||||
"build": "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": [
|
||||
|
||||
48
rollup.config.js
Normal file
48
rollup.config.js
Normal file
@@ -0,0 +1,48 @@
|
||||
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')
|
||||
})
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
input: [`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.
|
||||
194
runtime/app.js
194
runtime/app.js
@@ -1,192 +1,2 @@
|
||||
const detach = node => {
|
||||
node.parentNode.removeChild(node);
|
||||
};
|
||||
|
||||
export let component;
|
||||
let target;
|
||||
let routes;
|
||||
|
||||
const scroll_history = {};
|
||||
let uid = 1;
|
||||
let cid;
|
||||
|
||||
if ('scrollRestoration' in history) {
|
||||
history.scrollRestoration = 'manual'
|
||||
}
|
||||
|
||||
function select_route(url) {
|
||||
if (url.origin !== window.location.origin) return null;
|
||||
|
||||
for (const route of routes) {
|
||||
const match = route.pattern.exec(url.pathname);
|
||||
if (match) {
|
||||
const params = route.params(match);
|
||||
|
||||
const query = {};
|
||||
for (const [key, value] of url.searchParams) query[key] = value || true;
|
||||
|
||||
return { route, data: { params, query } };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function render(Component, data, scroll) {
|
||||
Promise.resolve(
|
||||
Component.preload ? Component.preload(data) : {}
|
||||
).then(preloaded => {
|
||||
if (component) {
|
||||
component.destroy();
|
||||
} else {
|
||||
// first load — remove SSR'd <head> contents
|
||||
const start = document.querySelector('#sapper-head-start');
|
||||
let end = document.querySelector('#sapper-head-end');
|
||||
|
||||
if (start && end) {
|
||||
while (start.nextSibling !== end) detach(start.nextSibling);
|
||||
detach(start);
|
||||
detach(end);
|
||||
}
|
||||
|
||||
// preload additional routes
|
||||
routes.reduce((promise, route) => promise.then(route.load), Promise.resolve());
|
||||
}
|
||||
|
||||
component = new Component({
|
||||
target,
|
||||
data: Object.assign(data, preloaded),
|
||||
hydrate: !!component
|
||||
});
|
||||
|
||||
if (scroll) {
|
||||
window.scrollTo(scroll.x, scroll.y);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function navigate(url, id) {
|
||||
const selected = select_route(url);
|
||||
if (selected) {
|
||||
if (id) {
|
||||
// popstate or initial navigation
|
||||
cid = id;
|
||||
} else {
|
||||
// clicked on a link. preserve scroll state
|
||||
scroll_history[cid] = scroll_state();
|
||||
|
||||
id = cid = ++uid;
|
||||
scroll_history[cid] = { x: 0, y: 0 };
|
||||
|
||||
history.pushState({ id }, '', url.href);
|
||||
}
|
||||
|
||||
selected.route.load().then(mod => {
|
||||
render(mod.default, selected.data, scroll_history[id]);
|
||||
});
|
||||
|
||||
cid = id;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function handle_click(event) {
|
||||
// Adapted from https://github.com/visionmedia/page.js
|
||||
// MIT license https://github.com/visionmedia/page.js#license
|
||||
if (which(event) !== 1) return;
|
||||
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
|
||||
if (event.defaultPrevented) return;
|
||||
|
||||
const a = findAnchor(event.target);
|
||||
if (!a) return;
|
||||
|
||||
// check if link is inside an svg
|
||||
// in this case, both href and target are always inside an object
|
||||
const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
|
||||
const href = svg ? a.href.baseVal : a.href;
|
||||
|
||||
if (href === window.location.href) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore if tag has
|
||||
// 1. 'download' attribute
|
||||
// 2. rel='external' attribute
|
||||
if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
|
||||
|
||||
// Ignore if <a> has a target
|
||||
if (svg ? a.target.baseVal : a.target) return;
|
||||
|
||||
const url = new URL(href);
|
||||
|
||||
// Don't handle hash changes
|
||||
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
|
||||
|
||||
if (navigate(url, null)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function handle_popstate(event) {
|
||||
scroll_history[cid] = scroll_state();
|
||||
|
||||
if (event.state) {
|
||||
navigate(new URL(window.location), event.state.id);
|
||||
} else {
|
||||
// hashchange
|
||||
cid = ++uid;
|
||||
history.replaceState({ id: cid }, '', window.location.href);
|
||||
}
|
||||
}
|
||||
|
||||
function prefetch(event) {
|
||||
const a = findAnchor(event.target);
|
||||
if (!a || a.rel !== 'prefetch') return;
|
||||
|
||||
const selected = select_route(new URL(a.href));
|
||||
|
||||
if (selected) {
|
||||
selected.route.load().then(mod => {
|
||||
if (mod.default.preload) mod.default.preload(selected.data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function findAnchor(node) {
|
||||
while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG <a> elements have a lowercase name
|
||||
return node;
|
||||
}
|
||||
|
||||
let inited;
|
||||
|
||||
export function init(_target, _routes) {
|
||||
target = _target;
|
||||
routes = _routes;
|
||||
|
||||
if (!inited) { // this check makes HMR possible
|
||||
window.addEventListener('click', handle_click);
|
||||
window.addEventListener('popstate', handle_popstate);
|
||||
|
||||
// prefetch
|
||||
window.addEventListener('touchstart', prefetch);
|
||||
window.addEventListener('mouseover', prefetch);
|
||||
|
||||
inited = true;
|
||||
}
|
||||
|
||||
const scroll = scroll_history[uid] = scroll_state();
|
||||
|
||||
history.replaceState({ id: uid }, '', window.location.href);
|
||||
navigate(new URL(window.location), uid);
|
||||
}
|
||||
|
||||
function which(event) {
|
||||
event = event || window.event;
|
||||
return event.which === null ? event.button : event.which;
|
||||
}
|
||||
|
||||
function scroll_state() {
|
||||
return {
|
||||
x: window.scrollX,
|
||||
y: window.scrollY
|
||||
};
|
||||
}
|
||||
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();
|
||||
}
|
||||
};
|
||||
}
|
||||
80
src/cli.ts
Executable file
80
src/cli.ts
Executable file
@@ -0,0 +1,80 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as child_process from 'child_process';
|
||||
import sade from 'sade';
|
||||
import * as clorox from 'clorox';
|
||||
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')
|
||||
.action(async (dest = 'build') => {
|
||||
console.log(`> Building...`);
|
||||
|
||||
process.env.NODE_ENV = 'production';
|
||||
process.env.SAPPER_DEST = dest;
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
try {
|
||||
const { build } = await import('./cli/build');
|
||||
await build();
|
||||
console.error(`\n> Finished in ${elapsed(start)}. Type ${clorox.bold.cyan(dest === 'build' ? 'npx sapper start' : `npx sapper start ${dest}`)} to run the app.`);
|
||||
} catch (err) {
|
||||
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
||||
}
|
||||
});
|
||||
|
||||
prog.command('start [dir]')
|
||||
.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 ${clorox.bold.cyan(`npx serve ${dest}`)} to run the app.`);
|
||||
} catch (err) {
|
||||
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
||||
}
|
||||
});
|
||||
|
||||
// TODO upgrade
|
||||
|
||||
prog.parse(process.argv);
|
||||
|
||||
function elapsed(start: number) {
|
||||
return prettyMs(Date.now() - start);
|
||||
}
|
||||
76
src/cli/build.ts
Normal file
76
src/cli/build.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as clorox from 'clorox';
|
||||
import mkdirp from 'mkdirp';
|
||||
import rimraf from 'rimraf';
|
||||
import { minify_html } from './utils/minify_html';
|
||||
import { create_compilers, create_main_manifests, create_routes, create_serviceworker_manifest } from '../core'
|
||||
import { locations } from '../config';
|
||||
|
||||
export async function build() {
|
||||
const output = locations.dest();
|
||||
|
||||
mkdirp.sync(output);
|
||||
rimraf.sync(path.join(output, '**/*'));
|
||||
|
||||
// minify app/template.html
|
||||
// TODO compile this to a function? could be quicker than str.replace(...).replace(...).replace(...)
|
||||
const template = fs.readFileSync(`${locations.app()}/template.html`, 'utf-8');
|
||||
|
||||
// remove this in a future version
|
||||
if (template.indexOf('%sapper.base%') === -1) {
|
||||
console.log(`${clorox.bold.red(`> As of Sapper v0.10, your template.html file must include %sapper.base% in the <head>`)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
fs.writeFileSync(`${output}/template.html`, minify_html(template));
|
||||
|
||||
const routes = create_routes();
|
||||
|
||||
// create app/manifest/client.js and app/manifest/server.js
|
||||
create_main_manifests({ routes });
|
||||
|
||||
const { client, server, serviceworker } = create_compilers();
|
||||
|
||||
const client_stats = await compile(client);
|
||||
console.log(`${clorox.inverse(`\nbuilt client`)}`);
|
||||
console.log(client_stats.toString({ colors: true }));
|
||||
fs.writeFileSync(path.join(output, 'client_info.json'), JSON.stringify(client_stats.toJson()));
|
||||
|
||||
const server_stats = await compile(server);
|
||||
console.log(`${clorox.inverse(`\nbuilt server`)}`);
|
||||
console.log(server_stats.toString({ colors: true }));
|
||||
|
||||
let serviceworker_stats;
|
||||
|
||||
if (serviceworker) {
|
||||
create_serviceworker_manifest({
|
||||
routes,
|
||||
client_files: client_stats.toJson().assets.map((chunk: { name: string }) => `client/${chunk.name}`)
|
||||
});
|
||||
|
||||
serviceworker_stats = await compile(serviceworker);
|
||||
console.log(`${clorox.inverse(`\nbuilt service worker`)}`);
|
||||
console.log(serviceworker_stats.toString({ colors: true }));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
321
src/cli/dev.ts
Normal file
321
src/cli/dev.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as net from 'net';
|
||||
import * as clorox from 'clorox';
|
||||
import * as child_process from 'child_process';
|
||||
import * as http from 'http';
|
||||
import mkdirp from 'mkdirp';
|
||||
import rimraf from 'rimraf';
|
||||
import format_messages from 'webpack-format-messages';
|
||||
import prettyMs from 'pretty-ms';
|
||||
import * as ports from 'port-authority';
|
||||
import { locations } from '../config';
|
||||
import { create_compilers, create_main_manifests, create_routes, create_serviceworker_manifest } from '../core';
|
||||
|
||||
type Deferred = {
|
||||
promise?: Promise<any>;
|
||||
fulfil?: (value?: any) => void;
|
||||
reject?: (err: Error) => void;
|
||||
}
|
||||
|
||||
function deferred() {
|
||||
const d: Deferred = {};
|
||||
|
||||
d.promise = new Promise((fulfil, reject) => {
|
||||
d.fulfil = fulfil;
|
||||
d.reject = reject;
|
||||
});
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
function create_hot_update_server(port: number, interval = 10000) {
|
||||
const clients = new Set();
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.url !== '/__sapper__') return;
|
||||
|
||||
req.socket.setKeepAlive(true);
|
||||
res.writeHead(200, {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'Cache-Control',
|
||||
'Content-Type': 'text/event-stream;charset=utf-8',
|
||||
'Cache-Control': 'no-cache, no-transform',
|
||||
'Connection': 'keep-alive',
|
||||
// While behind nginx, event stream should not be buffered:
|
||||
// http://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffering
|
||||
'X-Accel-Buffering': 'no'
|
||||
});
|
||||
|
||||
res.write('\n');
|
||||
|
||||
clients.add(res);
|
||||
req.on('close', () => {
|
||||
clients.delete(res);
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(port);
|
||||
|
||||
function send(data: any) {
|
||||
clients.forEach(client => {
|
||||
client.write(`data: ${JSON.stringify(data)}\n\n`);
|
||||
});
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
send(null)
|
||||
}, interval);
|
||||
|
||||
return { send };
|
||||
}
|
||||
|
||||
export async function dev(opts: { port: number, open: boolean }) {
|
||||
// remove this in a future version
|
||||
const template = fs.readFileSync(path.join(locations.app(), 'template.html'), 'utf-8');
|
||||
if (template.indexOf('%sapper.base%') === -1) {
|
||||
console.log(`${clorox.bold.red(`> As of Sapper v0.10, your template.html file must include %sapper.base% in the <head>`)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
let port = opts.port || +process.env.PORT;
|
||||
|
||||
if (port) {
|
||||
if (!await ports.check(port)) {
|
||||
console.log(`${clorox.bold.red(`> Port ${port} is unavailable`)}`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
port = await ports.find(3000);
|
||||
}
|
||||
|
||||
const dir = locations.dest();
|
||||
rimraf.sync(dir);
|
||||
mkdirp.sync(dir);
|
||||
|
||||
const dev_port = await ports.find(10000);
|
||||
|
||||
const routes = create_routes();
|
||||
create_main_manifests({ routes, dev_port });
|
||||
|
||||
const hot_update_server = create_hot_update_server(dev_port);
|
||||
|
||||
watch_files(locations.routes(), ['add', 'unlink'], () => {
|
||||
const routes = create_routes();
|
||||
create_main_manifests({ routes, dev_port });
|
||||
});
|
||||
|
||||
watch_files(`${locations.app()}/template.html`, ['change'], () => {
|
||||
hot_update_server.send({
|
||||
action: 'reload'
|
||||
});
|
||||
});
|
||||
|
||||
let proc: child_process.ChildProcess;
|
||||
|
||||
process.on('exit', () => {
|
||||
// sometimes webpack crashes, so we need to kill our children
|
||||
if (proc) proc.kill();
|
||||
});
|
||||
|
||||
const deferreds = {
|
||||
server: deferred(),
|
||||
client: deferred()
|
||||
};
|
||||
|
||||
let restarting = false;
|
||||
let build = {
|
||||
unique_warnings: new Set(),
|
||||
unique_errors: new Set()
|
||||
};
|
||||
|
||||
function restart_build(filename: string) {
|
||||
if (restarting) return;
|
||||
|
||||
restarting = true;
|
||||
build = {
|
||||
unique_warnings: new Set(),
|
||||
unique_errors: new Set()
|
||||
};
|
||||
|
||||
process.nextTick(() => {
|
||||
restarting = false;
|
||||
});
|
||||
|
||||
console.log(`\n${clorox.bold.cyan(path.relative(process.cwd(), filename))} changed. rebuilding...`);
|
||||
}
|
||||
|
||||
// TODO watch the configs themselves?
|
||||
const compilers = create_compilers();
|
||||
|
||||
function watch(compiler: any, { name, invalid = noop, error = noop, result }: {
|
||||
name: string,
|
||||
invalid?: (filename: string) => void;
|
||||
error?: (error: Error) => void;
|
||||
result: (stats: any) => void;
|
||||
}) {
|
||||
compiler.hooks.invalid.tap('sapper', (filename: string) => {
|
||||
invalid(filename);
|
||||
});
|
||||
|
||||
compiler.watch({}, (err: Error, stats: any) => {
|
||||
if (err) {
|
||||
console.log(`${clorox.red(`✗ ${name}`)}`);
|
||||
console.log(`${clorox.red(err.message)}`);
|
||||
error(err);
|
||||
} else {
|
||||
const messages = format_messages(stats);
|
||||
const info = stats.toJson();
|
||||
|
||||
if (messages.errors.length > 0) {
|
||||
console.log(`${clorox.bold.red(`✗ ${name}`)}`);
|
||||
|
||||
const filtered = messages.errors.filter((message: string) => {
|
||||
return !build.unique_errors.has(message);
|
||||
});
|
||||
|
||||
filtered.forEach((message: string) => {
|
||||
build.unique_errors.add(message);
|
||||
console.log(message);
|
||||
});
|
||||
|
||||
const hidden = messages.errors.length - filtered.length;
|
||||
if (hidden > 0) {
|
||||
console.log(`${hidden} duplicate ${hidden === 1 ? 'error' : 'errors'} hidden\n`);
|
||||
}
|
||||
} else {
|
||||
if (messages.warnings.length > 0) {
|
||||
console.log(`${clorox.bold.yellow(`• ${name}`)}`);
|
||||
|
||||
const filtered = messages.warnings.filter((message: string) => {
|
||||
return !build.unique_warnings.has(message);
|
||||
});
|
||||
|
||||
filtered.forEach((message: string) => {
|
||||
build.unique_warnings.add(message);
|
||||
console.log(`${message}\n`);
|
||||
});
|
||||
|
||||
const hidden = messages.warnings.length - filtered.length;
|
||||
if (hidden > 0) {
|
||||
console.log(`${hidden} duplicate ${hidden === 1 ? 'warning' : 'warnings'} hidden\n`);
|
||||
}
|
||||
} else {
|
||||
console.log(`${clorox.bold.green(`✔ ${name}`)} ${clorox.gray(`(${prettyMs(info.time)})`)}`);
|
||||
}
|
||||
|
||||
result(info);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
watch(compilers.server, {
|
||||
name: 'server',
|
||||
|
||||
invalid: filename => {
|
||||
restart_build(filename);
|
||||
// TODO print message
|
||||
deferreds.server = deferred();
|
||||
},
|
||||
|
||||
result: info => {
|
||||
// TODO log compile errors/warnings
|
||||
|
||||
fs.writeFileSync(path.join(dir, 'server_info.json'), JSON.stringify(info, null, ' '));
|
||||
|
||||
deferreds.client.promise.then(() => {
|
||||
function restart() {
|
||||
ports.wait(port).then(deferreds.server.fulfil);
|
||||
}
|
||||
|
||||
if (proc) {
|
||||
proc.kill();
|
||||
proc.on('exit', restart);
|
||||
} else {
|
||||
restart();
|
||||
}
|
||||
|
||||
proc = child_process.fork(`${dir}/server.js`, [], {
|
||||
cwd: process.cwd(),
|
||||
env: Object.assign({
|
||||
PORT: port
|
||||
}, process.env)
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let first = true;
|
||||
|
||||
watch(compilers.client, {
|
||||
name: 'client',
|
||||
|
||||
invalid: filename => {
|
||||
restart_build(filename);
|
||||
deferreds.client = deferred();
|
||||
|
||||
// TODO we should delete old assets. due to a webpack bug
|
||||
// i don't even begin to comprehend, this is apparently
|
||||
// quite difficult
|
||||
},
|
||||
|
||||
result: info => {
|
||||
fs.writeFileSync(path.join(dir, 'client_info.json'), JSON.stringify(info, null, ' '));
|
||||
deferreds.client.fulfil();
|
||||
|
||||
const client_files = info.assets.map((chunk: { name: string }) => `client/${chunk.name}`);
|
||||
|
||||
deferreds.server.promise.then(() => {
|
||||
hot_update_server.send({
|
||||
status: 'completed'
|
||||
});
|
||||
|
||||
if (first) {
|
||||
first = false;
|
||||
console.log(`${clorox.bold.cyan(`> Listening on http://localhost:${port}`)}`);
|
||||
if (opts.open) child_process.exec(`open http://localhost:${port}`);
|
||||
}
|
||||
});
|
||||
|
||||
create_serviceworker_manifest({
|
||||
routes: create_routes(),
|
||||
client_files
|
||||
});
|
||||
|
||||
watch_serviceworker();
|
||||
}
|
||||
});
|
||||
|
||||
let watch_serviceworker = compilers.serviceworker
|
||||
? function() {
|
||||
watch_serviceworker = noop;
|
||||
|
||||
watch(compilers.serviceworker, {
|
||||
name: 'service worker',
|
||||
|
||||
result: info => {
|
||||
fs.writeFileSync(path.join(dir, 'serviceworker_info.json'), JSON.stringify(info, null, ' '));
|
||||
}
|
||||
});
|
||||
}
|
||||
: noop;
|
||||
}
|
||||
|
||||
function noop() {}
|
||||
|
||||
function watch_files(pattern: string, events: string[], callback: () => void) {
|
||||
const chokidar = require('chokidar');
|
||||
|
||||
const watcher = chokidar.watch(pattern, {
|
||||
persistent: true,
|
||||
ignoreInitial: true,
|
||||
disableGlobbing: true
|
||||
});
|
||||
|
||||
events.forEach(event => {
|
||||
watcher.on(event, callback);
|
||||
});
|
||||
}
|
||||
106
src/cli/export.ts
Normal file
106
src/cli/export.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import * as child_process from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as sander from 'sander';
|
||||
import * as clorox from 'clorox';
|
||||
import cheerio from 'cheerio';
|
||||
import URL from 'url-parse';
|
||||
import fetch from 'node-fetch';
|
||||
import * as ports from 'port-authority';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import { minify_html } from './utils/minify_html';
|
||||
import { locations } from '../config';
|
||||
|
||||
export async function exporter(export_dir: string, { basepath = '' }) {
|
||||
const build_dir = locations.dest();
|
||||
|
||||
export_dir = path.join(export_dir, basepath);
|
||||
|
||||
// Prep output directory
|
||||
sander.rimrafSync(export_dir);
|
||||
|
||||
sander.copydirSync('assets').to(export_dir);
|
||||
sander.copydirSync(build_dir, 'client').to(export_dir, 'client');
|
||||
|
||||
if (sander.existsSync(build_dir, 'service-worker.js')) {
|
||||
sander.copyFileSync(build_dir, 'service-worker.js').to(export_dir, 'service-worker.js');
|
||||
}
|
||||
|
||||
if (sander.existsSync(build_dir, 'service-worker.js.map')) {
|
||||
sander.copyFileSync(build_dir, '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_dir}/server.js`), [], {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
PORT: port,
|
||||
NODE_ENV: 'production',
|
||||
SAPPER_DEST: build_dir,
|
||||
SAPPER_EXPORT: 'true'
|
||||
}
|
||||
});
|
||||
|
||||
const seen = new Set();
|
||||
const saved = new Set();
|
||||
|
||||
proc.on('message', message => {
|
||||
if (!message.__sapper__) return;
|
||||
|
||||
let file = new URL(message.url, origin).pathname.slice(1);
|
||||
let { body } = message;
|
||||
|
||||
if (saved.has(file)) return;
|
||||
saved.add(file);
|
||||
|
||||
const is_html = message.type === 'text/html';
|
||||
|
||||
if (is_html) {
|
||||
file = file === '' ? 'index.html' : `${file}/index.html`;
|
||||
body = minify_html(body);
|
||||
}
|
||||
|
||||
console.log(`${clorox.bold.cyan(file)} ${clorox.gray(`(${prettyBytes(body.length)})`)}`);
|
||||
|
||||
sander.writeFileSync(export_dir, file, body);
|
||||
});
|
||||
|
||||
async function handle(url: URL) {
|
||||
const r = await fetch(url.href);
|
||||
const range = ~~(r.status / 100);
|
||||
|
||||
if (range >= 4) {
|
||||
console.log(`${clorox.red(`> Received ${r.status} response when fetching ${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());
|
||||
}
|
||||
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 clorox from 'clorox';
|
||||
import * as ports from 'port-authority';
|
||||
|
||||
export async function start(dir: string, opts: { port: number, open: boolean }) {
|
||||
let port = opts.port || +process.env.PORT;
|
||||
|
||||
const resolved = path.resolve(dir);
|
||||
const server = path.resolve(dir, 'server.js');
|
||||
|
||||
if (!fs.existsSync(server)) {
|
||||
console.log(clorox.bold.red(`> ${dir}/server.js does not exist — type ${clorox.bold.cyan(dir === 'build' ? `npx sapper build` : `npx sapper build ${dir}`)} to create it`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (port) {
|
||||
if (!await ports.check(port)) {
|
||||
console.log(clorox.bold.red(`> Port ${port} is unavailable`));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
port = await ports.find(3000);
|
||||
}
|
||||
|
||||
child_process.fork(server, [], {
|
||||
cwd: process.cwd(),
|
||||
env: Object.assign({
|
||||
NODE_ENV: 'production',
|
||||
PORT: port,
|
||||
SAPPER_DEST: dir
|
||||
}, process.env)
|
||||
});
|
||||
|
||||
await ports.wait(port);
|
||||
console.log(`${clorox.bold.cyan(`> Listening on 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 clorox from 'clorox';
|
||||
|
||||
export default async function upgrade() {
|
||||
const upgraded = [
|
||||
await upgrade_sapper_main()
|
||||
].filter(Boolean);
|
||||
|
||||
if (upgraded.length === 0) {
|
||||
console.log(`No changes!`);
|
||||
}
|
||||
}
|
||||
|
||||
async function upgrade_sapper_main() {
|
||||
const _2xx = read('templates/2xx.html');
|
||||
const _4xx = read('templates/4xx.html');
|
||||
const _5xx = read('templates/5xx.html');
|
||||
|
||||
const pattern = /<script src='\%sapper\.main\%'><\/script>/;
|
||||
|
||||
let replaced = false;
|
||||
|
||||
['2xx', '4xx', '5xx'].forEach(code => {
|
||||
const file = `templates/${code}.html`
|
||||
const template = read(file);
|
||||
if (!template) return;
|
||||
|
||||
if (/\%sapper\.main\%/.test(template)) {
|
||||
if (!pattern.test(template)) {
|
||||
console.log(`${clorox.red(`Could not replace %sapper.main% in ${file}`)}`);
|
||||
} else {
|
||||
write(file, template.replace(pattern, `%sapper.scripts%`));
|
||||
console.log(`${clorox.green(`Replaced %sapper.main% in ${file}`)}`);
|
||||
replaced = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return replaced;
|
||||
}
|
||||
|
||||
function read(file: string) {
|
||||
try {
|
||||
return fs.readFileSync(file, 'utf-8');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function write(file: string, data: string) {
|
||||
fs.writeFileSync(file, data);
|
||||
}
|
||||
21
src/cli/utils/minify_html.ts
Normal file
21
src/cli/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
|
||||
});
|
||||
}
|
||||
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() {
|
||||
const webpack = relative('webpack', process.cwd());
|
||||
|
||||
const serviceworker_config = try_require(path.resolve('webpack/service-worker.config.js'));
|
||||
|
||||
return {
|
||||
client: webpack(
|
||||
require(path.resolve('webpack/client.config.js'))
|
||||
),
|
||||
|
||||
server: webpack(
|
||||
require(path.resolve('webpack/server.config.js'))
|
||||
),
|
||||
|
||||
serviceworker: serviceworker_config && webpack(serviceworker_config)
|
||||
};
|
||||
}
|
||||
|
||||
function try_require(specifier: string) {
|
||||
try {
|
||||
return require(specifier);
|
||||
} catch (err) {
|
||||
if (err.code === 'MODULE_NOT_FOUND') return null;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
116
src/core/create_manifests.ts
Normal file
116
src/core/create_manifests.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as glob from 'glob';
|
||||
import create_routes from './create_routes';
|
||||
import { posixify, write_if_changed } from './utils';
|
||||
import { dev, locations } from '../config';
|
||||
import { Route } from '../interfaces';
|
||||
|
||||
export function create_main_manifests({ routes, dev_port }: {
|
||||
routes: Route[];
|
||||
dev_port?: number;
|
||||
}) {
|
||||
const path_to_routes = path.relative(`${locations.app()}/manifest`, locations.routes());
|
||||
|
||||
const client_manifest = generate_client(routes, path_to_routes, dev_port);
|
||||
const server_manifest = generate_server(routes, path_to_routes);
|
||||
|
||||
write_if_changed(`${locations.app()}/manifest/client.js`, client_manifest);
|
||||
write_if_changed(`${locations.app()}/manifest/server.js`, server_manifest);
|
||||
}
|
||||
|
||||
export function create_serviceworker_manifest({ routes, client_files }: {
|
||||
routes: Route[];
|
||||
client_files: string[];
|
||||
}) {
|
||||
const assets = glob.sync('**', { cwd: 'assets', nodir: true });
|
||||
|
||||
let code = `
|
||||
// This file is generated by Sapper — do not edit it!
|
||||
export const timestamp = ${Date.now()};
|
||||
|
||||
export const assets = [\n\t${assets.map((x: string) => `"${x}"`).join(',\n\t')}\n];
|
||||
|
||||
export const shell = [\n\t${client_files.map((x: string) => `"${x}"`).join(',\n\t')}\n];
|
||||
|
||||
export const routes = [\n\t${routes.filter((r: Route) => r.type === 'page' && !/^_[45]xx$/.test(r.id)).map((r: Route) => `{ pattern: ${r.pattern} }`).join(',\n\t')}\n];
|
||||
`.replace(/^\t\t/gm, '').trim();
|
||||
|
||||
write_if_changed(`${locations.app()}/manifest/service-worker.js`, code);
|
||||
}
|
||||
|
||||
function generate_client(routes: Route[], path_to_routes: string, dev_port?: number) {
|
||||
let code = `
|
||||
// This file is generated by Sapper — do not edit it!
|
||||
export const routes = [
|
||||
${routes
|
||||
.map(route => {
|
||||
if (route.type !== 'page') {
|
||||
return `{ pattern: ${route.pattern}, ignore: true }`;
|
||||
}
|
||||
|
||||
const file = posixify(`${path_to_routes}/${route.file}`);
|
||||
|
||||
if (route.id === '_4xx' || route.id === '_5xx') {
|
||||
return `{ error: '${route.id.slice(1)}', load: () => import(/* webpackChunkName: "${route.id}" */ '${file}') }`;
|
||||
}
|
||||
|
||||
const params = route.params.length === 0
|
||||
? '{}'
|
||||
: `{ ${route.params.map((part, i) => `${part}: match[${i + 1}]`).join(', ')} }`;
|
||||
|
||||
return `{ pattern: ${route.pattern}, params: ${route.params.length > 0 ? `match` : `()`} => (${params}), load: () => import(/* webpackChunkName: "${route.id}" */ '${file}') }`;
|
||||
})
|
||||
.join(',\n\t')}
|
||||
];`.replace(/^\t\t/gm, '').trim();
|
||||
|
||||
if (dev()) {
|
||||
const sapper_dev_client = posixify(
|
||||
path.resolve(__dirname, '../sapper-dev-client.js')
|
||||
);
|
||||
|
||||
code += `
|
||||
|
||||
if (module.hot) {
|
||||
import('${sapper_dev_client}').then(client => {
|
||||
client.connect(${dev_port});
|
||||
});
|
||||
}`.replace(/^\t{3}/gm, '');
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
function generate_server(routes: Route[], path_to_routes: string) {
|
||||
let code = `
|
||||
// This file is generated by Sapper — do not edit it!
|
||||
${routes
|
||||
.map(route => {
|
||||
const file = posixify(`${path_to_routes}/${route.file}`);
|
||||
return route.type === 'page'
|
||||
? `import ${route.id} from '${file}';`
|
||||
: `import * as ${route.id} from '${file}';`;
|
||||
})
|
||||
.join('\n')}
|
||||
|
||||
export const routes = [
|
||||
${routes
|
||||
.map(route => {
|
||||
const file = posixify(`../../${route.file}`);
|
||||
|
||||
if (route.id === '_4xx' || route.id === '_5xx') {
|
||||
return `{ error: '${route.id.slice(1)}', module: ${route.id} }`;
|
||||
}
|
||||
|
||||
const params = route.params.length === 0
|
||||
? '{}'
|
||||
: `{ ${route.params.map((part, i) => `${part}: match[${i + 1}]`).join(', ')} }`;
|
||||
|
||||
return `{ id: '${route.id}', type: '${route.type}', pattern: ${route.pattern}, params: ${route.params.length > 0 ? `match` : `()`} => (${params}), module: ${route.id} }`;
|
||||
})
|
||||
.join(',\n\t')
|
||||
}
|
||||
];`.replace(/^\t\t/gm, '').trim();
|
||||
|
||||
return code;
|
||||
}
|
||||
129
src/core/create_routes.ts
Normal file
129
src/core/create_routes.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import * as path from 'path';
|
||||
import glob from 'glob';
|
||||
import { locations } from '../config';
|
||||
import { Route } from '../interfaces';
|
||||
|
||||
export default function create_routes({ files } = { files: glob.sync('**/*.*', { cwd: locations.routes(), nodir: true }) }) {
|
||||
const routes: Route[] = files
|
||||
.map((file: string) => {
|
||||
if (/(^|\/|\\)_/.test(file)) return;
|
||||
|
||||
if (/]\[/.test(file)) {
|
||||
throw new Error(`Invalid route ${file} — parameters must be separated`);
|
||||
}
|
||||
|
||||
const base = file.replace(/\.[^/.]+$/, '');
|
||||
const parts = base.split('/'); // glob output is always posix-style
|
||||
if (parts[parts.length - 1] === 'index') parts.pop();
|
||||
|
||||
const id = (
|
||||
parts.join('_').replace(/[[\]]/g, '$').replace(/^\d/, '_$&').replace(/[^a-zA-Z0-9_$]/g, '_')
|
||||
) || '_';
|
||||
|
||||
const params: string[] = [];
|
||||
const param_pattern = /\[([^\]]+)\]/g;
|
||||
let match;
|
||||
while (match = param_pattern.exec(base)) {
|
||||
params.push(match[1]);
|
||||
}
|
||||
|
||||
// TODO can we do all this with sub-parts? or does
|
||||
// nesting make that impossible?
|
||||
let pattern_string = '';
|
||||
let i = parts.length;
|
||||
let nested = true;
|
||||
while (i--) {
|
||||
const part = encodeURIComponent(parts[i].normalize()).replace(/%5B/g, '[').replace(/%5D/g, ']');
|
||||
const dynamic = ~part.indexOf('[');
|
||||
|
||||
if (dynamic) {
|
||||
const matcher = part.replace(param_pattern, `([^\/]+?)`);
|
||||
pattern_string = nested ? `(?:\\/${matcher}${pattern_string})?` : `\\/${matcher}${pattern_string}`;
|
||||
} else {
|
||||
nested = false;
|
||||
pattern_string = `\\/${part}${pattern_string}`;
|
||||
}
|
||||
}
|
||||
|
||||
const pattern = new RegExp(`^${pattern_string}\\/?$`);
|
||||
|
||||
const test = (url: string) => pattern.test(url);
|
||||
|
||||
const exec = (url: string) => {
|
||||
const match = pattern.exec(url);
|
||||
if (!match) return;
|
||||
|
||||
const result: Record<string, string> = {};
|
||||
params.forEach((param, i) => {
|
||||
result[param] = match[i + 1];
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return {
|
||||
id,
|
||||
type: path.extname(file) === '.html' ? 'page' : 'route',
|
||||
file,
|
||||
pattern,
|
||||
test,
|
||||
exec,
|
||||
parts,
|
||||
params
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a: Route, b: Route) => {
|
||||
if (a.file === '4xx.html' || a.file === '5xx.html') return -1;
|
||||
if (b.file === '4xx.html' || b.file === '5xx.html') return 1;
|
||||
|
||||
const max = Math.max(a.parts.length, b.parts.length);
|
||||
|
||||
for (let i = 0; i < max; i += 1) {
|
||||
const a_part = a.parts[i];
|
||||
const b_part = b.parts[i];
|
||||
|
||||
if (!a_part) return -1;
|
||||
if (!b_part) return 1;
|
||||
|
||||
const a_sub_parts = get_sub_parts(a_part);
|
||||
const b_sub_parts = get_sub_parts(b_part);
|
||||
const max = Math.max(a_sub_parts.length, b_sub_parts.length);
|
||||
|
||||
for (let i = 0; i < max; i += 1) {
|
||||
const a_sub_part = a_sub_parts[i];
|
||||
const b_sub_part = b_sub_parts[i];
|
||||
|
||||
if (!a_sub_part) return 1; // b is more specific, so goes first
|
||||
if (!b_sub_part) return -1;
|
||||
|
||||
if (a_sub_part.dynamic !== b_sub_part.dynamic) {
|
||||
return a_sub_part.dynamic ? 1 : -1;
|
||||
}
|
||||
|
||||
if (!a_sub_part.dynamic && a_sub_part.content !== b_sub_part.content) {
|
||||
return (
|
||||
(b_sub_part.content.length - a_sub_part.content.length) ||
|
||||
(a_sub_part.content < b_sub_part.content ? -1 : 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`The ${a.file} and ${b.file} routes clash`);
|
||||
});
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
function get_sub_parts(part: string) {
|
||||
return part.split(/[\[\]]/)
|
||||
.map((content, i) => {
|
||||
if (!content) return null;
|
||||
return {
|
||||
content,
|
||||
dynamic: i % 2 === 1
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
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)
|
||||
);
|
||||
}
|
||||
19
src/interfaces.ts
Normal file
19
src/interfaces.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export type Route = {
|
||||
id: string;
|
||||
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;
|
||||
};
|
||||
418
src/middleware.ts
Normal file
418
src/middleware.ts
Normal file
@@ -0,0 +1,418 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { resolve, URL } from 'url';
|
||||
import { ClientRequest, ServerResponse } from 'http';
|
||||
import cookie from 'cookie';
|
||||
import mkdirp from 'mkdirp';
|
||||
import rimraf from 'rimraf';
|
||||
import devalue from 'devalue';
|
||||
import fetch from 'node-fetch';
|
||||
import { lookup } from './middleware/mime';
|
||||
import { create_routes, create_compilers } from './core';
|
||||
import { locations, dev } from './config';
|
||||
import { Route, Template } from './interfaces';
|
||||
import sourceMapSupport from 'source-map-support';
|
||||
|
||||
sourceMapSupport.install();
|
||||
|
||||
type RouteObject = {
|
||||
id: string;
|
||||
type: 'page' | 'route';
|
||||
pattern: RegExp;
|
||||
params: (match: RegExpMatchArray) => Record<string, string>;
|
||||
module: {
|
||||
render: (data: any, opts: { store: Store }) => {
|
||||
head: string;
|
||||
css: { code: string, map: any };
|
||||
html: string
|
||||
},
|
||||
preload: (data: any) => any | Promise<any>
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
type Handler = (req: Req, res: ServerResponse, next: () => void) => void;
|
||||
|
||||
type Store = {
|
||||
get: () => any
|
||||
};
|
||||
|
||||
interface Req extends ClientRequest {
|
||||
url: string;
|
||||
baseUrl: string;
|
||||
originalUrl: string;
|
||||
method: string;
|
||||
path: string;
|
||||
params: Record<string, string>;
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
|
||||
export default function middleware({ routes, store }: {
|
||||
routes: RouteObject[],
|
||||
store: (req: Req) => Store
|
||||
}) {
|
||||
const output = locations.dest();
|
||||
|
||||
const client_info = JSON.parse(fs.readFileSync(path.join(output, 'client_info.json'), 'utf-8'));
|
||||
|
||||
const middleware = compose_handlers([
|
||||
(req: Req, res: ServerResponse, next: () => void) => {
|
||||
if (req.baseUrl === undefined) {
|
||||
req.baseUrl = req.originalUrl
|
||||
? req.originalUrl.slice(0, -req.url.length)
|
||||
: '';
|
||||
}
|
||||
|
||||
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_route_handler(client_info.assetsByChunkName, routes, 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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const resolved = Promise.resolve();
|
||||
|
||||
function get_route_handler(chunks: Record<string, string>, routes: RouteObject[], store_getter: (req: Req) => Store) {
|
||||
const template = dev()
|
||||
? () => fs.readFileSync(`${locations.app()}/template.html`, 'utf-8')
|
||||
: (str => () => str)(fs.readFileSync(`${locations.dest()}/template.html`, 'utf-8'));
|
||||
|
||||
function handle_route(route: RouteObject, req: Req, res: ServerResponse) {
|
||||
req.params = route.params(route.pattern.exec(req.path));
|
||||
|
||||
const mod = route.module;
|
||||
|
||||
if (route.type === 'page') {
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
|
||||
// preload main.js and current route
|
||||
// TODO detect other stuff we can preload? images, CSS, fonts?
|
||||
const link = []
|
||||
.concat(chunks.main, chunks[route.id])
|
||||
.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;
|
||||
const data = { params: req.params, query: req.query };
|
||||
|
||||
let redirect: { statusCode: number, location: string };
|
||||
let error: { statusCode: number, message: Error | string };
|
||||
|
||||
Promise.resolve(
|
||||
mod.preload ? mod.preload.call({
|
||||
redirect: (statusCode: number, location: string) => {
|
||||
redirect = { statusCode, location };
|
||||
},
|
||||
error: (statusCode: number, message: Error | string) => {
|
||||
error = { statusCode, message };
|
||||
},
|
||||
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
|
||||
}, req) : {}
|
||||
).catch(err => {
|
||||
error = { statusCode: 500, message: err };
|
||||
}).then(preloaded => {
|
||||
if (redirect) {
|
||||
res.statusCode = redirect.statusCode;
|
||||
res.setHeader('Location', `${req.baseUrl}/${redirect.location}`);
|
||||
res.end();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
handle_error(req, res, error.statusCode, error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const serialized = {
|
||||
preloaded: mod.preload && try_serialize(preloaded),
|
||||
store: store && try_serialize(store.get())
|
||||
};
|
||||
Object.assign(data, preloaded);
|
||||
|
||||
const { html, head, css } = mod.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__={${[
|
||||
`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 page = 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.end(page);
|
||||
|
||||
if (process.send) {
|
||||
process.send({
|
||||
__sapper__: true,
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
status: 200,
|
||||
type: 'text/html',
|
||||
body: page
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
else {
|
||||
const method = req.method.toLowerCase();
|
||||
// 'delete' cannot be exported from a module because it is a keyword,
|
||||
// so check for 'del' instead
|
||||
const method_export = method === 'delete' ? 'del' : method;
|
||||
const handler = mod[method_export];
|
||||
if (handler) {
|
||||
if (process.env.SAPPER_EXPORT) {
|
||||
const { write, end, setHeader } = res;
|
||||
const chunks: any[] = [];
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
// intercept data so that it can be exported
|
||||
res.write = function(chunk: any) {
|
||||
chunks.push(new Buffer(chunk));
|
||||
write.apply(res, arguments);
|
||||
};
|
||||
|
||||
res.setHeader = function(name: string, value: string) {
|
||||
headers[name.toLowerCase()] = value;
|
||||
setHeader.apply(res, arguments);
|
||||
};
|
||||
|
||||
res.end = function(chunk?: any) {
|
||||
if (chunk) chunks.push(new Buffer(chunk));
|
||||
end.apply(res, arguments);
|
||||
|
||||
process.send({
|
||||
__sapper__: true,
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
status: res.statusCode,
|
||||
type: headers['content-type'],
|
||||
body: Buffer.concat(chunks).toString()
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const handle_bad_result = (err?: Error) => {
|
||||
if (err) {
|
||||
console.error(err.stack);
|
||||
res.statusCode = 500;
|
||||
res.end(err.message);
|
||||
} else {
|
||||
handle_error(req, res, 404, 'Not found');
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
handler(req, res, handle_bad_result);
|
||||
} catch (err) {
|
||||
handle_bad_result(err);
|
||||
}
|
||||
} else {
|
||||
// no matching handler for method — 404
|
||||
handle_error(req, res, 404, 'Not found');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const not_found_route = routes.find((route: RouteObject) => route.error === '4xx');
|
||||
const error_route = routes.find((route: RouteObject) => route.error === '5xx');
|
||||
|
||||
function handle_error(req: Req, res: ServerResponse, statusCode: number, message: Error | string) {
|
||||
res.statusCode = statusCode;
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
|
||||
const error = message instanceof Error ? message : new Error(message);
|
||||
|
||||
const not_found = statusCode >= 400 && statusCode < 500;
|
||||
|
||||
const route = not_found
|
||||
? not_found_route
|
||||
: error_route;
|
||||
|
||||
const title: string = not_found
|
||||
? 'Not found'
|
||||
: `Internal server error: ${error.message}`;
|
||||
|
||||
const rendered = route ? route.module.render({
|
||||
status: statusCode,
|
||||
error
|
||||
}, {
|
||||
store: store_getter && store_getter(req)
|
||||
}) : { head: '', css: null, html: title };
|
||||
|
||||
const { head, css, html } = rendered;
|
||||
|
||||
const page = template()
|
||||
.replace('%sapper.base%', `<base href="${req.baseUrl}/">`)
|
||||
.replace('%sapper.scripts%', `<script>__SAPPER__={baseUrl: "${req.baseUrl}"}</script><script src='${req.baseUrl}/client/${chunks.main}'></script>`)
|
||||
.replace('%sapper.html%', html)
|
||||
.replace('%sapper.head%', `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`)
|
||||
.replace('%sapper.styles%', (css && css.code ? `<style>${css.code}</style>` : ''));
|
||||
|
||||
res.end(page);
|
||||
}
|
||||
|
||||
return function find_route(req: Req, res: ServerResponse) {
|
||||
try {
|
||||
for (const route of routes) {
|
||||
if (!route.error && route.pattern.test(req.path)) return handle_route(route, req, res);
|
||||
}
|
||||
|
||||
handle_error(req, res, 404, 'Not found');
|
||||
} catch (error) {
|
||||
handle_error(req, res, 500, error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function compose_handlers(handlers: Handler[]) {
|
||||
return (req: Req, res: ServerResponse, next: () => void) => {
|
||||
let i = 0;
|
||||
function go() {
|
||||
const handler = handlers[i];
|
||||
|
||||
if (handler) {
|
||||
handler(req, res, () => {
|
||||
i += 1;
|
||||
go();
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
go();
|
||||
};
|
||||
}
|
||||
|
||||
function read_json(file: string) {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
}
|
||||
|
||||
function try_serialize(data: any) {
|
||||
try {
|
||||
return devalue(data);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
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]);
|
||||
}
|
||||
301
src/runtime/index.ts
Normal file
301
src/runtime/index.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
import { detach, findAnchor, scroll_state, which } from './utils';
|
||||
import { Component, ComponentConstructor, Params, Query, Route, RouteData, ScrollPosition, Store, Target } from './interfaces';
|
||||
|
||||
const manifest = typeof window !== 'undefined' && window.__SAPPER__;
|
||||
|
||||
export let component: Component;
|
||||
let target: Node;
|
||||
let store: Store;
|
||||
let routes: Route[];
|
||||
let errors: { '4xx': Route, '5xx': Route };
|
||||
|
||||
const history = typeof window !== 'undefined' ? window.history : {
|
||||
pushState: (state: any, title: string, href: string) => {},
|
||||
replaceState: (state: any, title: string, href: string) => {},
|
||||
scrollRestoration: ''
|
||||
};
|
||||
|
||||
const scroll_history: Record<string, ScrollPosition> = {};
|
||||
let uid = 1;
|
||||
let cid: number;
|
||||
|
||||
if ('scrollRestoration' in history) {
|
||||
history.scrollRestoration = 'manual';
|
||||
}
|
||||
|
||||
function select_route(url: URL): Target {
|
||||
if (url.origin !== window.location.origin) return null;
|
||||
if (!url.pathname.startsWith(manifest.baseUrl)) return null;
|
||||
|
||||
const pathname = url.pathname.slice(manifest.baseUrl.length);
|
||||
|
||||
for (const route of routes) {
|
||||
const match = route.pattern.exec(pathname);
|
||||
if (match) {
|
||||
if (route.ignore) return null;
|
||||
|
||||
const params = route.params(match);
|
||||
|
||||
const query: Record<string, string | true> = {};
|
||||
if (url.search.length > 0) {
|
||||
url.search.slice(1).split('&').forEach(searchParam => {
|
||||
const [, key, value] = /([^=]+)=(.*)/.exec(searchParam);
|
||||
query[key] = value || true;
|
||||
})
|
||||
}
|
||||
return { url, route, data: { params, query } };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let current_token: {};
|
||||
|
||||
function render(Component: ComponentConstructor, data: any, scroll: ScrollPosition, token: {}) {
|
||||
if (current_token !== token) return;
|
||||
|
||||
if (component) {
|
||||
component.destroy();
|
||||
} else {
|
||||
// first load — remove SSR'd <head> contents
|
||||
const start = document.querySelector('#sapper-head-start');
|
||||
const end = document.querySelector('#sapper-head-end');
|
||||
|
||||
if (start && end) {
|
||||
while (start.nextSibling !== end) detach(start.nextSibling);
|
||||
detach(start);
|
||||
detach(end);
|
||||
}
|
||||
}
|
||||
|
||||
component = new Component({
|
||||
target,
|
||||
data,
|
||||
store,
|
||||
hydrate: !component
|
||||
});
|
||||
|
||||
if (scroll) {
|
||||
window.scrollTo(scroll.x, scroll.y);
|
||||
}
|
||||
}
|
||||
|
||||
function prepare_route(Component: ComponentConstructor, data: RouteData) {
|
||||
let redirect: { statusCode: number, location: string } = null;
|
||||
let error: { statusCode: number, message: Error | string } = null;
|
||||
|
||||
if (!Component.preload) {
|
||||
return { Component, data, redirect, error };
|
||||
}
|
||||
|
||||
if (!component && manifest.preloaded) {
|
||||
return { Component, data: Object.assign(data, manifest.preloaded), redirect, error };
|
||||
}
|
||||
|
||||
return Promise.resolve(Component.preload.call({
|
||||
store,
|
||||
fetch: (url: string, opts?: any) => window.fetch(url, opts),
|
||||
redirect: (statusCode: number, location: string) => {
|
||||
redirect = { statusCode, location };
|
||||
},
|
||||
error: (statusCode: number, message: Error | string) => {
|
||||
error = { statusCode, message };
|
||||
}
|
||||
}, data)).catch(err => {
|
||||
error = { statusCode: 500, message: err };
|
||||
}).then(preloaded => {
|
||||
if (error) {
|
||||
const route = error.statusCode >= 400 && error.statusCode < 500
|
||||
? errors['4xx']
|
||||
: errors['5xx'];
|
||||
|
||||
return route.load().then(({ default: Component }: { default: ComponentConstructor }) => {
|
||||
const err = error.message instanceof Error ? error.message : new Error(error.message);
|
||||
Object.assign(data, { status: error.statusCode, error: err });
|
||||
return { Component, data, redirect: null };
|
||||
});
|
||||
}
|
||||
|
||||
Object.assign(data, preloaded)
|
||||
return { Component, data, redirect };
|
||||
});
|
||||
}
|
||||
|
||||
function navigate(target: Target, id: number) {
|
||||
if (id) {
|
||||
// popstate or initial navigation
|
||||
cid = id;
|
||||
} else {
|
||||
// clicked on a link. preserve scroll state
|
||||
scroll_history[cid] = scroll_state();
|
||||
|
||||
id = cid = ++uid;
|
||||
scroll_history[cid] = { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
cid = id;
|
||||
|
||||
const loaded = prefetching && prefetching.href === target.url.href ?
|
||||
prefetching.promise :
|
||||
target.route.load().then(mod => prepare_route(mod.default, target.data));
|
||||
|
||||
prefetching = null;
|
||||
|
||||
const token = current_token = {};
|
||||
|
||||
return loaded.then(({ Component, data, redirect }) => {
|
||||
if (redirect) {
|
||||
return goto(redirect.location, { replaceState: true });
|
||||
}
|
||||
|
||||
render(Component, data, scroll_history[id], token);
|
||||
});
|
||||
}
|
||||
|
||||
function handle_click(event: MouseEvent) {
|
||||
// Adapted from https://github.com/visionmedia/page.js
|
||||
// MIT license https://github.com/visionmedia/page.js#license
|
||||
if (which(event) !== 1) return;
|
||||
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
|
||||
if (event.defaultPrevented) return;
|
||||
|
||||
const a: HTMLAnchorElement | SVGAElement = <HTMLAnchorElement | SVGAElement>findAnchor(<Node>event.target);
|
||||
if (!a) return;
|
||||
|
||||
// check if link is inside an svg
|
||||
// in this case, both href and target are always inside an object
|
||||
const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
|
||||
const href = String(svg ? (<SVGAElement>a).href.baseVal : a.href);
|
||||
|
||||
if (href === window.location.href) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore if tag has
|
||||
// 1. 'download' attribute
|
||||
// 2. rel='external' attribute
|
||||
if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
|
||||
|
||||
// Ignore if <a> has a target
|
||||
if (svg ? (<SVGAElement>a).target.baseVal : a.target) return;
|
||||
|
||||
const url = new URL(href);
|
||||
|
||||
// Don't handle hash changes
|
||||
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
|
||||
|
||||
const target = select_route(url);
|
||||
if (target) {
|
||||
navigate(target, null);
|
||||
event.preventDefault();
|
||||
history.pushState({ id: cid }, '', url.href);
|
||||
}
|
||||
}
|
||||
|
||||
function handle_popstate(event: PopStateEvent) {
|
||||
scroll_history[cid] = scroll_state();
|
||||
|
||||
if (event.state) {
|
||||
const url = new URL(window.location.href);
|
||||
const target = select_route(url);
|
||||
navigate(target, event.state.id);
|
||||
} else {
|
||||
// hashchange
|
||||
cid = ++uid;
|
||||
history.replaceState({ id: cid }, '', window.location.href);
|
||||
}
|
||||
}
|
||||
|
||||
let prefetching: {
|
||||
href: string;
|
||||
promise: Promise<{ Component: ComponentConstructor, data: any }>;
|
||||
} = null;
|
||||
|
||||
export function prefetch(href: string) {
|
||||
const selected = select_route(new URL(href, document.baseURI));
|
||||
|
||||
if (selected) {
|
||||
prefetching = {
|
||||
href,
|
||||
promise: selected.route.load().then(mod => prepare_route(mod.default, selected.data))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function handle_touchstart_mouseover(event: MouseEvent | TouchEvent) {
|
||||
const a: HTMLAnchorElement = <HTMLAnchorElement>findAnchor(<Node>event.target);
|
||||
if (!a || a.rel !== 'prefetch') return;
|
||||
|
||||
prefetch(a.href);
|
||||
}
|
||||
|
||||
let inited: boolean;
|
||||
|
||||
export function init(_target: Node, _routes: Route[], opts?: { store?: (data: any) => Store }) {
|
||||
target = _target;
|
||||
routes = _routes.filter(r => !r.error);
|
||||
errors = {
|
||||
'4xx': _routes.find(r => r.error === '4xx'),
|
||||
'5xx': _routes.find(r => r.error === '5xx')
|
||||
};
|
||||
|
||||
if (opts && opts.store) {
|
||||
store = opts.store(manifest.store);
|
||||
}
|
||||
|
||||
if (!inited) { // this check makes HMR possible
|
||||
window.addEventListener('click', handle_click);
|
||||
window.addEventListener('popstate', handle_popstate);
|
||||
|
||||
// prefetch
|
||||
window.addEventListener('touchstart', handle_touchstart_mouseover);
|
||||
window.addEventListener('mouseover', handle_touchstart_mouseover);
|
||||
|
||||
inited = true;
|
||||
}
|
||||
|
||||
return Promise.resolve().then(() => {
|
||||
const { hash, href } = window.location;
|
||||
|
||||
const deep_linked = hash && document.getElementById(hash.slice(1));
|
||||
scroll_history[uid] = deep_linked ?
|
||||
{ x: 0, y: deep_linked.getBoundingClientRect().top } :
|
||||
scroll_state();
|
||||
|
||||
history.replaceState({ id: uid }, '', href);
|
||||
|
||||
const target = select_route(new URL(window.location.href));
|
||||
return navigate(target, uid);
|
||||
});
|
||||
}
|
||||
|
||||
export function goto(href: string, opts = { replaceState: false }) {
|
||||
const target = select_route(new URL(href, document.baseURI));
|
||||
|
||||
if (target) {
|
||||
navigate(target, null);
|
||||
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
|
||||
} else {
|
||||
window.location.href = href;
|
||||
}
|
||||
}
|
||||
|
||||
export function prefetchRoutes(pathnames: string[]) {
|
||||
if (!routes) throw new Error(`You must call init() first`);
|
||||
|
||||
return routes
|
||||
.filter(route => {
|
||||
if (!pathnames) return true;
|
||||
return pathnames.some(pathname => {
|
||||
return route.error
|
||||
? route.error === pathname
|
||||
: route.pattern.test(pathname)
|
||||
});
|
||||
})
|
||||
.reduce((promise: Promise<any>, route) => {
|
||||
return promise.then(route.load);
|
||||
}, Promise.resolve());
|
||||
}
|
||||
|
||||
// remove this in 0.9
|
||||
export { prefetchRoutes as preloadRoutes };
|
||||
34
src/runtime/interfaces.ts
Normal file
34
src/runtime/interfaces.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
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 };
|
||||
|
||||
export interface ComponentConstructor {
|
||||
new (options: { target: Node, data: any, store: Store, hydrate: boolean }): Component;
|
||||
preload: (data: { params: Params, query: Query }) => Promise<any>;
|
||||
};
|
||||
|
||||
export interface Component {
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
export type Route = {
|
||||
pattern: RegExp;
|
||||
load: () => Promise<{ default: ComponentConstructor }>;
|
||||
error?: string;
|
||||
params?: (match: RegExpExecArray) => Record<string, string>;
|
||||
ignore?: boolean;
|
||||
};
|
||||
|
||||
export type ScrollPosition = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type Target = {
|
||||
url: URL;
|
||||
route: Route;
|
||||
data: RouteData;
|
||||
};
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
2
test/app/.gitignore
vendored
2
test/app/.gitignore
vendored
@@ -3,3 +3,5 @@ node_modules
|
||||
.sapper
|
||||
yarn.lock
|
||||
cypress/screenshots
|
||||
templates/.*
|
||||
dist
|
||||
|
||||
11
test/app/app/client.js
Normal file
11
test/app/app/client.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { init, prefetchRoutes } from '../../../runtime.js';
|
||||
import { Store } from 'svelte/store.js';
|
||||
import { routes } from './manifest/client.js';
|
||||
|
||||
window.init = () => {
|
||||
return init(document.querySelector('#sapper'), routes, {
|
||||
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 { routes } from './manifest/server.js';
|
||||
|
||||
let pending;
|
||||
let ended;
|
||||
|
||||
process.on('message', message => {
|
||||
if (message.action === 'start') {
|
||||
if (pending) {
|
||||
throw new Error(`Already capturing`);
|
||||
}
|
||||
|
||||
pending = new Set();
|
||||
ended = false;
|
||||
process.send({ type: 'ready' });
|
||||
}
|
||||
|
||||
if (message.action === 'end') {
|
||||
ended = true;
|
||||
if (pending.size === 0) {
|
||||
process.send({ type: 'done' });
|
||||
pending = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const app = 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({
|
||||
routes,
|
||||
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"
|
||||
}
|
||||
}
|
||||
6
test/app/routes/4xx.html
Normal file
6
test/app/routes/4xx.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<:Head>
|
||||
<title>{{status}}</title>
|
||||
</:Head>
|
||||
|
||||
<h1>Not found</h1>
|
||||
<p>{{error.message}}</p>
|
||||
6
test/app/routes/5xx.html
Normal file
6
test/app/routes/5xx.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<:Head>
|
||||
<title>Internal server error</title>
|
||||
</:Head>
|
||||
|
||||
<h1>Internal server error</h1>
|
||||
<p>{{error.message}}</p>
|
||||
@@ -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>
|
||||
@@ -2,18 +2,20 @@
|
||||
<title>About</title>
|
||||
</: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>
|
||||
</Layout>
|
||||
<p>This is the 'about' page. There's not much here.</p>
|
||||
|
||||
<button class='goto' on:click='goto("blog/what-is-sapper")'>What is Sapper?</button>
|
||||
<button class='prefetch' on:click='prefetch("blog/why-the-name")'>Why the name?</button>
|
||||
|
||||
<script>
|
||||
import Layout from './_components/Layout.html';
|
||||
import { goto, prefetch } from '../../../runtime.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Layout
|
||||
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,4 +1,4 @@
|
||||
import posts from './_posts.js';
|
||||
import posts from './blog/_posts.js';
|
||||
|
||||
const contents = JSON.stringify(posts.map(post => {
|
||||
return {
|
||||
@@ -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
|
||||
});
|
||||
@@ -2,65 +2,34 @@
|
||||
<title>{{post.title}}</title>
|
||||
</: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'>
|
||||
{{{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>
|
||||
`
|
||||
},
|
||||
@@ -82,6 +82,30 @@ const posts = [
|
||||
html: `
|
||||
<p>We're so glad you asked! Come on over to the <a href='https://github.com/sveltejs/svelte'>Svelte</a> and <a href='https://github.com/sveltejs/sapper'>Sapper</a> repos, and join us in the <a href='https://gitter.im/sveltejs/svelte'>Gitter chatroom</a>. Everyone is welcome, especially you!</p>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
title: 'A very long post with deep links',
|
||||
slug: 'a-very-long-post',
|
||||
html: `
|
||||
<h2 id='one'>One</h2>
|
||||
<p>I'll have a vodka rocks. (Mom, it's breakfast time.) And a piece of toast. Let me out that Queen. Fried cheese… with club sauce.</p>
|
||||
<p>Her lawyers are claiming the seal is worth $250,000. And that's not even including Buster's Swatch. This was a big get for God. What, so the guy we are meeting with can't even grow his own hair? COME ON! She's always got to wedge herself in the middle of us so that she can control everything. Yeah. Mom's awesome. It's, like, Hey, you want to go down to the whirlpool? Yeah, I don't have a husband. I call it Swing City. The CIA should've just Googled for his hideout, evidently. There are dozens of us! DOZENS! Yeah, like I'm going to take a whiz through this $5,000 suit. COME ON.</p>
|
||||
|
||||
<h2 id='two'>Two</h2>
|
||||
<p>Tobias Fünke costume. Heart attack never stopped old big bear.</p>
|
||||
<p>Nellie is blowing them all AWAY. I will be a bigger and hairier mole than the one on your inner left thigh! I'll sacrifice anything for my children.</p>
|
||||
<p>Up yours, granny! You couldn't handle it! Hey, Dad. Look at you. You're a year older…and a year closer to death. Buster: Oh yeah, I guess that's kind of funny. Bob Loblaw Law Blog. The guy runs a prison, he can have any piece of ass he wants.</p>
|
||||
|
||||
<h2 id='three'>Three</h2>
|
||||
<p>I prematurely shot my wad on what was supposed to be a dry run, so now I'm afraid I have something of a mess on my hands. Dead Dove DO NOT EAT. Never once touched my per diem. I'd go to Craft Service, get some raw veggies, bacon, Cup-A-Soup…baby, I got a stew goin'. You're losing blood, aren't you? Gob: Probably, my socks are wet. Sure, let the little fruit do it. HUZZAH! Although George Michael had only got to second base, he'd gone in head first, like Pete Rose. I will pack your sweet pink mouth with so much ice cream you'll be the envy of every Jerry and Jane on the block!</p>
|
||||
<p>Gosh Mom… after all these years, God's not going to take a call from you. Come on, this is a Bluth family celebration. It's no place for children.</p>
|
||||
<p>And I wouldn't just lie there, if that's what you're thinking. That's not what I WAS thinking. Who? i just dont want him to point out my cracker ass in front of ann. When a man needs to prove to a woman that he's actually… When a man loves a woman… Heyyyyyy Uncle Father Oscar. [Stabbing Gob] White power! Gob: I'm white! Let me take off my assistant's skirt and put on my Barbra-Streisand-in-The-Prince-of-Tides ass-masking therapist pantsuit. In the mid '90s, Tobias formed a folk music band with Lindsay and Maebe which he called Dr. Funke's 100 Percent Natural Good Time Family Band Solution. The group was underwritten by the Natural Food Life Company, a division of Chem-Grow, an Allen Crayne acqusition, which was part of the Squimm Group. Their motto was simple: We keep you alive.</p>
|
||||
|
||||
<h2 id='four'>Four</h2>
|
||||
<p>If you didn't have adult onset diabetes, I wouldn't mind giving you a little sugar. Everybody dance NOW. And the soup of the day is bread. Great, now I'm gonna smell to high heaven like a tuna melt!</p>
|
||||
<p>That's how Tony Wonder lost a nut. She calls it a Mayonegg. Go ahead, touch the Cornballer. There's a new daddy in town. A discipline daddy.</p>
|
||||
`
|
||||
}
|
||||
];
|
||||
|
||||
@@ -2,37 +2,22 @@
|
||||
<title>Blog</title>
|
||||
</: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 };
|
||||
});
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -2,57 +2,25 @@
|
||||
<title>Sapper project template</title>
|
||||
</: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>
|
||||
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>
|
||||
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>
|
||||
9
test/app/routes/show-url.html
Normal file
9
test/app/routes/show-url.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<p>URL is {{url}}</p>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload({ url }) {
|
||||
if (url) return { url };
|
||||
}
|
||||
};
|
||||
</script>
|
||||
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');
|
||||
}
|
||||
@@ -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,10 +0,0 @@
|
||||
import { init } from '__app__';
|
||||
|
||||
// `routes` is an array of route objects injected by Sapper
|
||||
init(document.querySelector('#sapper'), __routes__);
|
||||
|
||||
// if (__dev__) {
|
||||
// // Enable hot-module reloading
|
||||
// import('sapper/webpack/hmr');
|
||||
// if (module.hot) module.hot.accept();
|
||||
// }
|
||||
@@ -1,50 +0,0 @@
|
||||
const config = require('../../webpack/config.js');
|
||||
const webpack = require('webpack');
|
||||
const ExtractTextPlugin = require("extract-text-webpack-plugin");
|
||||
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
entry: config.client.entry(),
|
||||
output: config.client.output(),
|
||||
resolve: {
|
||||
extensions: ['.js', '.html']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.html$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'svelte-loader',
|
||||
options: {
|
||||
hydratable: true,
|
||||
emitCss: !config.dev,
|
||||
cascade: false,
|
||||
store: true
|
||||
}
|
||||
}
|
||||
},
|
||||
config.dev && {
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
{ loader: "style-loader" },
|
||||
{ loader: "css-loader" }
|
||||
]
|
||||
},
|
||||
!config.dev && {
|
||||
test: /\.css$/,
|
||||
use: ExtractTextPlugin.extract({
|
||||
fallback: 'style-loader',
|
||||
use: [{ loader: 'css-loader', options: { sourceMap: config.dev } }]
|
||||
})
|
||||
}
|
||||
].filter(Boolean)
|
||||
},
|
||||
plugins: [
|
||||
config.dev && new webpack.HotModuleReplacementPlugin(),
|
||||
!config.dev && new ExtractTextPlugin('main.css'),
|
||||
!config.dev && new webpack.optimize.ModuleConcatenationPlugin(),
|
||||
!config.dev && new UglifyJSPlugin()
|
||||
].filter(Boolean),
|
||||
devtool: config.dev ? 'inline-source-map' : false
|
||||
};
|
||||
34
test/app/webpack/client.config.js
Normal file
34
test/app/webpack/client.config.js
Normal file
@@ -0,0 +1,34 @@
|
||||
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(),
|
||||
resolve: {
|
||||
extensions: ['.js', '.html']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.html$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'svelte-loader',
|
||||
options: {
|
||||
hydratable: true,
|
||||
cascade: false,
|
||||
store: true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
mode,
|
||||
plugins: [
|
||||
isDev && new webpack.HotModuleReplacementPlugin()
|
||||
].filter(Boolean),
|
||||
devtool: isDev && 'inline-source-map'
|
||||
};
|
||||
@@ -1,7 +1,5 @@
|
||||
const config = require('../../webpack/config.js');
|
||||
const webpack = require('webpack');
|
||||
const ExtractTextPlugin = require("extract-text-webpack-plugin");
|
||||
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
|
||||
const config = require('../../../webpack/config.js');
|
||||
const sapper_pkg = require('../../../package.json');
|
||||
|
||||
module.exports = {
|
||||
entry: config.server.entry(),
|
||||
@@ -10,6 +8,10 @@ module.exports = {
|
||||
resolve: {
|
||||
extensions: ['.js', '.html']
|
||||
},
|
||||
externals: [].concat(
|
||||
Object.keys(sapper_pkg.dependencies),
|
||||
Object.keys(sapper_pkg.devDependencies)
|
||||
),
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
@@ -26,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
|
||||
};
|
||||
615
test/common/test.js
Normal file
615
test/common/test.js
Normal file
@@ -0,0 +1,615 @@
|
||||
const path = require('path');
|
||||
const assert = require('assert');
|
||||
const Nightmare = require('nightmare');
|
||||
const serve = require('serve-static');
|
||||
const walkSync = require('walk-sync');
|
||||
const fetch = require('node-fetch');
|
||||
const rimraf = require('rimraf');
|
||||
const ports = require('port-authority');
|
||||
|
||||
Nightmare.action('page', {
|
||||
title(done) {
|
||||
this.evaluate_now(() => document.querySelector('h1').textContent, done);
|
||||
},
|
||||
|
||||
html(done) {
|
||||
this.evaluate_now(() => document.documentElement.innerHTML, done);
|
||||
},
|
||||
|
||||
text(done) {
|
||||
this.evaluate_now(() => document.body.textContent, done);
|
||||
}
|
||||
});
|
||||
|
||||
Nightmare.action('init', function(done) {
|
||||
this.evaluate_now(() => window.init(), done);
|
||||
});
|
||||
|
||||
Nightmare.action('prefetchRoutes', function(done) {
|
||||
this.evaluate_now(() => window.prefetchRoutes(), done);
|
||||
});
|
||||
|
||||
const cli = path.resolve(__dirname, '../../sapper');
|
||||
|
||||
describe('sapper', function() {
|
||||
process.chdir(path.resolve(__dirname, '../app'));
|
||||
|
||||
// clean up after previous test runs
|
||||
rimraf.sync('export');
|
||||
rimraf.sync('build');
|
||||
rimraf.sync('.sapper');
|
||||
|
||||
this.timeout(process.env.CI ? 30000 : 10000);
|
||||
|
||||
// TODO reinstate dev tests
|
||||
// run({
|
||||
// mode: 'development'
|
||||
// });
|
||||
|
||||
run({
|
||||
mode: 'production'
|
||||
});
|
||||
|
||||
run({
|
||||
mode: 'production',
|
||||
basepath: '/custom-basepath'
|
||||
});
|
||||
|
||||
describe('export', () => {
|
||||
before(() => {
|
||||
return exec(`node ${cli} export`);
|
||||
});
|
||||
|
||||
it('export all pages', () => {
|
||||
const dest = path.resolve(__dirname, '../app/export');
|
||||
|
||||
// Pages that should show up in the extraction directory.
|
||||
const expectedPages = [
|
||||
'index.html',
|
||||
'about/index.html',
|
||||
'slow-preload/index.html',
|
||||
|
||||
'blog/index.html',
|
||||
'blog/a-very-long-post/index.html',
|
||||
'blog/how-can-i-get-involved/index.html',
|
||||
'blog/how-is-sapper-different-from-next/index.html',
|
||||
'blog/how-to-use-sapper/index.html',
|
||||
'blog/what-is-sapper/index.html',
|
||||
'blog/why-the-name/index.html',
|
||||
|
||||
'blog.json',
|
||||
'blog/a-very-long-post.json',
|
||||
'blog/how-can-i-get-involved.json',
|
||||
'blog/how-is-sapper-different-from-next.json',
|
||||
'blog/how-to-use-sapper.json',
|
||||
'blog/what-is-sapper.json',
|
||||
'blog/why-the-name.json',
|
||||
|
||||
'favicon.png',
|
||||
'global.css',
|
||||
'great-success.png',
|
||||
'manifest.json',
|
||||
'service-worker.js',
|
||||
'svelte-logo-192.png',
|
||||
'svelte-logo-512.png',
|
||||
];
|
||||
// Client scripts that should show up in the extraction directory.
|
||||
const expectedClientRegexes = [
|
||||
/client\/[^/]+\/_(\.\d+)?\.js/,
|
||||
/client\/[^/]+\/about(\.\d+)?\.js/,
|
||||
/client\/[^/]+\/blog_\$slug\$(\.\d+)?\.js/,
|
||||
/client\/[^/]+\/blog(\.\d+)?\.js/,
|
||||
/client\/[^/]+\/main(\.\d+)?\.js/,
|
||||
/client\/[^/]+\/show_url(\.\d+)?\.js/,
|
||||
/client\/[^/]+\/slow_preload(\.\d+)?\.js/,
|
||||
];
|
||||
const allPages = walkSync(dest);
|
||||
|
||||
expectedPages.forEach((expectedPage) => {
|
||||
assert.ok(allPages.includes(expectedPage),`Could not find page matching ${expectedPage}`);
|
||||
});
|
||||
|
||||
expectedClientRegexes.forEach((expectedRegex) => {
|
||||
// Ensure each client page regular expression matches at least one
|
||||
// generated page.
|
||||
let matched = false;
|
||||
for (const page of allPages) {
|
||||
if (expectedRegex.test(page)) {
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert.ok(matched, `Could not find client page matching ${expectedRegex}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function run({ mode, basepath = '' }) {
|
||||
describe(`mode=${mode}`, function () {
|
||||
let proc;
|
||||
let capture;
|
||||
|
||||
let base;
|
||||
|
||||
const nightmare = new Nightmare();
|
||||
|
||||
nightmare.on('console', (type, ...args) => {
|
||||
console[type](...args);
|
||||
});
|
||||
|
||||
nightmare.on('page', (type, ...args) => {
|
||||
if (type === 'error') {
|
||||
console.error(args[1]);
|
||||
} else {
|
||||
console.warn(type, args);
|
||||
}
|
||||
});
|
||||
|
||||
before(() => {
|
||||
const promise = mode === 'production'
|
||||
? exec(`node ${cli} build`).then(() => ports.find(3000))
|
||||
: ports.find(3000).then(port => {
|
||||
exec(`node ${cli} dev`);
|
||||
return ports.wait(port).then(() => port);
|
||||
});
|
||||
|
||||
return promise.then(port => {
|
||||
base = `http://localhost:${port}`;
|
||||
if (basepath) base += basepath;
|
||||
|
||||
const dir = mode === 'production' ? 'build' : '.sapper';
|
||||
|
||||
proc = require('child_process').fork(`${dir}/server.js`, {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
NODE_ENV: mode,
|
||||
BASEPATH: basepath,
|
||||
SAPPER_DEST: dir,
|
||||
PORT: port
|
||||
}
|
||||
});
|
||||
|
||||
let handler;
|
||||
|
||||
proc.on('message', message => {
|
||||
if (message.__sapper__) return;
|
||||
if (handler) handler(message);
|
||||
});
|
||||
|
||||
capture = fn => {
|
||||
return new Promise((fulfil, reject) => {
|
||||
const captured = [];
|
||||
|
||||
let start = Date.now();
|
||||
|
||||
handler = message => {
|
||||
if (message.type === 'ready') {
|
||||
fn().then(() => {
|
||||
proc.send({
|
||||
action: 'end'
|
||||
});
|
||||
}, reject);
|
||||
}
|
||||
|
||||
else if (message.type === 'done') {
|
||||
fulfil(captured);
|
||||
handler = null;
|
||||
}
|
||||
|
||||
else {
|
||||
captured.push(message);
|
||||
}
|
||||
};
|
||||
|
||||
proc.send({
|
||||
action: 'start'
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
// give a chance to clean up
|
||||
return Promise.all([
|
||||
nightmare.end(),
|
||||
new Promise(fulfil => {
|
||||
proc.on('exit', fulfil);
|
||||
proc.kill();
|
||||
})
|
||||
]);
|
||||
});
|
||||
|
||||
describe('basic functionality', () => {
|
||||
it('serves /', () => {
|
||||
return nightmare.goto(base).page.title().then(title => {
|
||||
assert.equal(title, 'Great success!');
|
||||
});
|
||||
});
|
||||
|
||||
it('serves /?', () => {
|
||||
return nightmare.goto(`${base}?`).page.title().then(title => {
|
||||
assert.equal(title, 'Great success!');
|
||||
});
|
||||
});
|
||||
|
||||
it('serves static route', () => {
|
||||
return nightmare.goto(`${base}/about`).page.title().then(title => {
|
||||
assert.equal(title, 'About this site');
|
||||
});
|
||||
});
|
||||
|
||||
it('serves dynamic route', () => {
|
||||
return nightmare.goto(`${base}/blog/what-is-sapper`).page.title().then(title => {
|
||||
assert.equal(title, 'What is Sapper?');
|
||||
});
|
||||
});
|
||||
|
||||
it('navigates to a new page without reloading', () => {
|
||||
return nightmare.goto(base).init().prefetchRoutes()
|
||||
.then(() => {
|
||||
return capture(() => nightmare.click('a[href="about"]'));
|
||||
})
|
||||
.then(requests => {
|
||||
assert.deepEqual(requests.map(r => r.url), []);
|
||||
return nightmare.path();
|
||||
})
|
||||
.then(path => {
|
||||
assert.equal(path, `${basepath}/about`);
|
||||
return nightmare.title();
|
||||
})
|
||||
.then(title => {
|
||||
assert.equal(title, 'About');
|
||||
});
|
||||
});
|
||||
|
||||
it('navigates programmatically', () => {
|
||||
return nightmare
|
||||
.goto(`${base}/about`)
|
||||
.init()
|
||||
.click('.goto')
|
||||
.wait(url => window.location.pathname === url, `${basepath}/blog/what-is-sapper`)
|
||||
.wait(100)
|
||||
.title()
|
||||
.then(title => {
|
||||
assert.equal(title, 'What is Sapper?');
|
||||
});
|
||||
});
|
||||
|
||||
it('prefetches programmatically', () => {
|
||||
return capture(() => nightmare.goto(`${base}/about`).init())
|
||||
.then(() => {
|
||||
return capture(() => {
|
||||
return nightmare
|
||||
.click('.prefetch')
|
||||
.wait(200);
|
||||
});
|
||||
})
|
||||
.then(requests => {
|
||||
assert.ok(!!requests.find(r => r.url === `/blog/why-the-name.json`));
|
||||
});
|
||||
});
|
||||
|
||||
it('scrolls to active deeplink', () => {
|
||||
return nightmare
|
||||
.goto(`${base}/blog/a-very-long-post#four`)
|
||||
.init()
|
||||
.evaluate(() => window.scrollY)
|
||||
.then(scrollY => {
|
||||
assert.ok(scrollY > 0, scrollY);
|
||||
});
|
||||
});
|
||||
|
||||
it('reuses prefetch promise', () => {
|
||||
return nightmare
|
||||
.goto(`${base}/blog`)
|
||||
.init()
|
||||
.then(() => {
|
||||
return capture(() => {
|
||||
return nightmare
|
||||
.mouseover('[href="blog/what-is-sapper"]')
|
||||
.wait(200);
|
||||
});
|
||||
})
|
||||
.then(mouseover_requests => {
|
||||
assert.ok(mouseover_requests.findIndex(r => r.url === `/blog/what-is-sapper.json`) !== -1);
|
||||
|
||||
return capture(() => {
|
||||
return nightmare
|
||||
.click('[href="blog/what-is-sapper"]')
|
||||
.wait(200);
|
||||
});
|
||||
})
|
||||
.then(click_requests => {
|
||||
assert.ok(click_requests.findIndex(r => r.url === `/blog/what-is-sapper.json`) === -1);
|
||||
});
|
||||
});
|
||||
|
||||
it('cancels navigation if subsequent navigation occurs during preload', () => {
|
||||
return nightmare
|
||||
.goto(base)
|
||||
.init()
|
||||
.click('a[href="slow-preload"]')
|
||||
.wait(100)
|
||||
.click('a[href="about"]')
|
||||
.wait(100)
|
||||
.then(() => nightmare.path())
|
||||
.then(path => {
|
||||
assert.equal(path, `${basepath}/about`);
|
||||
return nightmare.title();
|
||||
})
|
||||
.then(title => {
|
||||
assert.equal(title, 'About');
|
||||
return nightmare.evaluate(() => window.fulfil({})).wait(100);
|
||||
})
|
||||
.then(() => nightmare.path())
|
||||
.then(path => {
|
||||
assert.equal(path, `${basepath}/about`);
|
||||
return nightmare.title();
|
||||
})
|
||||
.then(title => {
|
||||
assert.equal(title, 'About');
|
||||
});
|
||||
});
|
||||
|
||||
it('passes entire request object to preload', () => {
|
||||
return nightmare
|
||||
.goto(`${base}/show-url`)
|
||||
.init()
|
||||
.evaluate(() => document.querySelector('p').innerHTML)
|
||||
.then(html => {
|
||||
assert.equal(html, `URL is /show-url`);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls a delete handler', () => {
|
||||
return nightmare
|
||||
.goto(`${base}/delete-test`)
|
||||
.init()
|
||||
.click('.del')
|
||||
.wait(() => window.deleted)
|
||||
.evaluate(() => window.deleted.id)
|
||||
.then(id => {
|
||||
assert.equal(id, 42);
|
||||
});
|
||||
});
|
||||
|
||||
it('hydrates initial route', () => {
|
||||
return nightmare.goto(base)
|
||||
.wait('.hydrate-test')
|
||||
.evaluate(() => {
|
||||
window.el = document.querySelector('.hydrate-test');
|
||||
})
|
||||
.init()
|
||||
.evaluate(() => {
|
||||
return document.querySelector('.hydrate-test') === window.el;
|
||||
})
|
||||
.then(matches => {
|
||||
assert.ok(matches);
|
||||
});
|
||||
});
|
||||
|
||||
it('redirects on server', () => {
|
||||
return nightmare.goto(`${base}/redirect-from`)
|
||||
.path()
|
||||
.then(path => {
|
||||
assert.equal(path, `${basepath}/redirect-to`);
|
||||
})
|
||||
.then(() => nightmare.page.title())
|
||||
.then(title => {
|
||||
assert.equal(title, 'redirected');
|
||||
});
|
||||
});
|
||||
|
||||
it('redirects in client', () => {
|
||||
return nightmare.goto(base)
|
||||
.wait('[href="redirect-from"]')
|
||||
.click('[href="redirect-from"]')
|
||||
.wait(200)
|
||||
.path()
|
||||
.then(path => {
|
||||
assert.equal(path, `${basepath}/redirect-to`);
|
||||
})
|
||||
.then(() => nightmare.page.title())
|
||||
.then(title => {
|
||||
assert.equal(title, 'redirected');
|
||||
});
|
||||
});
|
||||
|
||||
it('handles 4xx error on server', () => {
|
||||
return nightmare.goto(`${base}/blog/nope`)
|
||||
.path()
|
||||
.then(path => {
|
||||
assert.equal(path, `${basepath}/blog/nope`);
|
||||
})
|
||||
.then(() => nightmare.page.title())
|
||||
.then(title => {
|
||||
assert.equal(title, 'Not found')
|
||||
});
|
||||
});
|
||||
|
||||
it('handles 4xx error in client', () => {
|
||||
return nightmare.goto(base)
|
||||
.init()
|
||||
.click('[href="blog/nope"]')
|
||||
.wait(200)
|
||||
.path()
|
||||
.then(path => {
|
||||
assert.equal(path, `${basepath}/blog/nope`);
|
||||
})
|
||||
.then(() => nightmare.page.title())
|
||||
.then(title => {
|
||||
assert.equal(title, 'Not found');
|
||||
});
|
||||
});
|
||||
|
||||
it('handles non-4xx error on server', () => {
|
||||
return nightmare.goto(`${base}/blog/throw-an-error`)
|
||||
.path()
|
||||
.then(path => {
|
||||
assert.equal(path, `${basepath}/blog/throw-an-error`);
|
||||
})
|
||||
.then(() => nightmare.page.title())
|
||||
.then(title => {
|
||||
assert.equal(title, 'Internal server error')
|
||||
});
|
||||
});
|
||||
|
||||
it('handles non-4xx error in client', () => {
|
||||
return nightmare.goto(base)
|
||||
.init()
|
||||
.click('[href="blog/throw-an-error"]')
|
||||
.wait(200)
|
||||
.path()
|
||||
.then(path => {
|
||||
assert.equal(path, `${basepath}/blog/throw-an-error`);
|
||||
})
|
||||
.then(() => nightmare.page.title())
|
||||
.then(title => {
|
||||
assert.equal(title, 'Internal server error');
|
||||
});
|
||||
});
|
||||
|
||||
it('does not attempt client-side navigation to server routes', () => {
|
||||
return nightmare.goto(`${base}/blog/how-is-sapper-different-from-next`)
|
||||
.init()
|
||||
.click(`[href="blog/how-is-sapper-different-from-next.json"]`)
|
||||
.wait(200)
|
||||
.page.text()
|
||||
.then(text => {
|
||||
JSON.parse(text);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not serve error page for non-page errors', () => {
|
||||
return nightmare.goto(`${base}/throw-an-error`)
|
||||
.page.text()
|
||||
.then(text => {
|
||||
assert.equal(text, 'nope');
|
||||
});
|
||||
});
|
||||
|
||||
it('encodes routes', () => {
|
||||
return nightmare.goto(`${base}/fünke`)
|
||||
.page.title()
|
||||
.then(title => {
|
||||
assert.equal(title, `I'm afraid I just blue myself`);
|
||||
});
|
||||
});
|
||||
|
||||
it('serializes Set objects returned from preload', () => {
|
||||
return nightmare.goto(`${base}/preload-values/set`)
|
||||
.page.title()
|
||||
.then(title => {
|
||||
assert.equal(title, 'true');
|
||||
return nightmare.init().page.title();
|
||||
})
|
||||
.then(title => {
|
||||
assert.equal(title, 'true');
|
||||
});
|
||||
});
|
||||
|
||||
it('bails on custom classes returned from preload', () => {
|
||||
return nightmare.goto(`${base}/preload-values/custom-class`)
|
||||
.page.title()
|
||||
.then(title => {
|
||||
assert.equal(title, '42');
|
||||
return nightmare.init().page.title();
|
||||
})
|
||||
.then(title => {
|
||||
assert.equal(title, '42');
|
||||
});
|
||||
});
|
||||
|
||||
it('renders store props', () => {
|
||||
return nightmare.goto(`${base}/store`)
|
||||
.page.title()
|
||||
.then(title => {
|
||||
assert.equal(title, 'Stored title');
|
||||
return nightmare.init().page.title();
|
||||
})
|
||||
.then(title => {
|
||||
assert.equal(title, 'Stored title');
|
||||
});
|
||||
});
|
||||
|
||||
it('sends cookies when using this.fetch with credentials: "include"', () => {
|
||||
return nightmare.goto(`${base}/credentials?creds=include`)
|
||||
.page.title()
|
||||
.then(title => {
|
||||
assert.equal(title, 'woohoo!');
|
||||
});
|
||||
});
|
||||
|
||||
it('does not send cookies when using this.fetch without credentials', () => {
|
||||
return nightmare.goto(`${base}/credentials`)
|
||||
.page.title()
|
||||
.then(title => {
|
||||
assert.equal(title, 'unauthorized');
|
||||
});
|
||||
});
|
||||
|
||||
it('delegates to fetch on the client', () => {
|
||||
return nightmare.goto(base).init()
|
||||
.click('[href="credentials?creds=include"]')
|
||||
.wait(100)
|
||||
.page.title()
|
||||
.then(title => {
|
||||
assert.equal(title, 'woohoo!');
|
||||
});
|
||||
});
|
||||
|
||||
it('includes service worker', () => {
|
||||
return nightmare.goto(base).page.html().then(html => {
|
||||
assert.ok(html.indexOf('service-worker.js') !== -1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('headers', () => {
|
||||
it('sets Content-Type and Link...preload headers', () => {
|
||||
return capture(() => nightmare.goto(base)).then(requests => {
|
||||
const { headers } = requests[0];
|
||||
|
||||
assert.equal(
|
||||
headers['content-type'],
|
||||
'text/html'
|
||||
);
|
||||
|
||||
const str = ['main', '_\\.\\d+']
|
||||
.map(file => {
|
||||
return `<${basepath}/client/[^/]+/${file}\\.js>;rel="preload";as="script"`;
|
||||
})
|
||||
.join(', ');
|
||||
|
||||
const regex = new RegExp(str);
|
||||
|
||||
assert.ok(
|
||||
regex.test(headers['link']),
|
||||
headers['link']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function exec(cmd) {
|
||||
return new Promise((fulfil, reject) => {
|
||||
const parts = cmd.trim().split(' ');
|
||||
const proc = require('child_process').spawn(parts.shift(), parts);
|
||||
|
||||
proc.stdout.on('data', data => {
|
||||
process.stdout.write(data);
|
||||
});
|
||||
|
||||
proc.stderr.on('data', data => {
|
||||
process.stderr.write(data);
|
||||
});
|
||||
|
||||
proc.on('error', reject);
|
||||
|
||||
proc.on('close', () => fulfil());
|
||||
});
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
describe('dev mode', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/')
|
||||
});
|
||||
|
||||
it('has the correct <h1>', () => {
|
||||
cy.contains('h1', 'Great success!')
|
||||
});
|
||||
|
||||
it('navigates to /about', () => {
|
||||
cy.get('nav a').contains('about').click();
|
||||
cy.url().should('include', '/about');
|
||||
});
|
||||
|
||||
it('navigates to /blog', () => {
|
||||
cy.get('nav a').contains('blog').click();
|
||||
cy.url().should('include', '/blog');
|
||||
});
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
describe('prod mode', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/')
|
||||
});
|
||||
|
||||
it('has the correct <h1>', () => {
|
||||
cy.contains('h1', 'Great success!')
|
||||
});
|
||||
|
||||
it('navigates to /about', () => {
|
||||
cy.get('nav a').contains('about').click();
|
||||
cy.url().should('include', '/about');
|
||||
});
|
||||
|
||||
it('navigates to /blog', () => {
|
||||
cy.get('nav a').contains('blog').click();
|
||||
cy.url().should('include', '/blog');
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 331 KiB |
@@ -1,25 +0,0 @@
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add("login", (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
@@ -1,20 +0,0 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
@@ -1,75 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const rimraf = require('rimraf');
|
||||
const child_process = require('child_process');
|
||||
|
||||
// ensure sapper doesn't exist in app/node_modules
|
||||
rimraf.sync(
|
||||
path.join(__dirname, 'app/node_modules/sapper')
|
||||
);
|
||||
|
||||
rimraf.sync(
|
||||
path.join(__dirname, 'app/node_modules/.bin/sapper')
|
||||
);
|
||||
|
||||
// create symlinks
|
||||
fs.symlinkSync(
|
||||
path.join(__dirname, '..'),
|
||||
path.join(__dirname, 'app/node_modules/sapper')
|
||||
);
|
||||
|
||||
fs.symlinkSync(
|
||||
path.join(__dirname, '../cli/index.js'),
|
||||
path.join(__dirname, 'app/node_modules/.bin/sapper')
|
||||
);
|
||||
|
||||
const app_dir = path.join(__dirname, 'app');
|
||||
|
||||
function start_server() {
|
||||
const server = child_process.spawn(process.execPath, ['server.js'], {
|
||||
cwd: app_dir,
|
||||
env: {
|
||||
NODE_ENV: 'development'
|
||||
},
|
||||
stdio: 'pipe'
|
||||
});
|
||||
|
||||
server.stdout.on('data', (data) => {
|
||||
process.stdout.write(data);
|
||||
});
|
||||
|
||||
server.stderr.on('data', (data) => {
|
||||
process.stderr.write(data);
|
||||
});
|
||||
}
|
||||
|
||||
function launch() {
|
||||
if (process.argv[2] === '--dev') {
|
||||
start_server();
|
||||
} else {
|
||||
child_process.exec(`npm run build`, {
|
||||
cwd: app_dir
|
||||
}, (err, stdout, stderr) => {
|
||||
if (err) throw err;
|
||||
|
||||
process.stdout.write(stdout);
|
||||
process.stderr.write(stderr);
|
||||
start_server();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// this is a terrible hack
|
||||
if (process.env.APPVEYOR) {
|
||||
child_process.exec(`npm install`, {
|
||||
cwd: app_dir
|
||||
}, (err, stdout, stderr) => {
|
||||
if (err) throw err;
|
||||
|
||||
process.stdout.write(stdout);
|
||||
process.stderr.write(stderr);
|
||||
launch();
|
||||
});
|
||||
} else {
|
||||
launch();
|
||||
}
|
||||
@@ -1,25 +1,105 @@
|
||||
const path = require('path');
|
||||
const assert = require('assert');
|
||||
|
||||
const create_routes = require('../../lib/utils/create_routes.js');
|
||||
const { create_routes } = require('../../dist/core.ts.js');
|
||||
|
||||
describe('create_routes', () => {
|
||||
it('sorts routes correctly', () => {
|
||||
const routes = create_routes(['index.html', 'about.html', '[wildcard].html', 'post/[id].html']);
|
||||
const routes = create_routes({
|
||||
files: ['index.html', 'about.html', 'post/f[xx].html', '[wildcard].html', 'post/foo.html', 'post/[id].html', 'post/bar.html', 'post/[id].json.js']
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
routes.map(r => r.file),
|
||||
[
|
||||
'about.html',
|
||||
'index.html',
|
||||
'about.html',
|
||||
'post/bar.html',
|
||||
'post/foo.html',
|
||||
'post/f[xx].html',
|
||||
'post/[id].json.js',
|
||||
'post/[id].html',
|
||||
'[wildcard].html'
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it('prefers index page to nested route', () => {
|
||||
let routes = create_routes({
|
||||
files: [
|
||||
'api/examples/[slug].js',
|
||||
'api/examples/index.js',
|
||||
'blog/[slug].html',
|
||||
'api/gists/[id].js',
|
||||
'api/gists/index.js',
|
||||
'4xx.html',
|
||||
'5xx.html',
|
||||
'blog/index.html',
|
||||
'blog/rss.xml.js',
|
||||
'guide/index.html',
|
||||
'index.html'
|
||||
]
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
routes.map(r => r.file),
|
||||
[
|
||||
'4xx.html',
|
||||
'5xx.html',
|
||||
'index.html',
|
||||
'guide/index.html',
|
||||
'blog/index.html',
|
||||
'blog/rss.xml.js',
|
||||
'blog/[slug].html',
|
||||
'api/examples/index.js',
|
||||
'api/examples/[slug].js',
|
||||
'api/gists/index.js',
|
||||
'api/gists/[id].js',
|
||||
]
|
||||
);
|
||||
|
||||
routes = create_routes({
|
||||
files: [
|
||||
'4xx.html',
|
||||
'5xx.html',
|
||||
'api/blog/[slug].js',
|
||||
'api/blog/index.js',
|
||||
'api/guide/contents.js',
|
||||
'api/guide/index.js',
|
||||
'blog/[slug].html',
|
||||
'blog/index.html',
|
||||
'blog/rss.xml.js',
|
||||
'gist/[id].js',
|
||||
'gist/create.js',
|
||||
'guide/index.html',
|
||||
'index.html',
|
||||
'repl/index.html'
|
||||
]
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
routes.map(r => r.file),
|
||||
[
|
||||
'4xx.html',
|
||||
'5xx.html',
|
||||
'index.html',
|
||||
'guide/index.html',
|
||||
'blog/index.html',
|
||||
'blog/rss.xml.js',
|
||||
'blog/[slug].html',
|
||||
'gist/create.js',
|
||||
'gist/[id].js',
|
||||
'repl/index.html',
|
||||
'api/guide/index.js',
|
||||
'api/guide/contents.js',
|
||||
'api/blog/index.js',
|
||||
'api/blog/[slug].js',
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it('generates params', () => {
|
||||
const routes = create_routes(['index.html', 'about.html', '[wildcard].html', 'post/[id].html']);
|
||||
const routes = create_routes({
|
||||
files: ['index.html', 'about.html', '[wildcard].html', 'post/[id].html']
|
||||
});
|
||||
|
||||
let file;
|
||||
let params;
|
||||
@@ -38,14 +118,124 @@ describe('create_routes', () => {
|
||||
});
|
||||
|
||||
it('ignores files and directories with leading underscores', () => {
|
||||
const routes = create_routes(['index.html', '_foo.html', 'a/_b/c/d.html', 'e/f/g/h.html', 'i/_j.html']);
|
||||
const routes = create_routes({
|
||||
files: ['index.html', '_foo.html', 'a/_b/c/d.html', 'e/f/g/h.html', 'i/_j.html']
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
routes.map(r => r.file),
|
||||
[
|
||||
'e/f/g/h.html',
|
||||
'index.html'
|
||||
'index.html',
|
||||
'e/f/g/h.html'
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it('matches /foo/:bar before /:baz/qux', () => {
|
||||
const a = create_routes({
|
||||
files: ['foo/[bar].html', '[baz]/qux.html']
|
||||
});
|
||||
const b = create_routes({
|
||||
files: ['[baz]/qux.html', 'foo/[bar].html']
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
a.map(r => r.file),
|
||||
['foo/[bar].html', '[baz]/qux.html']
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
b.map(r => r.file),
|
||||
['foo/[bar].html', '[baz]/qux.html']
|
||||
);
|
||||
});
|
||||
|
||||
it('fails if routes are indistinguishable', () => {
|
||||
assert.throws(() => {
|
||||
create_routes({
|
||||
files: ['[foo].html', '[bar]/index.html']
|
||||
});
|
||||
}, /The \[foo\].html and \[bar\]\/index.html routes clash/);
|
||||
|
||||
assert.throws(() => {
|
||||
create_routes({
|
||||
files: ['foo.html', 'foo.js']
|
||||
});
|
||||
}, /The foo.html and foo.js routes clash/);
|
||||
});
|
||||
|
||||
it('matches nested routes', () => {
|
||||
const route = create_routes({
|
||||
files: ['settings/[submenu].html']
|
||||
})[0];
|
||||
|
||||
assert.deepEqual(route.exec('/settings/foo'), {
|
||||
submenu: 'foo'
|
||||
});
|
||||
|
||||
assert.deepEqual(route.exec('/settings'), {
|
||||
submenu: null
|
||||
});
|
||||
});
|
||||
|
||||
it('prefers index routes to nested routes', () => {
|
||||
const routes = create_routes({
|
||||
files: ['settings/[submenu].html', 'settings.html']
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
routes.map(r => r.file),
|
||||
['settings.html', 'settings/[submenu].html']
|
||||
);
|
||||
});
|
||||
|
||||
it('matches deeply nested routes', () => {
|
||||
const route = create_routes({
|
||||
files: ['settings/[a]/[b]/index.html']
|
||||
})[0];
|
||||
|
||||
assert.deepEqual(route.exec('/settings/foo/bar'), {
|
||||
a: 'foo',
|
||||
b: 'bar'
|
||||
});
|
||||
|
||||
assert.deepEqual(route.exec('/settings/foo'), {
|
||||
a: 'foo',
|
||||
b: null
|
||||
});
|
||||
|
||||
assert.deepEqual(route.exec('/settings'), {
|
||||
a: null,
|
||||
b: null
|
||||
});
|
||||
});
|
||||
|
||||
it('matches a dynamic part within a part', () => {
|
||||
const route = create_routes({
|
||||
files: ['things/[slug].json.js']
|
||||
})[0];
|
||||
|
||||
assert.deepEqual(route.exec('/things/foo.json'), {
|
||||
slug: 'foo'
|
||||
});
|
||||
});
|
||||
|
||||
it('matches multiple dynamic parts within a part', () => {
|
||||
const route = create_routes({
|
||||
files: ['things/[id]_[slug].json.js']
|
||||
})[0];
|
||||
|
||||
assert.deepEqual(route.exec('/things/someid_someslug.json'), {
|
||||
id: 'someid',
|
||||
slug: 'someslug'
|
||||
});
|
||||
});
|
||||
|
||||
it('fails if dynamic params are not separated', () => {
|
||||
assert.throws(() => {
|
||||
create_routes({
|
||||
files: ['[foo][bar].js']
|
||||
});
|
||||
}, /Invalid route \[foo\]\[bar\]\.js — parameters must be separated/);
|
||||
});
|
||||
});
|
||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"diagnostics": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitAny": true,
|
||||
"noEmitOnError": true,
|
||||
"allowJs": true,
|
||||
"lib": ["es5", "es6", "dom"],
|
||||
"importHelpers": true
|
||||
},
|
||||
"target": "ES5",
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
2
webpack.js
Normal file
2
webpack.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// TODO write to this file, instead of webpack.ts.js
|
||||
module.exports = require('./dist/webpack.ts.js');
|
||||
@@ -1,41 +1,2 @@
|
||||
const path = require('path');
|
||||
const route_manager = require('../lib/route_manager.js');
|
||||
const { src, dest, dev, server_routes } = require('../lib/config.js');
|
||||
|
||||
module.exports = {
|
||||
dev,
|
||||
|
||||
client: {
|
||||
entry: () => {
|
||||
return {
|
||||
main: `${dest}/main.js`
|
||||
};
|
||||
},
|
||||
|
||||
output: () => {
|
||||
return {
|
||||
path: `${dest}/client`,
|
||||
filename: '[name].[hash].js',
|
||||
chunkFilename: '[name].[id].[hash].js',
|
||||
publicPath: '/client/'
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
server: {
|
||||
entry: () => {
|
||||
return {
|
||||
server_routes
|
||||
}
|
||||
},
|
||||
|
||||
output: () => {
|
||||
return {
|
||||
path: `${dest}/server`,
|
||||
filename: '[name].[hash].js',
|
||||
chunkFilename: '[name].[id].[hash].js',
|
||||
libraryTarget: 'commonjs2'
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
// TODO deprecate this file in favour of sapper/webpack.js
|
||||
module.exports = require('../dist/webpack.ts.js');
|
||||
@@ -1 +0,0 @@
|
||||
import 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user