Compare commits

..

331 Commits

Author SHA1 Message Date
Richard Harris
83c8d7f855 -> v0.25.0 2019-02-01 06:45:08 -05:00
Rich Harris
fdfe282130 Merge pull request #484 from mrkishi/goto-temporary-fix
Ensure deepest layout is always refreshed on goto
2019-02-01 06:40:43 -05:00
Rich Harris
b5fbc7e0e8 Merge pull request #539 from cudr/nested-routes-navigate-fix
Fix nested routes navigates
2019-02-01 06:39:25 -05:00
Rich Harris
64eb3f856a Merge pull request #369 from sveltejs/crawl-queue
Don't try to crawl hundreds of pages simultaneously
2019-02-01 06:15:30 -05:00
Richard Harris
18d8e61ecb remove unused declaration 2019-02-01 06:04:43 -05:00
Richard Harris
2a635f92a9 merge master -> crawl-queue 2019-02-01 06:04:00 -05:00
Richard Harris
44bcbeb7d6 remove redundant line 2019-02-01 05:57:17 -05:00
Rich Harris
4023831b18 Merge pull request #542 from sveltejs/gh-528
avoid mutating `opts.headers` in `this.fetch` (#528)
2019-02-01 05:47:54 -05:00
Richard Harris
969169ae20 -> v0.24.3 2019-02-01 05:40:58 -05:00
Rich Harris
79fa15da3d Merge pull request #525 from nolanlawson/nolan/sw-index-html
add service-worker-index.html
2019-02-01 05:36:08 -05:00
Rich Harris
ddc08d94cc Merge pull request #535 from nolanlawson/nolan/no-sourcemap-in-sw
Do not include sourcemap files in Service Worker shell
2019-02-01 05:34:20 -05:00
Conduitry
92206742d4 avoid mutating opts.headers in this.fetch (#528) 2018-12-31 17:08:29 -05:00
Richard Harris
e2193a6080 -> v0.24.2 2018-12-31 11:46:08 -05:00
Richard Harris
f66c7dcb0d -> v0.24.2 2018-12-31 11:45:20 -05:00
Rich Harris
06f1a0e6c0 Merge pull request #541 from sveltejs/rollup-1.0
support Rollup 1.0
2018-12-31 11:44:22 -05:00
Conduitry
7726325b4b support Rollup 1.0 2018-12-31 11:14:49 -05:00
cudr
b6bc90cea9 add test 2018-12-26 14:12:49 +03:00
cudr
cfba9b2168 fix nested routes navigate 2018-12-25 19:05:00 +03:00
Nolan Lawson
f97400caaa Do not include sourcemap files in Service Worker shell
Fixes #534
2018-12-16 11:48:54 -08:00
Nolan Lawson
03af9b1a16 add service-worker-index.html
fixes #422
2018-12-12 18:30:16 +00:00
Rich Harris
02cef046aa -> v0.24.1 2018-12-09 10:30:34 -05:00
Rich Harris
c2aeac34b6 Merge pull request #530 from artemjackson/patch-1
Fixed rel="preload" `as` attribute for styles
2018-12-09 10:28:39 -05:00
Rich Harris
abd2f7fd39 Merge pull request #529 from artemjackson/master
Added css build info for webpack apps
2018-12-09 10:28:19 -05:00
Artyom Stepanishchev
e7cf9bf1b6 Merge branch 'master' of github.com:sveltejs/sapper 2018-12-09 18:01:31 +03:00
Artyom Stepanishchev
1fdd0e3ad2 Merge branch 'master' of github.com:sveltejs/sapper into patch-1 2018-12-09 18:00:55 +03:00
Rich Harris
af0a7e04f9 Merge pull request #531 from artemjackson/bugfix/tests
Fixed tests for node 6
2018-12-09 09:57:12 -05:00
Artyom Stepanishchev
ed19a19fed Fixed tests for node 6 2018-12-09 17:28:59 +03:00
Artyom Stepanishchev
8ebfcc9a54 Fixed rel="preload" as attribute for styles 2018-12-07 23:59:51 +03:00
Artyom Stepanishchev
af2a792508 Added css build info for webpack apps 2018-12-07 23:28:28 +03:00
Conduitry
14e809af6e update npm-run-all to remove malicious flatmap-stream@0.1.1 2018-12-06 07:02:04 -05:00
Rich Harris
03c5f5b446 -> v0.24.0 2018-10-27 12:40:27 -04:00
Rich Harris
6655b1b49d Merge pull request #503 from sveltejs/gh-490
redirect to external URLs
2018-10-27 12:36:10 -04:00
Rich Harris
eebf076f23 Merge pull request #501 from sveltejs/gh-497
consistent query parameter handling between client and server
2018-10-27 12:35:58 -04:00
Rich Harris
198be28f4b Merge pull request #504 from sveltejs/gh-480
change scroll[X|Y] to page[X|Y]Offset
2018-10-27 12:35:47 -04:00
Rich Harris
4f720446b2 change scroll[X|Y] to page[X|Y]Offset - closes #480 2018-10-27 12:26:53 -04:00
Rich Harris
e69cb3639a redirect to external URLs - closes #490 2018-10-27 12:14:28 -04:00
Rich Harris
1b1a86764f Merge pull request #502 from sveltejs/gh-495
strip leading slash from basepath
2018-10-27 11:50:23 -04:00
Rich Harris
f50f83c4a4 strip leading slash from basepath - fixes #495 2018-10-27 11:41:02 -04:00
Rich Harris
eadefd996b consistent query parameter handling between client and server - fixes #497 2018-10-27 11:36:18 -04:00
Rich Harris
ab52aabd1d Merge pull request #500 from sveltejs/dont-buffer-logs
don't buffer stdout/stderr
2018-10-27 11:18:50 -04:00
Rich Harris
c5a80543b3 reduce flakiness of unrelated test 2018-10-27 11:12:10 -04:00
Rich Harris
cfd95ac024 dont buffer stdout/stderr 2018-10-27 11:02:34 -04:00
Rich Harris
73ff95c677 Merge pull request #498 from mrkishi/build-dir
Add missing posixify to `build_dir`
2018-10-27 10:17:58 -04:00
mrkishi
382fe6b7b9 Add missing posixify 2018-10-26 11:03:29 -03:00
Rich Harris
3b714c0de3 -> v0.23.5 2018-10-24 21:40:04 -04:00
Rich Harris
28186227a9 add tests 2018-10-24 21:20:27 -04:00
Rich Harris
2ac0f2bf3d Merge branch 'search-params-decoding' of https://github.com/mrkishi/sapper into mrkishi-search-params-decoding 2018-10-24 21:10:35 -04:00
Rich Harris
4991f3b359 support non-native promise implementations 2018-10-24 21:05:25 -04:00
Rich Harris
65128118c7 Merge branch '487-async-route-errors' of https://github.com/nikku/sapper into nikku-487-async-route-errors 2018-10-24 20:58:52 -04:00
Rich Harris
3eced6fa4d Merge pull request #492 from sveltejs/lazy-css
fix lazy css bug, add tests
2018-10-24 20:58:08 -04:00
mrkishi
c4aee66c32 Fix search params decoding 2018-10-24 21:19:03 -03:00
Rich Harris
410c52df41 fix lazy css bug, add tests 2018-10-24 18:48:38 -04:00
Rich Harris
ffd56e2a20 -> v0.23.4 2018-10-24 15:51:44 -04:00
Rich Harris
1e5a87cf71 Merge pull request #491 from sveltejs/empty-href-export
ignore empty anchors when exporting
2018-10-24 15:51:28 -04:00
Rich Harris
281e183c61 ignore empty anchors when exporting 2018-10-24 15:38:45 -04:00
Nico Rehwaldt
3fe7b55955 async -> Promise.reject 2018-10-20 22:46:42 +02:00
Nico Rehwaldt
464924ed67 handle async route errors
Related to #487
2018-10-20 22:40:21 +02:00
mrkishi
8108642845 Ensure deepest layout is always refreshed on goto 2018-10-18 18:14:23 -03:00
Rich Harris
e5d7d8ab2b -> v0.23.3 2018-10-16 16:16:11 -04:00
Rich Harris
d3e560325d Merge pull request #477 from sveltejs/clear-errors
clear errors on successful render
2018-10-16 16:15:28 -04:00
Rich Harris
64e5065aa5 clear errors on successful render 2018-10-16 15:59:57 -04:00
Rich Harris
cb45bb0fbe -> v0.23.2 2018-10-16 08:58:02 -04:00
Rich Harris
f39455014a update deps 2018-10-16 08:44:22 -04:00
Rich Harris
4fe8df3696 Merge pull request #471 from sveltejs/missing-css
include css depended upon by entry point, even if also depended on by a lazily-loaded component
2018-10-16 08:38:20 -04:00
Rich Harris
4fdc7055c1 -> v0.23.1 2018-10-15 22:10:18 -04:00
Rich Harris
cca417a85a simplify, preserve monomorphism 2018-10-15 22:07:23 -04:00
Rich Harris
635c13a175 Merge branch 'tags-from-other-pages' of https://github.com/DayBr3ak/sapper into DayBr3ak-tags-from-other-pages 2018-10-15 21:43:39 -04:00
Rich Harris
2e3aef8b21 simplify 2018-10-15 21:36:40 -04:00
Rich Harris
44736754ad fix file extension 2018-10-15 21:27:52 -04:00
Benjamin GROENEVELD
a399d87d9b handle tag click from another page 2018-10-13 21:59:47 +02:00
Benjamin GROENEVELD
a68c62ce91 Fix hash link reliability (fix #434) 2018-10-13 16:45:14 +02:00
Rich Harris
1b9b559d82 include css depended upon by entry point, even if also depended on by a lazily-loaded component 2018-10-11 23:18:51 -04:00
Rich Harris
abcac75826 -> v0.23.0 2018-10-09 15:03:02 -04:00
Rich Harris
4118c566d1 Merge pull request #470 from aubergene/ssi
Allow SSI comments
2018-10-09 15:02:21 -04:00
Rich Harris
0b76f12394 Merge pull request #469 from sveltejs/remove-env-vars
replace magic env vars with documented CLI flags
2018-10-09 14:59:30 -04:00
Julian Burgess
e51cb85c7c Allow SSI directives 2018-10-09 19:50:25 +01:00
Rich Harris
6ae9a5e7c5 replace magic env vars with documented CLI flags 2018-10-09 08:33:25 -04:00
Rich Harris
52f40f9e63 Improve internal API 2018-10-08 19:21:15 -04:00
Rich Harris
5e59855a15 overhaul tests 2018-10-07 18:23:43 -04:00
Rich Harris
18acef3190 -> v0.22.10 2018-10-05 21:25:49 -04:00
Rich Harris
d7f6ca8b4d Merge pull request #466 from sveltejs/css-basepath
no need to use basepath in <link>
2018-10-05 21:22:15 -04:00
Rich Harris
00321932ef no need to use basepath in <link> 2018-10-05 21:16:27 -04:00
Rich Harris
7eb1ec727c add tests for #376 2018-10-05 20:59:45 -04:00
Rich Harris
3f586e19a1 minor tweaks 2018-10-05 20:57:15 -04:00
Daniil Khanin
05b702938f implement sapper no scroll 2018-10-05 00:13:08 +03:00
Rich Harris
3026e7c36e remove some leftover logging 2018-10-02 11:39:38 -04:00
Rich Harris
27a5aed83e -> v0.22.9 2018-10-02 11:13:36 -04:00
Rich Harris
bb04af41bd fix legacy builds 2018-10-02 11:13:09 -04:00
Rich Harris
9403799393 -> v0.22.8 2018-10-02 10:37:19 -04:00
Rich Harris
472c0c198a Merge pull request #462 from sveltejs/overwrite-css-placeholders
ensure CSS placeholders are overwritten
2018-10-02 10:36:39 -04:00
Rich Harris
02256ae214 ensure CSS placeholders are overwritten 2018-10-02 10:27:02 -04:00
Rich Harris
e2d325ec9f -> v0.22.7 2018-10-01 22:58:44 -04:00
Rich Harris
954bcba333 Merge pull request #460 from sveltejs/cookies
more robust cookies
2018-10-01 22:58:30 -04:00
Rich Harris
709c9992e3 more robust cookies 2018-10-01 22:47:11 -04:00
Rich Harris
9773781262 -> v0.22.6 2018-10-01 20:55:25 -04:00
Rich Harris
48b1fafc33 Merge pull request #459 from sveltejs/gh-458
inject a <script> with crossOrigin - fixes #458
2018-10-01 20:48:42 -04:00
Rich Harris
d1624add66 Merge pull request #456 from mrkishi/windows-paths
Fix rollup input paths on Windows
2018-10-01 20:48:21 -04:00
Rich Harris
e2206d0e0d inject a <script> with crossOrigin - fixes #458 2018-10-01 18:55:38 -04:00
Rich Harris
9cd4da4c39 -> v0.22.5 2018-10-01 17:54:29 -04:00
Rich Harris
6ded1a5975 slap self in face 2018-10-01 17:52:57 -04:00
mrkishi
584ddd1c85 Fix rollup input paths on Windows 2018-10-01 17:47:44 -03:00
Rich Harris
4071acf7c0 -> v0.22.4 2018-10-01 15:53:40 -04:00
Rich Harris
e8773d3196 Merge pull request #455 from sveltejs/deconflict-server
put server assets in subfolder
2018-10-01 15:52:14 -04:00
Rich Harris
01a519a4d9 fix everything i just broke 2018-10-01 15:47:01 -04:00
Rich Harris
d9ad1d1b10 put server assets in subfolder 2018-10-01 15:21:35 -04:00
Rich Harris
0826a58995 -> v0.22.3 2018-10-01 12:13:52 -04:00
Rich Harris
6a74097b0c ensure dev client is not imported if server imports client.js 2018-10-01 12:13:03 -04:00
Rich Harris
278be67228 -> v0.22.2 2018-09-30 22:06:19 -04:00
Rich Harris
64921dfc3c make directories relative to project 2018-09-30 22:05:55 -04:00
Rich Harris
c8962ccf8c update gitignore 2018-09-30 21:12:46 -04:00
Rich Harris
664c093391 -> v0.22.1 2018-09-30 21:10:17 -04:00
Rich Harris
4375feac83 -> v0.22.0 2018-09-30 21:05:31 -04:00
Rich Harris
4d7d448597 Merge pull request #453 from sveltejs/gh-444
move app logic into templates (#444)
2018-09-30 21:03:10 -04:00
Rich Harris
2e2b8dcd83 small tweak 2018-09-30 18:26:44 -04:00
Rich Harris
b915bab070 gah cant use agadoo because of placeholder imports 2018-09-30 18:21:28 -04:00
Rich Harris
8530d06d00 golf things down a bit 2018-09-30 18:11:40 -04:00
Rich Harris
a43764a971 put everything in __sapper__ 2018-09-30 14:55:54 -04:00
Rich Harris
4f6efbda79 node 6 support 2018-09-30 12:54:50 -04:00
Rich Harris
5573258a10 update tests 2018-09-30 11:32:58 -04:00
Rich Harris
2185f89669 enforce client app treeshakeability 2018-09-30 10:50:25 -04:00
Rich Harris
e30842caa8 move app logic into templates (#444) 2018-09-30 10:30:00 -04:00
Rich Harris
ff24877d8f -> v0.21.1 2018-09-23 23:21:37 -04:00
Rich Harris
9cf90ce01d -> v0.21.0 2018-09-23 21:48:53 -04:00
Rich Harris
e7f9ddae86 Merge pull request #446 from mrkishi/escaping-issues
Fix filenames
2018-09-23 21:47:28 -04:00
Rich Harris
ffa1e1f704 Merge branch 'master' into escaping-issues 2018-09-23 21:41:02 -04:00
Rich Harris
80bb958b47 Merge pull request #443 from sveltejs/gh-432
update folder structure
2018-09-23 21:39:05 -04:00
mrkishi
532f559fc5 Update package-lock.json 2018-09-21 15:40:12 -03:00
mrkishi
0bd1b0b8e2 Fix filename escaping issues 2018-09-21 15:34:59 -03:00
Rich Harris
10c5ff4169 remove test app manifest from repo 2018-09-19 17:51:56 -04:00
Rich Harris
273823dfd7 move config into single file, add errors to help people migrate 2018-09-19 16:27:30 -04:00
Rich Harris
8f064fe5ac update folder structure (#432) 2018-09-19 12:02:11 -04:00
Rich Harris
f29e7efbd6 const -> let 2018-09-19 11:25:04 -04:00
Rich Harris
e66e3cd7eb -> v0.20.4 2018-09-19 11:11:42 -04:00
Rich Harris
ff415b391b Merge pull request #436 from nsivertsen/devtools
Enable debugging in Chrome and VS Code - fixes #435
2018-09-19 11:09:19 -04:00
Rich Harris
91182ad0a2 Merge pull request #441 from silentworks/bugfix/legacy-manifest
Fix for legacy manifest file
2018-09-19 10:43:50 -04:00
Andrew Smith
467041a3cd Fix for legacy manifest file 2018-09-13 19:45:30 +01:00
Nikolai Sivertsen
520949c5e1 Enable debugging in Chrome and VS Code - fixes 435 2018-09-12 16:55:45 +02:00
Rich Harris
8c07d9d2ac -> v0.20.3 2018-09-08 10:24:21 -04:00
Rich Harris
7bd684a80e Merge pull request #428 from nolanlawson/cache-control-service-worker
Set service-worker max-age to 0
2018-09-08 10:20:04 -04:00
Rich Harris
cbb5e8755b Use MDN recommendation for preventing SW caching 2018-09-08 10:12:55 -04:00
Rich Harris
7ef72dbb77 Merge pull request #429 from nolanlawson/consistent-cache-control
Use consistent cache-control:max-age=600 for HTML pages
2018-09-08 09:40:56 -04:00
Nolan Lawson
87ff9c2aeb fix test 2018-09-07 17:25:17 -07:00
Nolan Lawson
2d1f535314 use consistent cache-control:max-age=600 for HTML pages 2018-09-07 16:46:40 -07:00
Rich Harris
cd1b53b80d Merge pull request #424 from nolanlawson/csp-nonce
Allow scripts to contain a CSP nonce
2018-09-07 19:46:07 -04:00
Rich Harris
0a7be736c0 snake_case 2018-09-07 19:45:40 -04:00
Nolan Lawson
5ee53a98c6 set service-worker max-age to 0 2018-09-07 16:28:38 -07:00
Rich Harris
0e8ed6612c -> v0.20.2 2018-09-07 18:19:39 -04:00
Rich Harris
5ec748b95d Merge pull request #427 from sveltejs/gh-426
handle value-less query string parameters
2018-09-07 18:18:28 -04:00
Rich Harris
64b16715cd handle value-less query string parameters - fixes #426 2018-09-07 18:02:58 -04:00
Rich Harris
9ea5e5e251 Merge pull request #425 from nolanlawson/cache-control-immutable
Add cache-control:immutable for immutable assets
2018-09-07 17:12:23 -04:00
Rich Harris
68b78f56d6 remove unused file 2018-09-07 16:42:56 -04:00
Nolan Lawson
68e93a8fa0 add cache-control:immutable for immutable assets 2018-09-07 11:07:50 -07:00
Nolan Lawson
e377515867 allow scripts to contain a CSP nonce 2018-09-07 09:46:53 -07:00
Rich Harris
99ae39b8a8 -> v0.20.1 2018-09-06 18:57:11 -04:00
Rich Harris
1b489f4687 -> v0.20.0 2018-09-04 07:35:24 -04:00
Rich Harris
91f2c6e49c Merge pull request #418 from nsivertsen/sourcemaps
Enable source maps by default in dev mode when using rollup
2018-09-04 07:32:15 -04:00
Rich Harris
f5e07e9f78 Merge pull request #419 from sveltejs/gh-417
decode req.params
2018-09-04 07:31:53 -04:00
Rich Harris
17297a9794 Merge pull request #414 from sveltejs/gh-347-b
decode req.path during export
2018-09-04 07:31:36 -04:00
Rich Harris
9ef4f33e38 decode query params 2018-09-03 20:09:25 -04:00
Rich Harris
30966ee7f2 decode req.params - fixes #417 2018-09-03 18:36:07 -04:00
Nikolai Sivertsen
ae90f774e1 Enable source maps by default in dev mode when using rollup 2018-09-04 00:26:18 +02:00
Rich Harris
0706b5f50a decode req.path during export 2018-09-03 09:02:13 -04:00
Rich Harris
499b377bfd -> v0.19.3 2018-09-03 07:55:30 -04:00
Rich Harris
1baeb79d4b Merge branch 'master' of github.com:sveltejs/sapper 2018-09-03 07:54:42 -04:00
Rich Harris
0cc5ff95d6 minor tidy up 2018-09-03 07:54:39 -04:00
Rich Harris
e90525c1e8 Merge pull request #413 from sveltejs/gh-347
better unicode handling - fixes #347, i think
2018-09-03 07:47:41 -04:00
Rich Harris
6ccae0cd33 better unicode handling - fixes #347, i think 2018-09-02 23:00:39 -04:00
Rich Harris
8b60d568dc -> v0.19.2 2018-09-02 21:56:45 -04:00
Rich Harris
64c2394c9d Merge branch 'master' of github.com:sveltejs/sapper 2018-09-02 21:54:13 -04:00
Rich Harris
b28037291a Merge pull request #412 from sveltejs/gh-315
allow reserved words as route names
2018-09-02 21:54:09 -04:00
Rich Harris
bf9cbe2f3b print details of webpack errors - fixes #403 2018-09-02 21:53:46 -04:00
Rich Harris
2c507b5a2e allow reserved words as route names - fixes #315 2018-09-02 21:46:25 -04:00
Rich Harris
4a92fbbbfa Merge pull request #410 from sveltejs/gh-220
ignore things that look like temp files when generating manifest data
2018-09-02 21:13:33 -04:00
Rich Harris
b16440ff0f Merge pull request #411 from sveltejs/gh-235
ignore clicks on <a> elements without hrefs
2018-09-02 21:13:18 -04:00
Rich Harris
64223b572b ignore clicks on <a> elements without hrefs - fixes #235 2018-09-02 20:56:50 -04:00
Rich Harris
1b6dfd3580 ignore things that look like temp files when generating manifest data - fixes #220 2018-09-02 20:33:00 -04:00
Rich Harris
c0b833862a -> v0.19.1 2018-09-02 18:41:32 -04:00
Rich Harris
45f4c47a3e oops that wasnt quite right 2018-09-02 18:41:17 -04:00
Rich Harris
48b87edb5b Merge branch 'master' of github.com:sveltejs/sapper 2018-09-02 18:27:20 -04:00
Rich Harris
f9f283603e Merge pull request #409 from sveltejs/fix-redirects
don't include origin in export redirects
2018-09-02 18:26:56 -04:00
Rich Harris
a56ee6bdb7 regenerate lockfile 2018-09-02 17:01:43 -04:00
Rich Harris
a18af2a473 dont include origin in export redirects 2018-09-02 17:01:29 -04:00
Rich Harris
fe5a8fb1e7 dont include .map files in package 2018-09-02 14:53:12 -04:00
Rich Harris
57a26e3511 -> v0.19.0 2018-09-02 14:50:01 -04:00
Rich Harris
bebb0dd595 CSS extraction and code-splitting
closes #388
2018-09-02 14:46:27 -04:00
Rich Harris
afba0491ed -> v0.18.7 2018-08-31 16:40:50 -04:00
Rich Harris
350d37e210 Merge pull request #397 from sveltejs/gh-280
Implement differential bundling
2018-08-31 16:39:20 -04:00
Rich Harris
96fc19e939 update shimport 2018-08-31 16:32:56 -04:00
Rich Harris
5be3809d9e -> v0.18.6 2018-08-31 11:13:24 -04:00
Rich Harris
15cc4bf296 bundle webpack-format-messages 2018-08-31 11:13:03 -04:00
Rich Harris
c7cce985e3 serve legacy assets if such there be 2018-08-30 23:13:34 -04:00
Rich Harris
e00b315dec emit legacy build 2018-08-30 22:58:07 -04:00
Rich Harris
afcd643035 -> v0.18.5 2018-08-30 20:41:03 -04:00
Rich Harris
7cc2a03aae oops 2018-08-30 20:38:40 -04:00
Rich Harris
002718b609 -> v0.18.4 2018-08-30 20:00:58 -04:00
Rich Harris
45d216c64d Merge pull request #396 from sveltejs/gh-381
implement --dev-port flag
2018-08-30 19:59:09 -04:00
Rich Harris
3d69d483d7 merge master -> gh-381 2018-08-30 19:49:28 -04:00
Rich Harris
54da524467 implement --dev-port flag - fixes #381 2018-08-30 19:46:14 -04:00
Rich Harris
ee95240ca6 Merge pull request #394 from sveltejs/gh-390
omit trailing slash from server route matchers
2018-08-30 19:37:23 -04:00
Rich Harris
74d5d1f9c0 Merge pull request #395 from sveltejs/gh-391
Remove webpack annotations when building with Rollup
2018-08-30 19:37:01 -04:00
Rich Harris
8c2688b1be remove url-parse 2018-08-30 19:34:59 -04:00
Rich Harris
e170e4af9b use builtin url module 2018-08-30 19:29:36 -04:00
Rich Harris
bc31c73c33 omit trailing slash from server route matchers - fixes #390 2018-08-30 18:48:10 -04:00
Rich Harris
7798f8f684 minor tidy up 2018-08-30 18:41:37 -04:00
Rich Harris
70fd7038b0 skip webpack annotations when using Rollup 2018-08-30 18:38:46 -04:00
Rich Harris
c6af2ddfa3 Merge pull request #393 from sveltejs/gh-392
handle non-Sapper responses when exporting
2018-08-30 18:29:17 -04:00
Rich Harris
65d0172abe handle non-Sapper responses when exporting - fixes #392 2018-08-30 18:22:40 -04:00
Rich Harris
1e22031765 -> v0.18.3 2018-08-29 22:52:01 -04:00
Rich Harris
46bf8f2b78 -> v0.18.2 2018-08-29 22:43:21 -04:00
Rich Harris
553db81b7b -> v0.18.1 2018-08-29 22:29:46 -04:00
Rich Harris
67cc29ed38 Merge pull request #386 from sveltejs/gh-385
implement --live
2018-08-29 22:28:42 -04:00
Rich Harris
36f930f489 use --live by default, if using Rollup or --no-hot 2018-08-29 22:19:53 -04:00
Rich Harris
3b098caa6e implement --live and --hot - fixes #385 2018-08-29 21:51:08 -04:00
Rich Harris
d63b9437b5 -> v0.18.0 2018-08-29 21:08:15 -04:00
Rich Harris
e51c733e3f Merge pull request #379 from sveltejs/gh-130
Rollup support
2018-08-29 21:02:48 -04:00
Rich Harris
708fe4c74b rebuild lockfile 2018-08-29 20:53:48 -04:00
Rich Harris
4259fc8e58 update shimport, minor tidy up 2018-08-29 20:20:02 -04:00
Rich Harris
f05a8e52a0 deprecate, dont break 2018-08-29 18:01:27 -04:00
Rich Harris
76cb6d97f3 oops 2018-08-29 18:01:17 -04:00
Rich Harris
5d0b7af47b i feel good about this 2018-08-29 17:40:13 -04:00
Rich Harris
bb737eeb32 success i think? 2018-08-29 17:37:20 -04:00
Rich Harris
86dee17040 come ON 2018-08-29 17:29:17 -04:00
Rich Harris
01a709e017 ffffuuuuuuuuuuu 2018-08-29 17:27:34 -04:00
Rich Harris
f87f0e3b80 stab in the dark 2018-08-29 17:24:48 -04:00
Rich Harris
8226e9bc1f starting to lose my sense of humour 2018-08-29 17:17:36 -04:00
Rich Harris
d6d0a15015 WHAT IS HAPPENING 2018-08-29 17:10:00 -04:00
Rich Harris
ddec58ebd4 ffs 2018-08-29 17:05:56 -04:00
Rich Harris
9d904b3911 argh 2018-08-29 17:02:21 -04:00
Rich Harris
c36df0d650 try to diagnose latest windows idiocy 2018-08-29 16:58:50 -04:00
Rich Harris
ae19288797 update rollup 2018-08-29 16:45:29 -04:00
Rich Harris
de308d5bb0 rebuild lockfile 2018-08-29 16:29:37 -04:00
Rich Harris
99b096a5c4 rimraf before mkdirp 2018-08-29 16:05:41 -04:00
Rich Harris
36fc8a947b use shimport 2018-08-29 15:58:27 -04:00
Rich Harris
6393a30b13 print nice build summaries 2018-08-29 15:03:10 -04:00
Rich Harris
458be49b35 emit errors and warnings, albeit clumsily 2018-08-29 13:26:31 -04:00
Rich Harris
f8d742bdd0 hashing 2018-08-29 10:55:52 -04:00
Rich Harris
7e698f1613 use watchChange plugin hook to detect invalidations 2018-08-28 18:06:31 -04:00
Rich Harris
70b5cc86dc replace client_assets.json with build.json, include bundler name 2018-08-28 17:56:46 -04:00
Rich Harris
19a5dcad1d update CLI 2018-08-28 17:29:27 -04:00
Rich Harris
85e25d6380 add a --bundler option, for forcing rollup or webpack 2018-08-28 17:29:14 -04:00
Rich Harris
6e2383b66b add shimport 2018-08-28 17:28:40 -04:00
Rich Harris
200c5fcbd2 get tests passing again 2018-08-25 14:01:07 -04:00
Rich Harris
9cbb8bdc33 first stab at supporting Rollup (#130) 2018-08-25 12:42:27 -04:00
Rich Harris
3d39836cfb prevent deprecation warnings 2018-08-25 09:12:06 -04:00
Rich Harris
24f2855f89 create a facade over webpack, to support alternative compilers 2018-08-25 09:11:45 -04:00
Rich Harris
d5bf206d2a Merge pull request #378 from sveltejs/export-timeout
export should fail on timeouts
2018-08-25 08:14:18 -04:00
Rich Harris
8abc01551e export should fail on timeouts 2018-08-24 14:25:48 -04:00
Rich Harris
62b8a79e9f -> v0.17.1 2018-08-23 11:23:40 -04:00
Rich Harris
7f255563a4 Merge pull request #371 from sveltejs/show-files-on-error
show which file is causing an error/warning
2018-08-23 11:21:04 -04:00
Rich Harris
32f4a50f25 show which file is causing an error/warning 2018-08-23 11:09:02 -04:00
Rich Harris
18e6f29de7 merge master -> crawl-queue 2018-08-22 18:39:28 -04:00
Rich Harris
b1a9be2dc3 -> v0.17.0 2018-08-19 22:07:04 -04:00
Rich Harris
c5456d3033 Merge pull request #365 from sveltejs/cheap-watch
Use cheap-watch
2018-08-19 22:05:33 -04:00
Rich Harris
9b33dad589 merge master -> cheap-watch 2018-08-19 19:03:12 -04:00
Rich Harris
4315a46ff2 -> v0.16.1 2018-08-19 18:55:41 -04:00
Rich Harris
0fb5827968 fix file watching 2018-08-19 18:55:09 -04:00
Rich Harris
f9bf23dc43 use cheap-watch instead of chokidar (#364) 2018-08-19 18:52:01 -04:00
Rich Harris
611017fd28 -> v0.16.0 2018-08-19 17:39:02 -04:00
Rich Harris
72b265a35f Merge pull request #363 from sveltejs/bundle-deps
[WIP] Slim down the bundle
2018-08-19 17:36:18 -04:00
Rich Harris
e0d533f2ea bundle more stuff 2018-08-19 16:15:04 -04:00
Rich Harris
dba83641e4 remove glob and cheerio from dependencies 2018-08-19 15:20:14 -04:00
Rich Harris
d0c6b9cdca limit parallel crawls 2018-08-19 12:49:58 -04:00
Rich Harris
14e5c8e761 update lockfile 2018-08-16 12:48:50 -04:00
Rich Harris
cbbf4a95db -> v0.15.8 2018-08-16 12:46:13 -04:00
Rich Harris
55b7ffd2ed Merge pull request #361 from sveltejs/handle-unknown-preload-errors
handle unknown preload errors
2018-08-16 12:44:27 -04:00
Rich Harris
9f4d4e70de can remove this, preloading is set false on render 2018-08-16 12:39:54 -04:00
Rich Harris
deef1bbfcf handle unknown preload errors 2018-08-16 12:25:23 -04:00
Seth Thompson
17b0fc0d0c nit 2018-08-11 17:52:53 -04:00
Seth Thompson
3c44c511e4 make sure page has expected preloading value 2018-08-11 17:51:44 -04:00
Seth Thompson
7cf1b9613a prefetching should not set root preloading value, closes #352 2018-08-11 12:26:27 -04:00
Rich Harris
99e5a9601c -> v0.15.7 2018-08-09 20:14:37 -04:00
Rich Harris
4c9c1dccf5 Merge pull request #350 from sveltejs/gh-344
pass response object to store getter
2018-08-09 20:13:17 -04:00
Rich Harris
2cddd5afa0 Merge pull request #345 from sveltejs/fix/redirect
Fix Preload's Redirect
2018-08-09 20:08:51 -04:00
Rich Harris
8c6a0c4773 Merge branch 'master' into gh-344 2018-08-09 20:03:37 -04:00
Rich Harris
af5063552d Merge pull request #351 from sveltejs/argh-windows
doh
2018-08-09 20:02:51 -04:00
Rich Harris
419d154794 fffffuuuuu 2018-08-09 19:53:26 -04:00
Rich Harris
abda059be5 doh 2018-08-09 19:46:09 -04:00
Rich Harris
444908cac5 pass response object to store getter - fixes #344 2018-08-08 10:57:10 -04:00
Luke Edwards
c6da26e1a0 add redirect test to root (“/“) 2018-08-06 20:29:28 -07:00
Luke Edwards
aad87857ce fix: replace leading slash in preload’s redirect 2018-08-06 20:28:28 -07:00
Rich Harris
666c113297 -> v0.15.6 2018-08-06 22:36:17 -04:00
Rich Harris
84a58f34a0 add test for exporting with custom basepath 2018-08-06 22:35:02 -04:00
Rich Harris
75f5b5c721 Merge pull request #342 from aubergene/gh-338
Remove basepath from deferred urls and add trailing slash to root
2018-08-06 22:06:09 -04:00
Julian Burgess
a176a3b79b Remove basepath from deferred urls and add trailing slash to root request 2018-08-06 16:43:02 +01:00
Rich Harris
1627a5767a -> v0.15.5 2018-08-03 01:18:12 -04:00
Rich Harris
6ff3a9e9ab Merge branch 'master' of github.com:sveltejs/sapper 2018-08-03 01:16:43 -04:00
Rich Harris
3ce2bd30f9 Use npm ci instead of npm install (#336)
* dont write server_info.json either - second half of #318

* use npm ci

* update lockfile

* try this
2018-08-03 01:16:26 -04:00
Rich Harris
de4f99807f Merge branch 'master' of github.com:sveltejs/sapper 2018-08-03 00:11:09 -04:00
Rich Harris
eae8351f77 Better/faster exporting
* add --build and --build-dir options to sapper export (#325)

* tweak export logging, update port-authority to prevent timeout bug

* better logging of export progress

* handle case where linked resource is already fetched

* default to .sapper/dev instead of .sapper

* handle query params and redirects

* dont write server_info.json either - second half of #318

* update changelog

* update lockfile

* try to track down ci test failures

* err wut

* curiouser and curiouser

* ok, seems to work now
2018-08-03 00:10:58 -04:00
Rich Harris
d386308301 update changelog 2018-08-02 13:23:12 -04:00
Rich Harris
13afbc84d7 dont write server_info.json either - second half of #318 2018-08-02 10:59:23 -04:00
Rich Harris
31327b3780 Merge pull request #333 from sveltejs/gh-332
only blur activeElement if there is one
2018-08-02 10:43:27 -04:00
Rich Harris
81f483d7b8 Merge pull request #334 from sveltejs/gh-318
dont emit client_info.json
2018-08-02 08:40:30 -04:00
Rich Harris
1bcf20511b dont emit client_info.json - fixes #318 2018-08-01 21:46:26 -04:00
Rich Harris
003fa8ab2c only blur activeElement if there is one - fixes #332 2018-08-01 21:34:30 -04:00
Rich Harris
d1fcd07c92 -> v0.15.4 2018-08-01 08:47:41 -04:00
Rich Harris
47a6d6f662 Merge pull request #326 from lukeed/feat/ignore
Add `ignore` option
2018-08-01 08:46:49 -04:00
Luke Edwards
4b2b6440d0 fix: use more specific ignore pattern;
~> leaked into another test’s route
2018-07-31 16:54:29 -07:00
Rich Harris
fc855f30f8 -> v0.15.3 2018-07-31 15:06:47 -04:00
Rich Harris
4a75fff4ec Merge pull request #329 from sveltejs/parallel-exports
parallelize exports
2018-07-31 15:05:38 -04:00
Rich Harris
7b7b695938 Merge pull request #320 from sveltejs/gh-319
Some dev documentation
2018-07-31 14:59:57 -04:00
Rich Harris
2fca2e295f Merge pull request #328 from sveltejs/export-no-minify-js
don't minify JS when minifying HTML
2018-07-31 14:59:27 -04:00
Rich Harris
eae991d369 parallelize exports 2018-07-31 14:56:29 -04:00
Rich Harris
c2b393d3fd dont minify JS when minifying HTML 2018-07-31 14:43:58 -04:00
Luke Edwards
566addd406 add tests for opts.ignore 2018-07-29 14:17:13 -07:00
Luke Edwards
3d77dacbd6 attach ignore options to test app, w/ matching routes 2018-07-29 14:17:04 -07:00
Luke Edwards
51b4f9cbbf add opts.ignore support 2018-07-29 14:01:44 -07:00
Robert Hall
1d611be83e Using npm 2018-07-25 15:50:56 -06:00
Robert Hall
1782904994 Some dev documentation 2018-07-25 10:10:22 -06:00
Rich Harris
e3ddbfc181 Merge pull request #314 from sveltejs/fix-child-segment
fix child.segment bug
2018-07-23 17:08:25 -04:00
Rich Harris
8e3830b646 fix child.segment bug 2018-07-23 17:02:35 -04:00
Rich Harris
b28cdff233 -> v0.15.2 2018-07-23 16:38:49 -04:00
Rich Harris
7f586ff1a3 Merge pull request #313 from sveltejs/gh-312
Skip layout components where none is provided
2018-07-23 16:37:30 -04:00
Rich Harris
731d4f535c skip layout components where none is provided - fixes #312 2018-07-23 16:31:00 -04:00
Rich Harris
f8c731ca21 failing tests for #312 2018-07-23 14:31:11 -04:00
Rich Harris
39eb3be01e -> v0.15.1 2018-07-22 21:25:33 -04:00
Rich Harris
d0bb728e25 -> v0.15.0 2018-07-22 21:04:03 -04:00
Rich Harris
58de0f9c99 Nested routes
Fixes #262
2018-07-22 21:00:37 -04:00
Rich Harris
b75ae7ba96 -> v0.14.2 2018-07-14 21:05:39 -04:00
Rich Harris
091e38082e Merge pull request #307 from sveltejs/unsafe-replacements
prevent unsafe replacements of preloaded data etc
2018-07-15 02:05:20 +01:00
Rich Harris
74acf93c7a prevent unsafe replacements of preloaded data etc 2018-07-14 20:56:05 -04:00
Rich Harris
0e3775397f Merge pull request #306 from sveltejs/refactor-route-generation
Refactor route generation
2018-07-05 08:25:59 -04:00
Rich Harris
8dc52a04e4 split pages and server routes into separate arrays 2018-07-05 08:14:07 -04:00
Rich Harris
008b607c01 generate pages and server routes separately 2018-07-04 10:53:41 -04:00
Rich Harris
d01a407137 -> v0.14.1 2018-07-02 10:24:54 -04:00
Rich Harris
c0c717d9ec Merge pull request #283 from elcobvg/feature/route-regexp
Feature: use regexp in routes
2018-07-02 10:23:30 -04:00
Elco Brouwer von Gonzenbach
4f011bfc37 Convert whitespace to tabs. Add some unit tests for create routes 2018-07-02 11:52:17 +07:00
Elco Brouwer von Gonzenbach
6c4ab32cf0 Sync update to sapper 0.14.0 2018-06-29 06:57:01 +07:00
Elco Brouwer von Gonzenbach
0e2c2ca101 Correct errors in param pattern and matching patterns 2018-06-13 10:00:39 +07:00
Elco Brouwer von Gonzenbach
8015be8069 Correct spacing 2018-06-12 09:15:54 +07:00
Elco Brouwer von Gonzenbach
e39ad59589 Add regexp option to routes 2018-06-12 09:11:54 +07:00
340 changed files with 15586 additions and 3563 deletions

6
.gitignore vendored
View File

@@ -4,10 +4,12 @@ yarn-error.log
node_modules
cypress/screenshots
test/app/.sapper
test/app/app/manifest
test/app/src/manifest
__sapper__
test/app/export
test/app/build
sapper
runtime.js
dist
!rollup.config.js
!rollup.config.js
templates/*.js

View File

@@ -18,4 +18,4 @@ addons:
install:
- export DISPLAY=':99.0'
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
- npm install
- npm ci || npm i

View File

@@ -1,5 +1,266 @@
# sapper changelog
## 0.25.0
* Force refresh on `goto(current_url)` ([#484](https://github.com/sveltejs/sapper/pull/484))
* Fix preloading navigation bug ([#532](https://github.com/sveltejs/sapper/issues/532))
* Don't mutate opts.headers ([#528](https://github.com/sveltejs/sapper/issues/528))
* Don't crawl hundreds of pages simultaneously ([#369](https://github.com/sveltejs/sapper/pull/369))
## 0.24.3
* Add service-worker-index.html shell file for offline support ([#422](https://github.com/sveltejs/sapper/issues/422))
* Don't cache .map files ([#534](https://github.com/sveltejs/sapper/issues/534))
## 0.24.2
* Support Rollup 1.0 ([#541](https://github.com/sveltejs/sapper/pull/541))
## 0.24.1
* Include CSS chunks in webpack build info to avoid duplication ([#529](https://github.com/sveltejs/sapper/pull/529))
* Fix preload `as` for styles ([#530](https://github.com/sveltejs/sapper/pull/530))
## 0.24.0
* Handle external URLs in `this.redirect` ([#490](https://github.com/sveltejs/sapper/issues/490))
* Strip leading `/` from basepath ([#495](https://github.com/sveltejs/sapper/issues/495))
* Treat duplicate query string parameters as arrays ([#497](https://github.com/sveltejs/sapper/issues/497))
* Don't buffer `stdout` and `stderr` ([#305](https://github.com/sveltejs/sapper/issues/305))
* Posixify `build_dir` ([#498](https://github.com/sveltejs/sapper/pull/498))
* Use `page[XY]Offset` instead of `scroll[XY]` ([#480](https://github.com/sveltejs/sapper/issues/480))
## 0.23.5
* Include lazily-imported CSS in main CSS chunk ([#492](https://github.com/sveltejs/sapper/pull/492))
* Make search param decoding spec-compliant ([#493](https://github.com/sveltejs/sapper/pull/493))
* Handle async route errors ([#488](https://github.com/sveltejs/sapper/pull/488))
## 0.23.4
* Ignore empty anchors when exporting ([#491](https://github.com/sveltejs/sapper/pull/491))
## 0.23.3
* Clear `error` and `status` on successful render ([#477](https://github.com/sveltejs/sapper/pull/477))
## 0.23.2
* Fix entry point CSS ([#471](https://github.com/sveltejs/sapper/pull/471))
## 0.23.1
* Scroll to deeplink that matches current URL ([#472](https://github.com/sveltejs/sapper/pull/472))
* Scroll to deeplink on another page ([#341](https://github.com/sveltejs/sapper/issues/341))
## 0.23.0
* Overhaul internal APIs ([#468](https://github.com/sveltejs/sapper/pull/468))
* Remove unused `sapper start` and `sapper upgrade` ([#468](https://github.com/sveltejs/sapper/pull/468))
* Remove magic environment variables ([#469](https://github.com/sveltejs/sapper/pull/469))
* Preserve SSI comments ([#470](https://github.com/sveltejs/sapper/pull/470))
## 0.22.10
* Handle `sapper-noscroll` attribute on `<a>` elements ([#376](https://github.com/sveltejs/sapper/issues/376))
* Fix CSS paths when using a base path ([#466](https://github.com/sveltejs/sapper/pull/466))
## 0.22.9
* Fix legacy builds ([#462](https://github.com/sveltejs/sapper/pull/462))
## 0.22.8
* Ensure CSS placeholders are overwritten ([#462](https://github.com/sveltejs/sapper/pull/462))
## 0.22.7
* Fix cookies ([#460](https://github.com/sveltejs/sapper/pull/460))
## 0.22.6
* Normalise chunk filenames on Windows ([#456](https://github.com/sveltejs/sapper/pull/456))
* Load modules with credentials ([#458](https://github.com/sveltejs/sapper/pull/458))
## 0.22.5
* Fix `sapper dev`. Oops.
## 0.22.4
* Ensure launcher does not overwrite a module ([#455](https://github.com/sveltejs/sapper/pull/455))
## 0.22.3
* Prevent server from accidentally importing dev client
## 0.22.2
* Make paths in generated code relative to project
## 0.22.1
* Fix `pkg.files`
## 0.22.0
* Move generated files into `__sapper__` ([#453](https://github.com/sveltejs/sapper/pull/453))
* Change default build and export directories to `__sapper__/build` and `__sapper__/export` ([#453](https://github.com/sveltejs/sapper/pull/453))
## 0.21.1
* Read template from build directory in production
## 0.21.0
* Change project folder structure ([#432](https://github.com/sveltejs/sapper/issues/432))
* Escape filenames ([#446](https://github.com/sveltejs/sapper/pull/446/))
## 0.20.4
* Fix legacy build CSS ([#439](https://github.com/sveltejs/sapper/issues/439))
* Enable debugging in Chrome and VSCode ([#435](https://github.com/sveltejs/sapper/issues/435))
## 0.20.3
* Inject `nonce` attribute if `res.locals.nonce` is present ([#424](https://github.com/sveltejs/sapper/pull/424))
* Prevent service worker caching ([#428](https://github.com/sveltejs/sapper/pull/428))
* Consistent caching for HTML responses ([#429](https://github.com/sveltejs/sapper/pull/429))
## 0.20.2
* Add `immutable` cache control header for hashed assets ([#425](https://github.com/sveltejs/sapper/pull/425))
* Handle value-less query string params ([#426](https://github.com/sveltejs/sapper/issues/426))
## 0.20.1
* Update shimport
## 0.20.0
* Decode `req.params` and `req.query` ([#417](https://github.com/sveltejs/sapper/issues/417))
* Decode URLs before writing files in `sapper export` ([#414](https://github.com/sveltejs/sapper/pull/414))
* Generate server sourcemaps for Rollup apps in dev mode ([#418](https://github.com/sveltejs/sapper/pull/418))
## 0.19.3
* Better unicode route handling ([#347](https://github.com/sveltejs/sapper/issues/347))
## 0.19.2
* Ignore editor tmp files ([#220](https://github.com/sveltejs/sapper/issues/220))
* Ignore clicks an `<a>` element without `href` ([#235](https://github.com/sveltejs/sapper/issues/235))
* Allow routes that are reserved JavaScript words ([#315](https://github.com/sveltejs/sapper/issues/315))
* Print out webpack errors ([#403](https://github.com/sveltejs/sapper/issues/403))
## 0.19.1
* Don't include local origin in export redirects ([#409](https://github.com/sveltejs/sapper/pull/409))
## 0.19.0
* Extract styles out of JS into .css files, for Rollup apps ([#388](https://github.com/sveltejs/sapper/issues/388))
* Fix `prefetchRoutes` ([#380](https://github.com/sveltejs/sapper/issues/380))
## 0.18.7
* Support differential bundling for Rollup apps via a `--legacy` flag ([#280](https://github.com/sveltejs/sapper/issues/280))
## 0.18.6
* Bundle missing dependency
## 0.18.5
* Bugfix
## 0.18.4
* Handle non-Sapper responses when exporting ([#382](https://github.com/sveltejs/sapper/issues/392))
* Add `--dev-port` flag to `sapper dev` ([#381](https://github.com/sveltejs/sapper/issues/381))
## 0.18.3
* Fix service worker Rollup build config
## 0.18.2
* Update `pkg.files`
## 0.18.1
* Add live reloading ([#385](https://github.com/sveltejs/sapper/issues/385))
## 0.18.0
* Rollup support ([#379](https://github.com/sveltejs/sapper/pull/379))
* Fail `export` if a page times out (configurable with `--timeout`) ([#378](https://github.com/sveltejs/sapper/pull/378))
## 0.17.1
* Print which file is causing build errors/warnings ([#371](https://github.com/sveltejs/sapper/pull/371))
## 0.17.0
* Use `cheap-watch` instead of `chokidar` ([#364](https://github.com/sveltejs/sapper/issues/364))
## 0.16.1
* Fix file watching regression in previous version
## 0.16.0
* Slim down installed package ([#363](https://github.com/sveltejs/sapper/pull/363))
## 0.15.8
* Only set `preloading: true` on navigation, not prefetch ([#352](https://github.com/sveltejs/sapper/issues/352))
* Provide fallback for missing preload errors ([#361](https://github.com/sveltejs/sapper/pull/361))
## 0.15.7
* Strip leading slash from redirects ([#291](https://github.com/sveltejs/sapper/issues/291))
* Pass `(req, res)` to store getter ([#344](https://github.com/sveltejs/sapper/issues/344))
## 0.15.6
* Fix exporting with custom basepath ([#342](https://github.com/sveltejs/sapper/pull/342))
## 0.15.5
* Faster `export` with more explanatory output ([#335](https://github.com/sveltejs/sapper/pull/335))
* Only blur `activeElement` if it exists ([#332](https://github.com/sveltejs/sapper/issues/332))
* Don't emit `client_info.json` or `server_info.json` ([#318](https://github.com/sveltejs/sapper/issues/318))
## 0.15.4
* Add `ignore` option ([#326](https://github.com/sveltejs/sapper/pull/326))
## 0.15.3
* Crawl pages in parallel when exporting ([#329](https://github.com/sveltejs/sapper/pull/329))
* Don't minify inline JS when exporting ([#328](https://github.com/sveltejs/sapper/pull/328))
## 0.15.2
* Collapse component chains where no intermediate layout component is specified ([#312](https://github.com/sveltejs/sapper/issues/312))
## 0.15.1
* Prevent confusing error when no root layout is specified
## 0.15.0
* Nested routes (consult [migration guide](https://sapper.svelte.technology/guide#0-14-to-0-15) and docs on [layouts](https://sapper.svelte.technology/guide#layouts)) ([#262](https://github.com/sveltejs/sapper/issues/262))
## 0.14.2
* Prevent unsafe replacements ([#307](https://github.com/sveltejs/sapper/pull/307))
## 0.14.1
* Route parameters can be qualified with regex characters ([#283](https://github.com/sveltejs/sapper/pull/283))
## 0.14.0
* `4xx.html` and `5xx.html` are replaced with `_error.html` ([#209](https://github.com/sveltejs/sapper/issues/209))

View File

@@ -31,6 +31,44 @@ npm run build
npm start
```
## Development
Pull requests are encouraged and always welcome. [Pick an issue](https://github.com/sveltejs/sapper/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) and help us out!
To install and work on Sapper locally:
```bash
git clone git@github.com:sveltejs/sapper.git
cd sapper
npm install
npm run dev
```
### Linking to a Live Project
You can make changes locally to Sapper and test it against a local Sapper project. For a quick project that takes almost no setup, use the default [sapper-template](https://github.com/sveltejs/sapper-template) project. Instruction on setup are found in that project repository.
To link Sapper to your project, from the root of your local Sapper git checkout:
```bash
cd sapper
npm link
```
Then, to link from `sapper-template` (or any other given project):
```bash
cd sapper-template
npm link sapper
```
You should be good to test changes locally.
### Running Tests
```bash
npm run test
```
## License

2
api.js
View File

@@ -1 +1 @@
module.exports = require('./dist/api.ts.js');
module.exports = require('./dist/api.js');

View File

@@ -10,11 +10,11 @@ build: off
environment:
matrix:
# node.js
- nodejs_version: stable
- nodejs_version: 10.5
install:
- ps: Install-Product node $env:nodejs_version
- npm install
- npm ci
test_script:
- node --version && npm --version

1
config/rollup.js Normal file
View File

@@ -0,0 +1 @@
module.exports = require('../dist/rollup.js');

1
config/webpack.js Normal file
View File

@@ -0,0 +1 @@
module.exports = require('../dist/webpack.js');

View File

@@ -1,9 +0,0 @@
{
"baseUrl": "http://localhost:3000",
"videoRecording": false,
"fixturesFolder": "test/cypress/fixtures",
"integrationFolder": "test/cypress/integration",
"pluginsFile": false,
"screenshotsFolder": "test/cypress/screenshots",
"supportFile": "test/cypress/support/index.js"
}

1
index.js Normal file
View File

@@ -0,0 +1 @@
throw new Error(`As of Sapper 0.22, you should not import 'sapper' directly. See https://sapper.svelte.technology/guide#0-21-to-0-22 for more information`);

View File

@@ -1,4 +1,5 @@
--require source-map-support/register
--require ts-node/register
--recursive
test/unit/**/*.js
test/common/test.js
test/unit/*/*.ts
test/apps/*/test.ts

6702
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,76 +1,79 @@
{
"name": "sapper",
"version": "0.14.0",
"version": "0.25.0",
"description": "Military-grade apps, engineered by Svelte",
"main": "dist/middleware.ts.js",
"bin": {
"sapper": "./sapper"
},
"files": [
"*.js",
"*.ts.js",
"runtime",
"webpack",
"config",
"sapper",
"dist"
"dist/*.js",
"templates/*.js"
],
"directories": {
"test": "test"
},
"dependencies": {
"ansi-colors": "^2.0.1",
"cheerio": "^1.0.0-rc.2",
"chokidar": "^2.0.3",
"cookie": "^0.3.1",
"devalue": "^1.0.1",
"glob": "^7.1.2",
"html-minifier": "^3.5.16",
"mkdirp": "^0.5.1",
"node-fetch": "^2.1.1",
"port-authority": "^1.0.2",
"pretty-bytes": "^5.0.0",
"pretty-ms": "^3.1.0",
"require-relative": "^0.8.7",
"rimraf": "^2.6.2",
"sade": "^1.4.1",
"sander": "^0.6.0",
"source-map-support": "^0.5.6",
"tslib": "^1.9.1",
"url-parse": "^1.2.0",
"webpack-format-messages": "^2.0.1"
"html-minifier": "^3.5.20",
"shimport": "0.0.11",
"source-map-support": "^0.5.9",
"sourcemap-codec": "^1.4.3",
"string-hash": "^1.1.3",
"tslib": "^1.9.3"
},
"devDependencies": {
"@types/glob": "^5.0.34",
"@types/mkdirp": "^0.5.2",
"@types/mocha": "^5.2.5",
"@types/node": "^10.12.0",
"@types/puppeteer": "^1.9.0",
"@types/rimraf": "^2.0.2",
"compression": "^1.7.1",
"eslint": "^4.13.1",
"eslint-plugin-import": "^2.12.0",
"express": "^4.16.3",
"agadoo": "^1.0.1",
"cheap-watch": "^1.0.0",
"cookie": "^0.3.1",
"devalue": "^1.0.4",
"eslint": "^5.7.0",
"eslint-plugin-import": "^2.14.0",
"kleur": "^2.0.2",
"mkdirp": "^0.5.1",
"mocha": "^5.2.0",
"nightmare": "^3.0.0",
"npm-run-all": "^4.1.3",
"polka": "^0.4.0",
"rollup": "^0.59.2",
"rollup-plugin-commonjs": "^9.1.3",
"rollup-plugin-json": "^3.0.0",
"node-fetch": "^2.2.0",
"npm-run-all": "^4.1.5",
"polka": "^0.5.1",
"port-authority": "^1.0.5",
"pretty-bytes": "^5.1.0",
"puppeteer": "^1.9.0",
"require-relative": "^0.8.7",
"rimraf": "^2.6.2",
"rollup": "^0.66.6",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-json": "^3.1.0",
"rollup-plugin-node-resolve": "^3.4.0",
"rollup-plugin-replace": "^2.1.0",
"rollup-plugin-string": "^2.0.2",
"rollup-plugin-typescript": "^0.8.1",
"serve-static": "^1.13.2",
"svelte": "^2.6.3",
"svelte-loader": "^2.9.0",
"typescript": "^2.8.3",
"walk-sync": "^0.3.2",
"webpack": "^4.8.3"
"rollup-plugin-svelte": "^4.3.2",
"rollup-plugin-typescript": "^1.0.0",
"sade": "^1.4.1",
"sander": "^0.6.0",
"sirv": "^0.2.2",
"svelte": "^2.13.5",
"svelte-loader": "^2.11.0",
"ts-node": "^7.0.1",
"typescript": "^3.1.3",
"webpack": "^4.20.2",
"webpack-format-messages": "^2.0.3",
"yootils": "0.0.14"
},
"scripts": {
"cy:open": "cypress open",
"test": "mocha --opts mocha.opts",
"pretest": "npm run build",
"build": "rollup -c",
"build": "rm -rf dist && rollup -c",
"prepare": "npm run build",
"dev": "rollup -cw",
"prepublishOnly": "npm test",
"update_mime_types": "curl http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types | grep -e \"^[^#]\" > src/middleware/mime-types.md"
"update_mime_types": "curl http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types | grep -e \"^[^#]\" > templates/src/server/middleware/mime-types.md"
},
"repository": "https://github.com/sveltejs/sapper",
"keywords": [

View File

@@ -1,8 +1,10 @@
import typescript from 'rollup-plugin-typescript';
import string from 'rollup-plugin-string';
import json from 'rollup-plugin-json';
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import pkg from './package.json';
import { builtinModules } from 'module';
const external = [].concat(
Object.keys(pkg.dependencies),
@@ -10,27 +12,39 @@ const external = [].concat(
'sapper/core.js'
);
export default [
{
input: `src/runtime/index.ts`,
function template(kind, external, target) {
return {
input: `templates/src/${kind}/index.ts`,
output: {
file: `runtime.js`,
file: `templates/${kind}.js`,
format: 'es'
},
external,
plugins: [
resolve(),
commonjs(),
string({
include: '**/*.md'
}),
typescript({
typescript: require('typescript')
typescript: require('typescript'),
target
})
]
},
};
}
export default [
template('client', ['__ROOT__', '__ERROR__'], 'ES2017'),
template('server', builtinModules, 'ES2015'),
{
input: [
`src/api.ts`,
`src/cli.ts`,
`src/core.ts`,
`src/middleware.ts`,
`src/webpack.ts`
`src/config/rollup.ts`,
`src/config/webpack.ts`
],
output: {
dir: 'dist',
@@ -39,16 +53,13 @@ export default [
},
external,
plugins: [
string({
include: '**/*.md'
}),
json(),
resolve(),
commonjs(),
typescript({
typescript: require('typescript')
})
],
experimentalCodeSplitting: true,
experimentalDynamicImport: true
experimentalCodeSplitting: true
}
];

View File

@@ -1 +0,0 @@
This directory exists for legacy reasons and should be deleted before releasing version 1.

View File

@@ -1,2 +0,0 @@
console.error('sapper/runtime/app.js has been deprecated in favour of sapper/runtime.js');
export * from '../runtime.js';

2
sapper
View File

@@ -1,2 +1,2 @@
#!/usr/bin/env node
require('./dist/cli.ts.js');
require('./dist/cli.js');

View File

@@ -1,6 +1,8 @@
let source;
function check() {
if (typeof module === 'undefined') return;
if (module.hot.status() === 'idle') {
module.hot.check(true).then(modules => {
console.log(`[SAPPER] applied HMR update`);

View File

@@ -1,6 +1,4 @@
import { dev } from './api/dev';
import { build } from './api/build';
import { exporter } from './api/export';
import { find_page } from './api/find_page';
export { dev, build, exporter, find_page };
export { dev } from './api/dev';
export { build } from './api/build';
export { export } from './api/export';
export { find_page } from './api/find_page';

View File

@@ -2,41 +2,58 @@ import * as fs from 'fs';
import * as path from 'path';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import { EventEmitter } from 'events';
import { minify_html } from './utils/minify_html';
import { create_compilers, create_main_manifests, create_routes, create_serviceworker_manifest } from '../core'
import { locations } from '../config';
import * as events from './interfaces';
import minify_html from './utils/minify_html';
import { create_compilers, create_main_manifests, create_manifest_data, create_serviceworker_manifest } from '../core';
import { copy_shimport } from './utils/copy_shimport';
import read_template from '../core/read_template';
import { CompileResult } from '../core/create_compilers/interfaces';
import { noop } from './utils/noop';
import validate_bundler from './utils/validate_bundler';
export function build(opts: {}) {
const emitter = new EventEmitter();
type Opts = {
cwd?: string;
src?: string;
routes?: string;
dest?: string;
output?: string;
static?: string;
legacy?: boolean;
bundler?: 'rollup' | 'webpack';
oncompile?: ({ type, result }: { type: string, result: CompileResult }) => void;
};
execute(emitter, opts).then(
() => {
emitter.emit('done', <events.DoneEvent>{}); // TODO do we need to pass back any info?
},
error => {
emitter.emit('error', <events.ErrorEvent>{
error
});
}
);
export async function build({
cwd,
src = 'src',
routes = 'src/routes',
output = '__sapper__',
static: static_files = 'static',
dest = '__sapper__/build',
return emitter;
}
bundler,
legacy = false,
oncompile = noop
}: Opts = {}) {
bundler = validate_bundler(bundler);
cwd = path.resolve(cwd);
src = path.resolve(cwd, src);
dest = path.resolve(cwd, dest);
routes = path.resolve(cwd, routes);
output = path.resolve(cwd, output);
static_files = path.resolve(cwd, static_files);
if (legacy && bundler === 'webpack') {
throw new Error(`Legacy builds are not supported for projects using webpack`);
}
async function execute(emitter: EventEmitter, {
dest = 'build',
app = 'app',
webpack = 'webpack',
routes = 'routes'
} = {}) {
mkdirp.sync(dest);
rimraf.sync(path.join(dest, '**/*'));
mkdirp.sync(`${dest}/client`);
copy_shimport(dest);
// minify app/template.html
// minify src/template.html
// TODO compile this to a function? could be quicker than str.replace(...).replace(...).replace(...)
const template = fs.readFileSync(`${app}/template.html`, 'utf-8');
const template = read_template(src);
// remove this in a future version
if (template.indexOf('%sapper.base%') === -1) {
@@ -47,65 +64,74 @@ async function execute(emitter: EventEmitter, {
fs.writeFileSync(`${dest}/template.html`, minify_html(template));
const route_objects = create_routes();
const manifest_data = create_manifest_data(routes);
// create app/manifest/client.js and app/manifest/server.js
create_main_manifests({ routes: route_objects });
const { client, server, serviceworker } = create_compilers({ webpack });
const client_stats = await compile(client);
emitter.emit('build', <events.BuildEvent>{
type: 'client',
// TODO duration/warnings
webpack_stats: client_stats
// create src/manifest/client.js and src/manifest/server.js
create_main_manifests({
bundler,
manifest_data,
cwd,
src,
dest,
routes,
output,
dev: false
});
const client_info = client_stats.toJson();
fs.writeFileSync(path.join(dest, 'client_info.json'), JSON.stringify(client_info));
fs.writeFileSync(path.join(dest, 'client_assets.json'), JSON.stringify(client_info.assetsByChunkName));
const { client, server, serviceworker } = await create_compilers(bundler, cwd, src, dest, true);
const server_stats = await compile(server);
emitter.emit('build', <events.BuildEvent>{
const client_result = await client.compile();
oncompile({
type: 'client',
result: client_result
});
const build_info = client_result.to_json(manifest_data, { src, routes, dest });
if (legacy) {
process.env.SAPPER_LEGACY_BUILD = 'true';
const { client } = await create_compilers(bundler, cwd, src, dest, true);
const client_result = await client.compile();
oncompile({
type: 'client (legacy)',
result: client_result
});
client_result.to_json(manifest_data, { src, routes, dest });
build_info.legacy_assets = client_result.assets;
delete process.env.SAPPER_LEGACY_BUILD;
}
fs.writeFileSync(path.join(dest, 'build.json'), JSON.stringify(build_info));
const server_stats = await server.compile();
oncompile({
type: 'server',
// TODO duration/warnings
webpack_stats: server_stats
result: server_stats
});
let serviceworker_stats;
if (serviceworker) {
const client_files = client_result.chunks
.filter(chunk => !chunk.file.endsWith('.map')) // SW does not need to cache sourcemap files
.map(chunk => `client/${chunk.file}`);
create_serviceworker_manifest({
routes: route_objects,
client_files: client_stats.toJson().assets.map((chunk: { name: string }) => `client/${chunk.name}`)
manifest_data,
output,
client_files,
static_files
});
serviceworker_stats = await compile(serviceworker);
serviceworker_stats = await serviceworker.compile();
emitter.emit('build', <events.BuildEvent>{
oncompile({
type: 'serviceworker',
// TODO duration/warnings
webpack_stats: serviceworker_stats
result: serviceworker_stats
});
}
}
function compile(compiler: any) {
return new Promise((fulfil, reject) => {
compiler.run((err: Error, stats: any) => {
if (err) {
reject(err);
process.exit(1);
}
if (stats.hasErrors()) {
console.error(stats.toString({ colors: true }));
reject(new Error(`Encountered errors while building app`));
}
else {
fulfil(stats);
}
});
});
}
}

View File

@@ -5,33 +5,59 @@ import * as child_process from 'child_process';
import * as ports from 'port-authority';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import format_messages from 'webpack-format-messages';
import { locations } from '../config';
import { EventEmitter } from 'events';
import { create_routes, create_main_manifests, create_compilers, create_serviceworker_manifest } from '../core';
import * as events from './interfaces';
import { create_manifest_data, create_main_manifests, create_compilers, create_serviceworker_manifest } from '../core';
import { Compiler, Compilers } from '../core/create_compilers';
import { CompileResult } from '../core/create_compilers/interfaces';
import Deferred from './utils/Deferred';
import validate_bundler from './utils/validate_bundler';
import { copy_shimport } from './utils/copy_shimport';
import { ManifestData, FatalEvent, ErrorEvent, ReadyEvent, InvalidEvent } from '../interfaces';
import read_template from '../core/read_template';
import { noop } from './utils/noop';
export function dev(opts) {
type Opts = {
cwd?: string,
src?: string,
dest?: string,
routes?: string,
output?: string,
static?: string,
'dev-port'?: number,
live?: boolean,
hot?: boolean,
'devtools-port'?: number,
bundler?: 'rollup' | 'webpack',
port?: number
};
export function dev(opts: Opts) {
return new Watcher(opts);
}
class Watcher extends EventEmitter {
bundler: 'rollup' | 'webpack';
dirs: {
app: string;
cwd: string;
src: string;
dest: string;
routes: string;
webpack: string;
output: string;
static: string;
}
port: number;
closed: boolean;
dev_port: number;
live: boolean;
hot: boolean;
devtools_port: number;
dev_server: DevServer;
proc: child_process.ChildProcess;
filewatchers: Array<{ close: () => void }>;
deferreds: {
client: Deferred;
server: Deferred;
};
deferred: Deferred;
crashed: boolean;
restarting: boolean;
@@ -43,24 +69,42 @@ class Watcher extends EventEmitter {
}
constructor({
app = locations.app(),
dest = locations.dest(),
routes = locations.routes(),
webpack = 'webpack',
cwd = '.',
src = 'src',
routes = 'src/routes',
output = '__sapper__',
static: static_files = 'static',
dest = '__sapper__/dev',
'dev-port': dev_port,
live,
hot,
'devtools-port': devtools_port,
bundler,
port = +process.env.PORT
}: {
app: string,
dest: string,
routes: string,
webpack: string,
port: number
}) {
}: Opts) {
super();
this.dirs = { app, dest, routes, webpack };
cwd = path.resolve(cwd);
this.bundler = validate_bundler(bundler);
this.dirs = {
cwd,
src: path.resolve(cwd, src),
dest: path.resolve(cwd, dest),
routes: path.resolve(cwd, routes),
output: path.resolve(cwd, output),
static: path.resolve(cwd, static_files)
};
this.port = port;
this.closed = false;
this.dev_port = dev_port;
this.live = live;
this.hot = hot;
this.devtools_port = devtools_port;
this.filewatchers = [];
this.current_build = {
@@ -71,7 +115,7 @@ class Watcher extends EventEmitter {
};
// remove this in a future version
const template = fs.readFileSync(path.join(app, 'template.html'), 'utf-8');
const template = read_template(src);
if (template.indexOf('%sapper.base%') === -1) {
const error = new Error(`As of Sapper v0.10, your template.html file must include %sapper.base% in the <head>`);
error.code = `missing-sapper-base`;
@@ -90,7 +134,7 @@ class Watcher extends EventEmitter {
async init() {
if (this.port) {
if (!await ports.check(this.port)) {
this.emit('fatal', <events.FatalEvent>{
this.emit('fatal', <FatalEvent>{
message: `Port ${this.port} is unavailable`
});
return;
@@ -99,44 +143,80 @@ class Watcher extends EventEmitter {
this.port = await ports.find(3000);
}
const { dest } = this.dirs;
const { cwd, src, dest, routes, output, static: static_files } = this.dirs;
rimraf.sync(dest);
mkdirp.sync(dest);
mkdirp.sync(`${dest}/client`);
if (this.bundler === 'rollup') copy_shimport(dest);
const dev_port = await ports.find(10000);
if (!this.dev_port) this.dev_port = await ports.find(10000);
const routes = create_routes();
create_main_manifests({ routes, dev_port });
// Chrome looks for debugging targets on ports 9222 and 9229 by default
if (!this.devtools_port) this.devtools_port = await ports.find(9222);
this.dev_server = new DevServer(dev_port);
let manifest_data: ManifestData;
try {
manifest_data = create_manifest_data(routes);
create_main_manifests({
bundler: this.bundler,
manifest_data,
dev: true,
dev_port: this.dev_port,
cwd, src, dest, routes, output
});
} catch (err) {
this.emit('fatal', <FatalEvent>{
message: err.message
});
return;
}
this.dev_server = new DevServer(this.dev_port);
this.filewatchers.push(
watch_files(locations.routes(), ['add', 'unlink'], () => {
const routes = create_routes();
create_main_manifests({ routes, dev_port });
}),
watch_dir(
routes,
({ path: file, stats }) => {
if (stats.isDirectory()) {
return path.basename(file)[0] !== '_';
}
return true;
},
() => {
try {
const new_manifest_data = create_manifest_data(routes);
create_main_manifests({
bundler: this.bundler,
manifest_data, // TODO is this right? not new_manifest_data?
dev: true,
dev_port: this.dev_port,
cwd, src, dest, routes, output
});
watch_files(`${locations.app()}/template.html`, ['change'], () => {
manifest_data = new_manifest_data;
} catch (err) {
this.emit('error', <ErrorEvent>{
message: err.message
});
}
}
),
fs.watch(`${src}/template.html`, () => {
this.dev_server.send({
action: 'reload'
});
})
);
this.deferreds = {
server: new Deferred(),
client: new Deferred()
};
let deferred = new Deferred();
// TODO watch the configs themselves?
const compilers = create_compilers({ webpack: this.dirs.webpack });
let log = '';
const compilers: Compilers = await create_compilers(this.bundler, cwd, src, dest, false);
const emitFatal = () => {
this.emit('fatal', <events.FatalEvent>{
message: `Server crashed`,
log
this.emit('fatal', <FatalEvent>{
message: `Server crashed`
});
this.crashed = true;
@@ -148,36 +228,35 @@ class Watcher extends EventEmitter {
invalid: filename => {
this.restart(filename, 'server');
this.deferreds.server = new Deferred();
},
result: info => {
fs.writeFileSync(path.join(dest, 'server_info.json'), JSON.stringify(info, null, ' '));
this.deferreds.client.promise.then(() => {
handle_result: (result: CompileResult) => {
deferred.promise.then(() => {
const restart = () => {
log = '';
this.crashed = false;
ports.wait(this.port)
.then((() => {
this.emit('ready', <events.ReadyEvent>{
this.emit('ready', <ReadyEvent>{
port: this.port,
process: this.proc
});
this.deferreds.server.fulfil();
this.dev_server.send({
status: 'completed'
});
if (this.hot && this.bundler === 'webpack') {
this.dev_server.send({
status: 'completed'
});
} else {
this.dev_server.send({
action: 'reload'
});
}
}))
.catch(err => {
if (this.crashed) return;
this.emit('fatal', <events.FatalEvent>{
message: `Server is not listening on port ${this.port}`,
log
this.emit('fatal', <FatalEvent>{
message: `Server is not listening on port ${this.port}`
});
});
};
@@ -190,21 +269,28 @@ class Watcher extends EventEmitter {
restart();
}
this.proc = child_process.fork(`${dest}/server.js`, [], {
// we need to give the child process its own DevTools port,
// otherwise Node will try to use the parent's (and fail)
const debugArgRegex = /--inspect(?:-brk|-port)?|--debug-port/;
const execArgv = process.execArgv.slice();
if (execArgv.some((arg: string) => !!arg.match(debugArgRegex))) {
execArgv.push(`--inspect-port=${this.devtools_port}`);
}
this.proc = child_process.fork(`${dest}/server/server.js`, [], {
cwd: process.cwd(),
env: Object.assign({
PORT: this.port
}, process.env),
stdio: ['ipc']
stdio: ['ipc'],
execArgv
});
this.proc.stdout.on('data', chunk => {
log += chunk;
this.emit('stdout', chunk);
});
this.proc.stderr.on('data', chunk => {
log += chunk;
this.emit('stderr', chunk);
});
@@ -221,32 +307,37 @@ class Watcher extends EventEmitter {
}
});
let first = true;
this.watch(compilers.client, {
name: 'client',
invalid: filename => {
this.restart(filename, 'client');
this.deferreds.client = new Deferred();
deferred = new Deferred();
// TODO we should delete old assets. due to a webpack bug
// i don't even begin to comprehend, this is apparently
// quite difficult
},
result: info => {
fs.writeFileSync(path.join(dest, 'client_info.json'), JSON.stringify(info));
fs.writeFileSync(path.join(dest, 'client_assets.json'), JSON.stringify(info.assetsByChunkName, null, ' '));
this.deferreds.client.fulfil();
handle_result: (result: CompileResult) => {
fs.writeFileSync(
path.join(dest, 'build.json'),
const client_files = info.assets.map((chunk: { name: string }) => `client/${chunk.name}`);
// TODO should be more explicit that to_json has effects
JSON.stringify(result.to_json(manifest_data, this.dirs), null, ' ')
);
const client_files = result.chunks.map(chunk => `client/${chunk.file}`);
create_serviceworker_manifest({
routes: create_routes(),
client_files
manifest_data,
output,
client_files,
static_files
});
deferred.fulfil();
// we need to wait a beat before watching the service
// worker, because of some webpack nonsense
setTimeout(watch_serviceworker, 100);
@@ -258,11 +349,7 @@ class Watcher extends EventEmitter {
watch_serviceworker = noop;
this.watch(compilers.serviceworker, {
name: 'service worker',
result: info => {
fs.writeFileSync(path.join(dest, 'serviceworker_info.json'), JSON.stringify(info, null, ' '));
}
name: 'service worker'
});
}
: noop;
@@ -272,7 +359,7 @@ class Watcher extends EventEmitter {
if (this.closed) return;
this.closed = true;
this.dev_server.close();
if (this.dev_server) this.dev_server.close();
if (this.proc) this.proc.kill();
this.filewatchers.forEach(watcher => {
@@ -295,7 +382,7 @@ class Watcher extends EventEmitter {
};
process.nextTick(() => {
this.emit('invalid', <events.InvalidEvent>{
this.emit('invalid', <InvalidEvent>{
changed: Array.from(this.current_build.changed),
invalid: {
server: this.current_build.rebuilding.has('server'),
@@ -309,95 +396,34 @@ class Watcher extends EventEmitter {
}
}
watch(compiler: any, { name, invalid = noop, result }: {
watch(compiler: Compiler, { name, invalid = noop, handle_result = noop }: {
name: string,
invalid?: (filename: string) => void;
result: (stats: any) => void;
handle_result?: (result: CompileResult) => void;
}) {
compiler.hooks.invalid.tap('sapper', (filename: string) => {
invalid(filename);
});
compiler.oninvalid(invalid);
compiler.watch({}, (err: Error, stats: any) => {
compiler.watch((err?: Error, result?: CompileResult) => {
if (err) {
this.emit('error', <events.ErrorEvent>{
this.emit('error', <ErrorEvent>{
type: name,
message: err.message
});
} else {
const messages = format_messages(stats);
const info = stats.toJson();
this.emit('build', {
type: name,
duration: info.time,
errors: messages.errors.map((message: string) => {
const duplicate = this.current_build.unique_errors.has(message);
this.current_build.unique_errors.add(message);
return mungeWebpackError(message, duplicate);
}),
warnings: messages.warnings.map((message: string) => {
const duplicate = this.current_build.unique_warnings.has(message);
this.current_build.unique_warnings.add(message);
return mungeWebpackError(message, duplicate);
}),
duration: result.duration,
errors: result.errors,
warnings: result.warnings
});
result(info);
handle_result(result);
}
});
}
}
const locPattern = /\((\d+):(\d+)\)$/;
function mungeWebpackError(message: string, duplicate: boolean) {
// TODO this is all a bit rube goldberg...
const lines = message.split('\n');
const file = lines.shift()
.replace('', '') // careful — there is a special character at the beginning of this string
.replace('', '')
.replace('./', '');
let line = null;
let column = null;
const match = locPattern.exec(lines[0]);
if (match) {
lines[0] = lines[0].replace(locPattern, '');
line = +match[1];
column = +match[2];
}
return {
file,
line,
column,
message: lines.join('\n'),
originalMessage: message,
duplicate
};
}
class Deferred {
promise: Promise<any>;
fulfil: (value?: any) => void;
reject: (error: Error) => void;
constructor() {
this.promise = new Promise((fulfil, reject) => {
this.fulfil = fulfil;
this.reject = reject;
});
}
}
const INTERVAL = 10000;
class DevServer {
@@ -450,22 +476,32 @@ class DevServer {
}
}
function noop() {}
function watch_dir(
dir: string,
filter: ({ path, stats }: { path: string, stats: fs.Stats }) => boolean,
callback: () => void
) {
let watch: any;
let closed = false;
function watch_files(pattern: string, events: string[], callback: () => void) {
const chokidar = require('chokidar');
import('cheap-watch').then(CheapWatch => {
if (closed) return;
const watcher = chokidar.watch(pattern, {
persistent: true,
ignoreInitial: true,
disableGlobbing: true
});
watch = new CheapWatch({ dir, filter, debounce: 50 });
events.forEach(event => {
watcher.on(event, callback);
watch.on('+', ({ isNew }: { isNew: boolean }) => {
if (isNew) callback();
});
watch.on('-', callback);
watch.init();
});
return {
close: () => watcher.close()
close: () => {
if (watch) watch.close();
closed = true;
}
};
}
}

View File

@@ -1,63 +1,83 @@
import * as child_process from 'child_process';
import * as path from 'path';
import * as sander from 'sander';
import cheerio from 'cheerio';
import URL from 'url-parse';
import * as url from 'url';
import fetch from 'node-fetch';
import * as yootils from 'yootils';
import * as ports from 'port-authority';
import { EventEmitter } from 'events';
import { minify_html } from './utils/minify_html';
import { locations } from '../config';
import * as events from './interfaces';
import clean_html from './utils/clean_html';
import minify_html from './utils/minify_html';
import Deferred from './utils/Deferred';
import { noop } from './utils/noop';
export function exporter(opts: {}) {
const emitter = new EventEmitter();
type Opts = {
build_dir?: string,
export_dir?: string,
cwd?: string,
static?: string,
basepath?: string,
timeout?: number | false,
oninfo?: ({ message }: { message: string }) => void;
onfile?: ({ file, size, status }: { file: string, size: number, status: number }) => void;
};
execute(emitter, opts).then(
() => {
emitter.emit('done', <events.DoneEvent>{}); // TODO do we need to pass back any info?
},
error => {
emitter.emit('error', <events.ErrorEvent>{
error
});
}
);
return emitter;
function resolve(from: string, to: string) {
return url.parse(url.resolve(from, to));
}
async function execute(emitter: EventEmitter, {
build = 'build',
dest = 'export',
basepath = ''
} = {}) {
const export_dir = path.join(dest, basepath);
type URL = url.UrlWithStringQuery;
export { _export as export };
async function _export({
cwd,
static: static_files = 'static',
build_dir = '__sapper__/build',
export_dir = '__sapper__/export',
basepath = '',
timeout = 5000,
oninfo = noop,
onfile = noop
}: Opts = {}) {
basepath = basepath.replace(/^\//, '')
cwd = path.resolve(cwd);
static_files = path.resolve(cwd, static_files);
build_dir = path.resolve(cwd, build_dir);
export_dir = path.resolve(cwd, export_dir, basepath);
// Prep output directory
sander.rimrafSync(export_dir);
sander.copydirSync('assets').to(export_dir);
sander.copydirSync(build, 'client').to(export_dir, 'client');
sander.copydirSync(static_files).to(export_dir);
sander.copydirSync(build_dir, 'client').to(export_dir, 'client');
if (sander.existsSync(build, 'service-worker.js')) {
sander.copyFileSync(build, 'service-worker.js').to(export_dir, 'service-worker.js');
if (sander.existsSync(build_dir, 'service-worker.js')) {
sander.copyFileSync(build_dir, 'service-worker.js').to(export_dir, 'service-worker.js');
}
if (sander.existsSync(build, 'service-worker.js.map')) {
sander.copyFileSync(build, 'service-worker.js.map').to(export_dir, 'service-worker.js.map');
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 protocol = 'http:';
const host = `localhost:${port}`;
const origin = `${protocol}//${host}`;
const proc = child_process.fork(path.resolve(`${build}/server.js`), [], {
cwd: process.cwd(),
const root = resolve(origin, basepath);
if (!root.href.endsWith('/')) root.href += '/';
oninfo({
message: `Crawling ${root.href}`
});
const proc = child_process.fork(path.resolve(`${build_dir}/server/server.js`), [], {
cwd,
env: Object.assign({
PORT: port,
NODE_ENV: 'production',
SAPPER_DEST: build,
SAPPER_EXPORT: 'true'
}, process.env)
});
@@ -65,67 +85,118 @@ async function execute(emitter: EventEmitter, {
const seen = new Set();
const saved = new Set();
proc.on('message', message => {
if (!message.__sapper__ || message.event !== 'file') return;
let file = new URL(message.url, origin).pathname.slice(1);
let { body } = message;
function save(path: string, status: number, type: string, body: string) {
const { pathname } = resolve(origin, path);
let file = decodeURIComponent(pathname.slice(1));
if (saved.has(file)) return;
saved.add(file);
const is_html = message.type === 'text/html';
const is_html = type === 'text/html';
if (is_html) {
file = file === '' ? 'index.html' : `${file}/index.html`;
if (pathname !== '/service-worker-index.html') {
file = file === '' ? 'index.html' : `${file}/index.html`;
}
body = minify_html(body);
}
emitter.emit('file', <events.FileEvent>{
onfile({
file,
size: body.length
size: body.length,
status
});
sander.writeFileSync(export_dir, file, body);
}
proc.on('message', message => {
if (!message.__sapper__ || message.event !== 'file') return;
save(message.url, message.status, message.type, message.body);
});
async function handle(url: URL) {
const r = await fetch(url.href);
let pathname = url.pathname;
if (pathname !== '/service-worker-index.html') {
pathname = pathname.replace(root.pathname, '') || '/'
}
if (seen.has(pathname)) return;
seen.add(pathname);
const timeout_deferred = new Deferred();
const the_timeout = setTimeout(() => {
timeout_deferred.reject(new Error(`Timed out waiting for ${url.href}`));
}, timeout);
const r = await Promise.race([
fetch(url.href, {
redirect: 'manual'
}),
timeout_deferred.promise
]);
clearTimeout(the_timeout); // prevent it hanging at the end
let type = r.headers.get('Content-Type');
let body = await r.text();
const range = ~~(r.status / 100);
if (range >= 4) {
emitter.emit('failure', <events.FailureEvent>{
status: r.status,
pathname: url.pathname
});
return;
}
if (range === 2) {
if (r.headers.get('Content-Type') === 'text/html') {
const body = await r.text();
const $ = cheerio.load(body);
const urls: URL[] = [];
if (type === 'text/html' && pathname !== '/service-worker-index.html') {
const cleaned = clean_html(body);
const base = new URL($('base').attr('href') || '/', url.href);
const q = yootils.queue(8);
let promise;
$('a[href]').each((i: number, $a) => {
const url = new URL($a.attribs.href, base.href);
const base_match = /<base ([\s\S]+?)>/m.exec(cleaned);
const base_href = base_match && get_href(base_match[1]);
const base = resolve(url.href, base_href);
if (url.origin === origin && !seen.has(url.pathname)) {
seen.add(url.pathname);
urls.push(url);
let match;
let pattern = /<a ([\s\S]+?)>/gm;
while (match = pattern.exec(cleaned)) {
const attrs = match[1];
const href = get_href(attrs);
if (href) {
const url = resolve(base.href, href);
if (url.protocol === protocol && url.host === host) {
promise = q.add(() => handle(url));
}
}
});
for (const url of urls) {
await handle(url);
}
await promise;
}
}
if (range === 3) {
const location = r.headers.get('Location');
type = 'text/html';
body = `<script>window.location.href = "${location.replace(origin, '')}"</script>`;
await handle(resolve(root.href, location));
}
save(pathname, r.status, type, body);
}
return ports.wait(port)
.then(() => handle(new URL(`/${basepath}`, origin))) // TODO all static routes
.then(() => proc.kill());
.then(() => handle(root))
.then(() => handle(resolve(root.href, 'service-worker-index.html')))
.then(() => proc.kill())
.catch(err => {
proc.kill();
throw err;
});
}
function get_href(attrs: string) {
const match = /href\s*=\s*(?:"(.*?)"|'(.+?)'|([^\s>]+))/.exec(attrs);
return match[1] || match[2] || match[3];
}

View File

@@ -1,16 +1,13 @@
import * as glob from 'glob';
import { locations } from '../config';
import { create_routes } from '../core';
import { create_manifest_data } from '../core';
export function find_page(pathname: string, files: string[] = glob.sync('**/*.*', { cwd: locations.routes(), dot: true, nodir: true })) {
const routes = create_routes({ files });
export function find_page(pathname: string, cwd = 'src/routes') {
const { pages } = create_manifest_data(cwd);
for (let i = 0; i < routes.length; i += 1) {
const route = routes[i];
for (let i = 0; i < pages.length; i += 1) {
const page = pages[i];
if (route.pattern.test(pathname)) {
const page = route.handlers.find(handler => handler.type === 'page');
if (page) return page.file;
if (page.pattern.test(pathname)) {
return page.parts[page.parts.length - 1].component.file;
}
}
}

View File

@@ -1,44 +0,0 @@
import * as child_process from 'child_process';
export type ReadyEvent = {
port: number;
process: child_process.ChildProcess;
};
export type ErrorEvent = {
type: string;
message: string;
};
export type FatalEvent = {
message: string;
log?: string;
};
export type InvalidEvent = {
changed: string[];
invalid: {
client: boolean;
server: boolean;
serviceworker: boolean;
}
};
export type BuildEvent = {
type: string;
errors: Array<{ message: string, duplicate: boolean }>;
warnings: Array<{ message: string, duplicate: boolean }>;
duration: number;
webpack_stats: any;
}
export type FileEvent = {
file: string;
size: number;
}
export type FailureEvent = {
}
export type DoneEvent = {}

12
src/api/utils/Deferred.ts Normal file
View File

@@ -0,0 +1,12 @@
export default class Deferred {
promise: Promise<any>;
fulfil: (value?: any) => void;
reject: (error: Error) => void;
constructor() {
this.promise = new Promise((fulfil, reject) => {
this.fulfil = fulfil;
this.reject = reject;
});
}
}

View File

@@ -0,0 +1,7 @@
export default function clean_html(html: string) {
return html
.replace(/<!\[CDATA\[[\s\S]*?\]\]>/gm, '')
.replace(/(<script[\s\S]*?>)[\s\S]*?<\/script>/gm, '$1</' + 'script>')
.replace(/(<style[\s\S]*?>)[\s\S]*?<\/style>/gm, '$1</' + 'style>')
.replace(/<!--[\s\S]*?-->/gm, '');
}

View File

@@ -0,0 +1,9 @@
import * as fs from 'fs';
export function copy_shimport(dest: string) {
const shimport_version = require('shimport/package.json').version;
fs.writeFileSync(
`${dest}/client/shimport@${shimport_version}.js`,
fs.readFileSync(require.resolve('shimport/index.js'))
);
}

View File

@@ -1,14 +1,15 @@
import { minify } from 'html-minifier';
export function minify_html(html: string) {
export default function minify_html(html: string) {
return minify(html, {
collapseBooleanAttributes: true,
collapseWhitespace: true,
conservativeCollapse: true,
decodeEntities: true,
html5: true,
ignoreCustomComments: [/^#/],
minifyCSS: true,
minifyJS: true,
minifyJS: false,
removeAttributeQuotes: true,
removeComments: true,
removeOptionalTags: true,

1
src/api/utils/noop.ts Normal file
View File

@@ -0,0 +1 @@
export function noop() {}

View File

@@ -0,0 +1,38 @@
import * as fs from 'fs';
export default function validate_bundler(bundler?: 'rollup' | 'webpack') {
if (!bundler) {
bundler = (
fs.existsSync('rollup.config.js') ? 'rollup' :
fs.existsSync('webpack.config.js') ? 'webpack' :
null
);
if (!bundler) {
// TODO remove in a future version
deprecate_dir('rollup');
deprecate_dir('webpack');
throw new Error(`Could not find rollup.config.js or webpack.config.js`);
}
}
if (bundler !== 'rollup' && bundler !== 'webpack') {
throw new Error(`'${bundler}' is not a valid option for --bundler — must be either 'rollup' or 'webpack'`);
}
return bundler;
}
function deprecate_dir(bundler: 'rollup' | 'webpack') {
try {
const stats = fs.statSync(bundler);
if (!stats.isDirectory()) return;
} catch (err) {
// do nothing
return;
}
// TODO link to docs, once those docs exist
throw new Error(`As of Sapper 0.21, build configuration should be placed in a single ${bundler}.config.js file`);
}

View File

@@ -1,97 +1,285 @@
import * as fs from 'fs';
import * as path from 'path';
import * as child_process from 'child_process';
import sade from 'sade';
import * as colors from 'ansi-colors';
import prettyMs from 'pretty-ms';
// import upgrade from './cli/upgrade';
import * as ports from 'port-authority';
import colors from 'kleur';
import * as pkg from '../package.json';
import { elapsed, repeat, left_pad, format_milliseconds } from './utils';
import { InvalidEvent, ErrorEvent, FatalEvent, BuildEvent, ReadyEvent } from './interfaces';
const prog = sade('sapper').version(pkg.version);
if (process.argv[2] === 'start') {
// remove this in a future version
console.error(colors.bold.red(`'sapper start' has been removed`));
console.error(`Use 'node [build_dir]' instead`);
process.exit(1);
}
const start = Date.now();
prog.command('dev')
.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);
.option('--dev-port', 'Specify a port for development server')
.option('--hot', 'Use hot module replacement (requires webpack)', true)
.option('--live', 'Reload on changes if not using --hot', true)
.option('--bundler', 'Specify a bundler (rollup or webpack)')
.option('--cwd', 'Current working directory', '.')
.option('--src', 'Source directory', 'src')
.option('--routes', 'Routes directory', 'src/routes')
.option('--static', 'Static files directory', 'static')
.option('--output', 'Sapper output directory', '__sapper__')
.option('--build-dir', 'Development build directory', '__sapper__/dev')
.action(async (opts: {
port: number,
open: boolean,
'dev-port': number,
live: boolean,
hot: boolean,
bundler?: 'rollup' | 'webpack',
cwd: string,
src: string,
routes: string,
static: string,
output: string,
'build-dir': string
}) => {
const { dev } = await import('./api/dev');
try {
const watcher = dev({
cwd: opts.cwd,
src: opts.src,
routes: opts.routes,
static: opts.static,
output: opts.output,
dest: opts['build-dir'],
port: opts.port,
'dev-port': opts['dev-port'],
live: opts.live,
hot: opts.hot,
bundler: opts.bundler
});
let first = true;
watcher.on('stdout', data => {
process.stdout.write(data);
});
watcher.on('stderr', data => {
process.stderr.write(data);
});
watcher.on('ready', async (event: ReadyEvent) => {
if (first) {
console.log(colors.bold.cyan(`> Listening on http://localhost:${event.port}`));
if (opts.open) {
const { exec } = await import('child_process');
exec(`open http://localhost:${event.port}`);
}
first = false;
}
});
watcher.on('invalid', (event: InvalidEvent) => {
const changed = event.changed.map(filename => path.relative(process.cwd(), filename)).join(', ');
console.log(`\n${colors.bold.cyan(changed)} changed. rebuilding...`);
});
watcher.on('error', (event: ErrorEvent) => {
console.log(colors.red(`${event.type}`));
console.log(colors.red(event.message));
});
watcher.on('fatal', (event: FatalEvent) => {
console.log(colors.bold.red(`> ${event.message}`));
if (event.log) console.log(event.log);
});
watcher.on('build', (event: BuildEvent) => {
if (event.errors.length) {
console.log(colors.bold.red(`${event.type}`));
event.errors.filter(e => !e.duplicate).forEach(error => {
if (error.file) console.log(colors.bold(error.file));
console.log(error.message);
});
const hidden = event.errors.filter(e => e.duplicate).length;
if (hidden > 0) {
console.log(`${hidden} duplicate ${hidden === 1 ? 'error' : 'errors'} hidden\n`);
}
} else if (event.warnings.length) {
console.log(colors.bold.yellow(`${event.type}`));
event.warnings.filter(e => !e.duplicate).forEach(warning => {
if (warning.file) console.log(colors.bold(warning.file));
console.log(warning.message);
});
const hidden = event.warnings.filter(e => e.duplicate).length;
if (hidden > 0) {
console.log(`${hidden} duplicate ${hidden === 1 ? 'warning' : 'warnings'} hidden\n`);
}
} else {
console.log(`${colors.bold.green(`${event.type}`)} ${colors.gray(`(${format_milliseconds(event.duration)})`)}`);
}
});
} catch (err) {
console.log(colors.bold.red(`> ${err.message}`));
process.exit(1);
}
});
prog.command('build [dest]')
.describe('Create a production-ready version of your app')
.option('-p, --port', 'Default of process.env.PORT', '3000')
.option('--bundler', 'Specify a bundler (rollup or webpack, blank for auto)')
.option('--legacy', 'Create separate legacy build')
.option('--cwd', 'Current working directory', '.')
.option('--src', 'Source directory', 'src')
.option('--routes', 'Routes directory', 'src/routes')
.option('--output', 'Sapper output directory', '__sapper__')
.example(`build custom-dir -p 4567`)
.action(async (dest = 'build', opts: { port: string }) => {
.action(async (dest = '__sapper__/build', opts: {
port: string,
legacy: boolean,
bundler?: 'rollup' | 'webpack',
cwd: string,
src: string,
routes: string,
output: string
}) => {
console.log(`> Building...`);
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
process.env.SAPPER_DEST = dest;
const start = Date.now();
try {
const { build } = await import('./cli/build');
await build();
await _build(opts.bundler, opts.legacy, opts.cwd, opts.src, opts.routes, opts.output, dest);
const launcher = path.resolve(dest, 'index.js');
fs.writeFileSync(launcher, `
// generated by sapper build at ${new Date().toISOString()}
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
process.env.SAPPER_DEST = __dirname;
process.env.PORT = process.env.PORT || ${opts.port || 3000};
console.log('Starting server on port ' + process.env.PORT);
require('./server.js');
require('./server/server.js');
`.replace(/^\t+/gm, '').trim());
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold.cyan(`node ${dest}`)} to run the app.`);
} catch (err) {
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
console.log(`${colors.bold.red(`> ${err.message}`)}`);
process.exit(1);
}
});
prog.command('start [dir]')
.describe('Start your app')
.option('-p, --port', 'Specify a port')
.option('-o, --open', 'Open a browser window')
.action(async (dir = 'build', opts: { port: number, open: boolean }) => {
const { start } = await import('./cli/start');
start(dir, opts);
});
prog.command('export [dest]')
.describe('Export your app as static files (if possible)')
.option('--build', '(Re)build app before exporting', true)
.option('--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();
.option('--timeout', 'Milliseconds to wait for a page (--no-timeout to disable)', 5000)
.option('--legacy', 'Create separate legacy build')
.option('--bundler', 'Specify a bundler (rollup or webpack, blank for auto)')
.option('--cwd', 'Current working directory', '.')
.option('--src', 'Source directory', 'src')
.option('--routes', 'Routes directory', 'src/routes')
.option('--static', 'Static files directory', 'static')
.option('--output', 'Sapper output directory', '__sapper__')
.option('--build-dir', 'Intermediate build directory', '__sapper__/build')
.action(async (dest = '__sapper__/export', opts: {
build: boolean,
legacy: boolean,
bundler?: 'rollup' | 'webpack',
basepath?: string,
timeout: number | false,
cwd: string,
src: string,
routes: string,
static: string,
output: string,
'build-dir': string,
}) => {
try {
const { build } = await import('./cli/build');
await build();
console.error(`\n> Built in ${elapsed(start)}. Crawling site...`);
if (opts.build) {
console.log(`> Building...`);
await _build(opts.bundler, opts.legacy, opts.cwd, opts.src, opts.routes, opts.output, opts['build-dir']);
console.error(`\n> Built in ${elapsed(start)}`);
}
const { export: _export } = await import('./api/export');
const { default: pb } = await import('pretty-bytes');
await _export({
cwd: opts.cwd,
static: opts.static,
build_dir: opts['build-dir'],
export_dir: dest,
basepath: opts.basepath,
timeout: opts.timeout,
oninfo: event => {
console.log(colors.bold.cyan(`> ${event.message}`));
},
onfile: event => {
const size_color = event.size > 150000 ? colors.bold.red : event.size > 50000 ? colors.bold.yellow : colors.bold.gray;
const size_label = size_color(left_pad(pb(event.size), 10));
const file_label = event.status === 200
? event.file
: colors.bold[event.status >= 400 ? 'red' : 'yellow'](`(${event.status}) ${event.file}`);
console.log(`${size_label} ${file_label}`);
}
});
const { exporter } = await import('./cli/export');
await exporter(dest, opts);
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold.cyan(`npx serve ${dest}`)} to run the app.`);
} catch (err) {
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
console.error(colors.bold.red(`> ${err.message}`));
process.exit(1);
}
});
// TODO upgrade
prog.parse(process.argv);
function elapsed(start: number) {
return prettyMs(Date.now() - start);
}
async function _build(
bundler: 'rollup' | 'webpack',
legacy: boolean,
cwd: string,
src: string,
routes: string,
output: string,
dest: string
) {
const { build } = await import('./api/build');
await build({
bundler,
legacy,
cwd,
src,
routes,
dest,
oncompile: event => {
let banner = `built ${event.type}`;
let c = colors.cyan;
const { warnings } = event.result;
if (warnings.length > 0) {
banner += ` with ${warnings.length} ${warnings.length === 1 ? 'warning' : 'warnings'}`;
c = colors.yellow;
}
console.log();
console.log(c(`┌─${repeat('─', banner.length)}─┐`));
console.log(c(`${colors.bold(banner) }`));
console.log(c(`└─${repeat('─', banner.length)}─┘`));
console.log(event.result.print());
}
});
}

View File

@@ -1,32 +0,0 @@
import { build as _build } from '../api/build';
import * as colors from 'ansi-colors';
import { locations } from '../config';
export function build() {
return new Promise((fulfil, reject) => {
try {
const emitter = _build({
dest: locations.dest(),
app: locations.app(),
routes: locations.routes(),
webpack: 'webpack'
});
emitter.on('build', event => {
console.log(colors.inverse(`\nbuilt ${event.type}`));
console.log(event.webpack_stats.toString({ colors: true }));
});
emitter.on('error', event => {
reject(event.error);
});
emitter.on('done', event => {
fulfil();
});
} catch (err) {
console.log(`${colors.bold.red(`> ${err.message}`)}`);
process.exit(1);
}
});
}

View File

@@ -1,78 +0,0 @@
import * as path from 'path';
import * as colors from 'ansi-colors';
import * as child_process from 'child_process';
import prettyMs from 'pretty-ms';
import { dev as _dev } from '../api/dev';
import * as events from '../api/interfaces';
export function dev(opts: { port: number, open: boolean }) {
try {
const watcher = _dev(opts);
let first = true;
watcher.on('ready', (event: events.ReadyEvent) => {
if (first) {
console.log(`${colors.bold.cyan(`> Listening on http://localhost:${event.port}`)}`);
if (opts.open) child_process.exec(`open http://localhost:${event.port}`);
first = false;
}
// TODO clear screen?
event.process.stdout.on('data', data => {
process.stdout.write(data);
});
event.process.stderr.on('data', data => {
process.stderr.write(data);
});
});
watcher.on('invalid', (event: events.InvalidEvent) => {
const changed = event.changed.map(filename => path.relative(process.cwd(), filename)).join(', ');
console.log(`\n${colors.bold.cyan(changed)} changed. rebuilding...`);
});
watcher.on('error', (event: events.ErrorEvent) => {
console.log(`${colors.red(`${event.type}`)}`);
console.log(`${colors.red(event.message)}`);
});
watcher.on('fatal', (event: events.FatalEvent) => {
console.log(`${colors.bold.red(`> ${event.message}`)}`);
if (event.log) console.log(event.log);
});
watcher.on('build', (event: events.BuildEvent) => {
if (event.errors.length) {
console.log(`${colors.bold.red(`${event.type}`)}`);
event.errors.filter(e => !e.duplicate).forEach(error => {
console.log(error.message);
});
const hidden = event.errors.filter(e => e.duplicate).length;
if (hidden > 0) {
console.log(`${hidden} duplicate ${hidden === 1 ? 'error' : 'errors'} hidden\n`);
}
} else if (event.warnings.length) {
console.log(`${colors.bold.yellow(`${event.type}`)}`);
event.warnings.filter(e => !e.duplicate).forEach(warning => {
console.log(warning.message);
});
const hidden = event.warnings.filter(e => e.duplicate).length;
if (hidden > 0) {
console.log(`${hidden} duplicate ${hidden === 1 ? 'warning' : 'warnings'} hidden\n`);
}
} else {
console.log(`${colors.bold.green(`${event.type}`)} ${colors.gray(`(${prettyMs(event.duration)})`)}`);
}
});
} catch (err) {
console.log(`${colors.bold.red(`> ${err.message}`)}`);
process.exit(1);
}
}

View File

@@ -1,35 +0,0 @@
import { exporter as _exporter } from '../api/export';
import * as colors from 'ansi-colors';
import prettyBytes from 'pretty-bytes';
import { locations } from '../config';
export function exporter(export_dir: string, { basepath = '' }) {
return new Promise((fulfil, reject) => {
try {
const emitter = _exporter({
build: locations.dest(),
dest: export_dir,
basepath
});
emitter.on('file', event => {
console.log(`${colors.bold.cyan(event.file)} ${colors.gray(`(${prettyBytes(event.size)})`)}`);
});
emitter.on('failure', event => {
console.log(`${colors.red(`> Received ${event.status} response when fetching ${event.pathname}`)}`);
});
emitter.on('error', event => {
reject(event.error);
});
emitter.on('done', event => {
fulfil();
});
} catch (err) {
console.log(`${colors.bold.red(`> ${err.message}`)}`);
process.exit(1);
}
});
}

View File

@@ -1,39 +0,0 @@
import * as fs from 'fs';
import * as path from 'path';
import * as child_process from 'child_process';
import * as colors from 'ansi-colors';
import * as ports from 'port-authority';
export async function start(dir: string, opts: { port: number, open: boolean }) {
let port = opts.port || +process.env.PORT;
const resolved = path.resolve(dir);
const server = path.resolve(dir, 'server.js');
if (!fs.existsSync(server)) {
console.log(`${colors.bold.red(`> ${dir}/server.js does not exist — type ${colors.bold.cyan(dir === 'build' ? `npx sapper build` : `npx sapper build ${dir}`)} to create it`)}`);
return;
}
if (port) {
if (!await ports.check(port)) {
console.log(`${colors.bold.red(`> Port ${port} is unavailable`)}`);
return;
}
} else {
port = await ports.find(3000);
}
child_process.fork(server, [], {
cwd: process.cwd(),
env: Object.assign({
NODE_ENV: 'production',
PORT: port,
SAPPER_DEST: dir
}, process.env)
});
await ports.wait(port);
console.log(`${colors.bold.cyan(`> Listening on http://localhost:${port}`)}`);
if (opts.open) child_process.exec(`open http://localhost:${port}`);
}

View File

@@ -1,53 +0,0 @@
import * as fs from 'fs';
import * as colors from 'ansi-colors';
export default async function upgrade() {
const upgraded = [
await upgrade_sapper_main()
].filter(Boolean);
if (upgraded.length === 0) {
console.log(`No changes!`);
}
}
async function upgrade_sapper_main() {
const _2xx = read('templates/2xx.html');
const _4xx = read('templates/4xx.html');
const _5xx = read('templates/5xx.html');
const pattern = /<script src='\%sapper\.main\%'><\/script>/;
let replaced = false;
['2xx', '4xx', '5xx'].forEach(code => {
const file = `templates/${code}.html`
const template = read(file);
if (!template) return;
if (/\%sapper\.main\%/.test(template)) {
if (!pattern.test(template)) {
console.log(`${colors.red(`Could not replace %sapper.main% in ${file}`)}`);
} else {
write(file, template.replace(pattern, `%sapper.scripts%`));
console.log(`${colors.green(`Replaced %sapper.main% in ${file}`)}`);
replaced = true;
}
}
});
return replaced;
}
function read(file: string) {
try {
return fs.readFileSync(file, 'utf-8');
} catch (err) {
console.error(err);
return null;
}
}
function write(file: string, data: string) {
fs.writeFileSync(file, data);
}

View File

@@ -1,10 +0,0 @@
import * as path from 'path';
export const dev = () => process.env.NODE_ENV !== 'production';
export const locations = {
base: () => path.resolve(process.env.SAPPER_BASE || ''),
app: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_APP || 'app'),
routes: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_ROUTES || 'routes'),
dest: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_DEST || '.sapper')
};

7
src/config/env.ts Normal file
View File

@@ -0,0 +1,7 @@
export let dev: boolean;
export let src: string;
export let dest: string;
export const set_dev = (_: boolean) => dev = _;
export const set_src = (_: string) => src = _;
export const set_dest = (_: string) => dest = _;

53
src/config/rollup.ts Normal file
View File

@@ -0,0 +1,53 @@
import { dev, src, dest } from './env';
export default {
dev,
client: {
input: () => {
return `${src}/client.js`
},
output: () => {
let dir = `${dest}/client`;
if (process.env.SAPPER_LEGACY_BUILD) dir += `/legacy`;
return {
dir,
entryFileNames: '[name].[hash].js',
chunkFileNames: '[name].[hash].js',
format: 'esm',
sourcemap: dev
};
}
},
server: {
input: () => {
return {
server: `${src}/server.js`
};
},
output: () => {
return {
dir: `${dest}/server`,
format: 'cjs',
sourcemap: dev
};
}
},
serviceworker: {
input: () => {
return `${src}/service-worker.js`;
},
output: () => {
return {
file: `${dest}/service-worker.js`,
format: 'iife'
}
}
}
};

View File

@@ -1,18 +1,18 @@
import { locations, dev } from './config';
import { dev, src, dest } from './env';
export default {
dev: dev(),
dev,
client: {
entry: () => {
return {
main: `${locations.app()}/client`
main: `${src}/client`
};
},
output: () => {
return {
path: `${locations.dest()}/client`,
path: `${dest}/client`,
filename: '[hash]/[name].js',
chunkFilename: '[hash]/[name].[id].js',
publicPath: `client/`
@@ -23,13 +23,13 @@ export default {
server: {
entry: () => {
return {
server: `${locations.app()}/server`
server: `${src}/server`
};
},
output: () => {
return {
path: locations.dest(),
path: `${dest}/server`,
filename: '[name].js',
chunkFilename: '[hash]/[name].[id].js',
libraryTarget: 'commonjs2'
@@ -40,13 +40,13 @@ export default {
serviceworker: {
entry: () => {
return {
'service-worker': `${locations.app()}/service-worker`
'service-worker': `${src}/service-worker`
};
},
output: () => {
return {
path: locations.dest(),
path: dest,
filename: '[name].js',
chunkFilename: '[name].[id].[hash].js'
}

View File

@@ -1,3 +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';
export { default as create_compilers } from './core/create_compilers/index';
export { default as create_manifest_data } from './core/create_manifest_data';

View File

@@ -1,29 +0,0 @@
import * as path from 'path';
import relative from 'require-relative';
export default function create_compilers({ webpack }: { webpack: string }) {
const wp = relative('webpack', process.cwd());
const serviceworker_config = try_require(path.resolve(`${webpack}/service-worker.config.js`));
return {
client: wp(
require(path.resolve(`${webpack}/client.config.js`))
),
server: wp(
require(path.resolve(`${webpack}/server.config.js`))
),
serviceworker: serviceworker_config && wp(serviceworker_config)
};
}
function try_require(specifier: string) {
try {
return require(specifier);
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') return null;
throw err;
}
}

View File

@@ -0,0 +1,169 @@
import * as path from 'path';
import relative from 'require-relative';
import { CompileResult } from './interfaces';
import RollupResult from './RollupResult';
let rollup: any;
export default class RollupCompiler {
_: Promise<any>;
_oninvalid: (filename: string) => void;
_start: number;
input: string;
warnings: any[];
errors: any[];
chunks: any[];
css_files: Array<{ id: string, code: string }>;
constructor(config: any) {
this._ = this.get_config(config);
this.input = null;
this.warnings = [];
this.errors = [];
this.chunks = [];
this.css_files = [];
}
async get_config(mod: any) {
// TODO this is hacky, and doesn't need to apply to all three compilers
(mod.plugins || (mod.plugins = [])).push({
name: 'sapper-internal',
options: (opts: any) => {
this.input = opts.input;
},
renderChunk: (code: string, chunk: any) => {
this.chunks.push(chunk);
},
transform: (code: string, id: string) => {
if (/\.css$/.test(id)) {
this.css_files.push({ id, code });
return ``;
}
}
});
const onwarn = mod.onwarn || ((warning: any, handler: (warning: any) => void) => {
handler(warning);
});
mod.onwarn = (warning: any) => {
onwarn(warning, (warning: any) => {
this.warnings.push(warning);
});
};
return mod;
}
oninvalid(cb: (filename: string) => void) {
this._oninvalid = cb;
}
async compile(): Promise<CompileResult> {
const config = await this._;
const start = Date.now();
try {
const bundle = await rollup.rollup(config);
await bundle.write(config.output);
return new RollupResult(Date.now() - start, this);
} catch (err) {
if (err.filename) {
// TODO this is a bit messy. Also, can
// Rollup emit other kinds of error?
err.message = [
`Failed to build — error in ${err.filename}: ${err.message}`,
err.frame
].filter(Boolean).join('\n');
}
throw err;
}
}
async watch(cb: (err?: Error, stats?: any) => void) {
const config = await this._;
const watcher = rollup.watch(config);
watcher.on('change', (id: string) => {
this.chunks = [];
this.warnings = [];
this.errors = [];
this._oninvalid(id);
});
watcher.on('event', (event: any) => {
switch (event.code) {
case 'FATAL':
// TODO kill the process?
if (event.error.filename) {
// TODO this is a bit messy. Also, can
// Rollup emit other kinds of error?
event.error.message = [
`Failed to build — error in ${event.error.filename}: ${event.error.message}`,
event.error.frame
].filter(Boolean).join('\n');
}
cb(event.error);
break;
case 'ERROR':
this.errors.push(event.error);
cb(null, new RollupResult(Date.now() - this._start, this));
break;
case 'START':
case 'END':
// TODO is there anything to do with this info?
break;
case 'BUNDLE_START':
this._start = Date.now();
break;
case 'BUNDLE_END':
cb(null, new RollupResult(Date.now() - this._start, this));
break;
default:
console.log(`Unexpected event ${event.code}`);
}
});
}
static async load_config(cwd: string) {
if (!rollup) rollup = relative('rollup', cwd);
const input = path.resolve(cwd, 'rollup.config.js');
const bundle = await rollup.rollup({
input,
inlineDynamicImports: true,
external: (id: string) => {
return (id[0] !== '.' && !path.isAbsolute(id)) || id.slice(-5, id.length) === '.json';
}
});
const resp = await bundle.generate({ format: 'cjs' });
const { code } = resp.output ? resp.output[0] : resp;
// temporarily override require
const defaultLoader = require.extensions['.js'];
require.extensions['.js'] = (module: any, filename: string) => {
if (filename === input) {
module._compile(code, filename);
} else {
defaultLoader(module, filename);
}
};
const config: any = require(input);
delete require.cache[input];
return config;
}
}

View File

@@ -0,0 +1,118 @@
import * as path from 'path';
import colors from 'kleur';
import pb from 'pretty-bytes';
import RollupCompiler from './RollupCompiler';
import extract_css from './extract_css';
import { left_pad } from '../../utils';
import { CompileResult, BuildInfo, CompileError, Chunk, CssFile } from './interfaces';
import { ManifestData, Dirs } from '../../interfaces';
export default class RollupResult implements CompileResult {
duration: number;
errors: CompileError[];
warnings: CompileError[];
chunks: Chunk[];
assets: Record<string, string>;
css_files: CssFile[];
css: {
main: string,
chunks: Record<string, string[]>
};
summary: string;
constructor(duration: number, compiler: RollupCompiler) {
this.duration = duration;
this.errors = compiler.errors.map(munge_warning_or_error);
this.warnings = compiler.warnings.map(munge_warning_or_error); // TODO emit this as they happen
this.chunks = compiler.chunks.map(chunk => ({
file: chunk.fileName,
imports: chunk.imports.filter(Boolean),
modules: Object.keys(chunk.modules)
}));
this.css_files = compiler.css_files;
// TODO populate this properly. We don't have named chunks, as in
// webpack, but we can have a route -> [chunk] map or something
this.assets = {};
if (typeof compiler.input === 'string') {
compiler.chunks.forEach(chunk => {
if (compiler.input in chunk.modules) {
this.assets.main = chunk.fileName;
}
});
} else {
for (const name in compiler.input) {
const file = compiler.input[name];
this.assets[name] = compiler.chunks.find(chunk => file in chunk.modules).fileName;
}
}
this.summary = compiler.chunks.map(chunk => {
const size_color = chunk.code.length > 150000 ? colors.bold.red : chunk.code.length > 50000 ? colors.bold.yellow : colors.bold.white;
const size_label = left_pad(pb(chunk.code.length), 10);
const lines = [size_color(`${size_label} ${chunk.fileName}`)];
const deps = Object.keys(chunk.modules)
.map(file => {
return {
file: path.relative(process.cwd(), file),
size: chunk.modules[file].renderedLength
};
})
.filter(dep => dep.size > 0)
.sort((a, b) => b.size - a.size);
const total_unminified = deps.reduce((t, d) => t + d.size, 0);
deps.forEach((dep, i) => {
const c = i === deps.length - 1 ? '└' : '│';
let line = ` ${c} ${dep.file}`;
if (deps.length > 1) {
const p = (100 * dep.size / total_unminified).toFixed(1);
line += ` (${p}%)`;
}
lines.push(colors.gray(line));
});
return lines.join('\n');
}).join('\n');
}
to_json(manifest_data: ManifestData, dirs: Dirs): BuildInfo {
// TODO extract_css has side-effects that don't belong
// in a method called to_json
return {
bundler: 'rollup',
shimport: require('shimport/package.json').version,
assets: this.assets,
css: extract_css(this, manifest_data.components, dirs)
};
}
print() {
const blocks: string[] = this.warnings.map(warning => {
return warning.file
? `> ${colors.bold(warning.file)}\n${warning.message}`
: `> ${warning.message}`;
});
blocks.push(this.summary);
return blocks.join('\n\n');
}
}
function munge_warning_or_error(warning_or_error: any) {
return {
file: warning_or_error.filename,
message: [warning_or_error.message, warning_or_error.frame].filter(Boolean).join('\n')
};
}

View File

@@ -0,0 +1,46 @@
import relative from 'require-relative';
import { CompileResult } from './interfaces';
import WebpackResult from './WebpackResult';
let webpack: any;
export class WebpackCompiler {
_: any;
constructor(config: any) {
if (!webpack) webpack = relative('webpack', process.cwd());
this._ = webpack(config);
}
oninvalid(cb: (filename: string) => void) {
this._.hooks.invalid.tap('sapper', cb);
}
compile(): Promise<CompileResult> {
return new Promise((fulfil, reject) => {
this._.run((err: Error, stats: any) => {
if (err) {
reject(err);
process.exit(1);
}
const result = new WebpackResult(stats);
if (result.errors.length) {
console.error(stats.toString({ colors: true }));
reject(new Error(`Encountered errors while building app`));
}
else {
fulfil(result);
}
});
});
}
watch(cb: (err?: Error, stats?: any) => void) {
this._.watch({}, (err?: Error, stats?: any) => {
cb(err, stats && new WebpackResult(stats));
});
}
}

View File

@@ -0,0 +1,84 @@
import format_messages from 'webpack-format-messages';
import { CompileResult, BuildInfo, CompileError, Chunk, CssFile } from './interfaces';
import { ManifestData, Dirs } from '../../interfaces';
const locPattern = /\((\d+):(\d+)\)$/;
function munge_warning_or_error(message: string) {
// TODO this is all a bit rube goldberg...
const lines = message.split('\n');
const file = lines.shift()
.replace('', '') // careful — there is a special character at the beginning of this string
.replace('', '')
.replace('./', '');
let line = null;
let column = null;
const match = locPattern.exec(lines[0]);
if (match) {
lines[0] = lines[0].replace(locPattern, '');
line = +match[1];
column = +match[2];
}
return {
file,
message: lines.join('\n')
};
}
export default class WebpackResult implements CompileResult {
duration: number;
errors: CompileError[];
warnings: CompileError[];
chunks: Chunk[];
assets: Record<string, string>;
css_files: CssFile[];
stats: any;
constructor(stats: any) {
this.stats = stats;
const info = stats.toJson();
const messages = format_messages(stats);
this.errors = messages.errors.map(munge_warning_or_error);
this.warnings = messages.warnings.map(munge_warning_or_error);
this.duration = info.time;
this.chunks = info.assets.map((chunk: { name: string }) => ({ file: chunk.name }));
this.assets = info.assetsByChunkName;
}
to_json(manifest_data: ManifestData, dirs: Dirs): BuildInfo {
const extract_css = (assets: string[] | string) => {
assets = Array.isArray(assets) ? assets : [assets];
return assets.find(asset => /\.css$/.test(asset));
};
return {
bundler: 'webpack',
shimport: null, // webpack has its own loader
assets: this.assets,
css: {
main: extract_css(this.assets.main),
chunks: Object
.keys(this.assets)
.filter(chunkName => chunkName !== 'main')
.reduce((chunks: { [key: string]: string }, chukName) => {
const assets = this.assets[chukName];
chunks[chukName] = extract_css(assets);
return chunks;
}, {})
}
};
}
print() {
return this.stats.toString({ colors: true });
}
}

View File

@@ -0,0 +1,243 @@
import * as fs from 'fs';
import * as path from 'path';
import hash from 'string-hash';
import * as codec from 'sourcemap-codec';
import { PageComponent, Dirs } from '../../interfaces';
import { CompileResult, Chunk } from './interfaces';
import { posixify } from '../../utils'
const inline_sourcemap_header = 'data:application/json;charset=utf-8;base64,';
function extract_sourcemap(raw: string, id: string) {
let raw_map: string;
let map = null;
const code = raw.replace(/\/\*#\s+sourceMappingURL=(.+)\s+\*\//g, (m, url) => {
if (raw_map) {
// TODO should not happen!
throw new Error(`Found multiple sourcemaps in single CSS file (${id})`);
}
raw_map = url;
return '';
}).trim();
if (raw_map) {
if (raw_map.startsWith(inline_sourcemap_header)) {
const json = Buffer.from(raw_map.slice(inline_sourcemap_header.length), 'base64').toString();
map = JSON.parse(json);
} else {
// TODO do we want to handle non-inline sourcemaps? could be a rabbit hole
}
}
return {
code,
map
};
}
type SourceMap = {
version: 3;
file: string;
sources: string[];
sourcesContent: string[];
names: string[];
mappings: string;
};
function get_css_from_modules(modules: string[], css_map: Map<string, string>, dirs: Dirs) {
const parts: string[] = [];
const mappings: number[][][] = [];
const combined_map: SourceMap = {
version: 3,
file: null,
sources: [],
sourcesContent: [],
names: [],
mappings: null
};
modules.forEach(module => {
if (!/\.css$/.test(module)) return;
const css = css_map.get(module);
const { code, map } = extract_sourcemap(css, module);
parts.push(code);
if (map) {
const lines = codec.decode(map.mappings);
if (combined_map.sources.length > 0 || combined_map.names.length > 0) {
lines.forEach(line => {
line.forEach(segment => {
// adjust source index
segment[1] += combined_map.sources.length;
// adjust name index
if (segment[4]) segment[4] += combined_map.names.length;
});
});
}
combined_map.sources.push(...map.sources);
combined_map.sourcesContent.push(...map.sourcesContent);
combined_map.names.push(...map.names);
mappings.push(...lines);
}
});
if (parts.length > 0) {
combined_map.mappings = codec.encode(mappings);
combined_map.sources = combined_map.sources.map(source => path.relative(`${dirs.dest}/client`, source));
return {
code: parts.join('\n'),
map: combined_map
};
}
return null;
}
export default function extract_css(client_result: CompileResult, components: PageComponent[], dirs: Dirs) {
const result: {
main: string | null;
chunks: Record<string, string[]>
} = {
main: null,
chunks: {}
};
if (!client_result.css_files) return; // Rollup-only for now
let asset_dir = `${dirs.dest}/client`;
if (process.env.SAPPER_LEGACY_BUILD) asset_dir += '/legacy';
const unclaimed = new Set(client_result.css_files.map(x => x.id));
const lookup = new Map();
client_result.chunks.forEach(chunk => {
lookup.set(chunk.file, chunk);
});
const css_map = new Map();
client_result.css_files.forEach(css_module => {
css_map.set(css_module.id, css_module.code);
});
const chunks_with_css = new Set();
// concatenate and emit CSS
client_result.chunks.forEach(chunk => {
const css_modules = chunk.modules.filter(m => css_map.has(m));
if (!css_modules.length) return;
const css = get_css_from_modules(css_modules, css_map, dirs);
const { code, map } = css;
const output_file_name = chunk.file.replace(/\.js$/, '.css');
map.file = output_file_name;
map.sources = map.sources.map(source => path.relative(`${asset_dir}`, source));
fs.writeFileSync(`${asset_dir}/${output_file_name}`, `${code}\n/* sourceMappingURL=./${output_file_name}.map */`);
fs.writeFileSync(`${asset_dir}/${output_file_name}.map`, JSON.stringify(map, null, ' '));
chunks_with_css.add(chunk);
});
const entry = path.resolve(dirs.src, 'client.js');
const entry_chunk = client_result.chunks.find(chunk => chunk.modules.indexOf(entry) !== -1);
const entry_chunk_dependencies: Set<Chunk> = new Set([entry_chunk]);
const entry_css_modules: string[] = [];
// recursively find the chunks this component depends on
entry_chunk_dependencies.forEach(chunk => {
chunk.imports.forEach(file => {
entry_chunk_dependencies.add(lookup.get(file));
});
if (chunks_with_css.has(chunk)) {
chunk.modules.forEach(file => {
unclaimed.delete(file);
if (css_map.has(file)) {
entry_css_modules.push(file);
}
});
}
});
// figure out which (css-having) chunks each component depends on
components.forEach(component => {
const resolved = path.resolve(dirs.routes, component.file);
const chunk: Chunk = client_result.chunks.find(chunk => chunk.modules.indexOf(resolved) !== -1);
if (!chunk) {
// this should never happen!
throw new Error(`Could not find chunk that owns ${component.file}`);
}
const chunk_dependencies: Set<Chunk> = new Set([chunk]);
const css_dependencies: string[] = [];
// recursively find the chunks this component depends on
chunk_dependencies.forEach(chunk => {
chunk.imports.forEach(file => {
chunk_dependencies.add(lookup.get(file));
});
if (chunks_with_css.has(chunk)) {
css_dependencies.push(chunk.file.replace(/\.js$/, '.css'));
chunk.modules.forEach(file => {
unclaimed.delete(file);
});
}
});
result.chunks[component.file] = css_dependencies;
});
fs.readdirSync(asset_dir).forEach(file => {
if (fs.statSync(`${asset_dir}/${file}`).isDirectory()) return;
const source = fs.readFileSync(`${asset_dir}/${file}`, 'utf-8');
const replaced = source.replace(/["']__SAPPER_CSS_PLACEHOLDER:(.+?)__["']/g, (m, route) => {
return JSON.stringify(result.chunks[route]);
});
fs.writeFileSync(`${asset_dir}/${file}`, replaced);
});
unclaimed.forEach(file => {
entry_css_modules.push(file);
});
const leftover = get_css_from_modules(entry_css_modules, css_map, dirs);
if (leftover) {
const { code, map } = leftover;
const main_hash = hash(code);
const output_file_name = `main.${main_hash}.css`;
map.file = output_file_name;
map.sources = map.sources.map(source => path.relative(asset_dir, source));
fs.writeFileSync(`${asset_dir}/${output_file_name}`, `${code}\n/* sourceMappingURL=client/${output_file_name}.map */`);
fs.writeFileSync(`${asset_dir}/${output_file_name}.map`, JSON.stringify(map, null, ' '));
result.main = output_file_name;
}
return result;
}

View File

@@ -0,0 +1,72 @@
import * as path from 'path';
import RollupCompiler from './RollupCompiler';
import { WebpackCompiler } from './WebpackCompiler';
import { set_dev, set_src, set_dest } from '../../config/env';
export type Compiler = RollupCompiler | WebpackCompiler;
export type Compilers = {
client: Compiler;
server: Compiler;
serviceworker?: Compiler;
}
export default async function create_compilers(
bundler: 'rollup' | 'webpack',
cwd: string,
src: string,
dest: string,
dev: boolean
): Promise<Compilers> {
set_dev(dev);
set_src(src);
set_dest(dest);
if (bundler === 'rollup') {
const config = await RollupCompiler.load_config(cwd);
validate_config(config, 'rollup');
normalize_rollup_config(config.client);
normalize_rollup_config(config.server);
if (config.serviceworker) {
normalize_rollup_config(config.serviceworker);
}
return {
client: new RollupCompiler(config.client),
server: new RollupCompiler(config.server),
serviceworker: config.serviceworker && new RollupCompiler(config.serviceworker)
};
}
if (bundler === 'webpack') {
const config = require(path.resolve(cwd, 'webpack.config.js'));
validate_config(config, 'webpack');
return {
client: new WebpackCompiler(config.client),
server: new WebpackCompiler(config.server),
serviceworker: config.serviceworker && new WebpackCompiler(config.serviceworker)
};
}
// this shouldn't be possible...
throw new Error(`Invalid bundler option '${bundler}'`);
}
function validate_config(config: any, bundler: 'rollup' | 'webpack') {
if (!config.client || !config.server) {
throw new Error(`${bundler}.config.js must export a { client, server, serviceworker? } object`);
}
}
function normalize_rollup_config(config: any) {
if (typeof config.input === 'string') {
config.input = path.normalize(config.input);
} else {
for (const name in config.input) {
config.input[name] = path.normalize(config.input[name]);
}
}
}

View File

@@ -0,0 +1,39 @@
import { ManifestData, Dirs } from '../../interfaces';
export type Chunk = {
file: string;
imports: string[];
modules: string[];
}
export type CssFile = {
id: string;
code: string;
};
export class CompileError {
file: string;
message: string;
}
export interface CompileResult {
duration: number;
errors: CompileError[];
warnings: CompileError[];
chunks: Chunk[];
assets: Record<string, string>;
css_files: CssFile[];
to_json: (manifest_data: ManifestData, dirs: Dirs) => BuildInfo
}
export type BuildInfo = {
bundler: string;
shimport: string;
assets: Record<string, string>;
legacy_assets?: Record<string, string>;
css: {
main: string | null,
chunks: Record<string, string[]>
}
}

View File

@@ -0,0 +1,306 @@
import * as fs from 'fs';
import * as path from 'path';
import { Page, PageComponent, ServerRoute, ManifestData } from '../interfaces';
import { posixify, reserved_words } from '../utils';
export default function create_manifest_data(cwd: string): ManifestData {
// TODO remove in a future version
if (!fs.existsSync(cwd)) {
throw new Error(`As of Sapper 0.21, the routes/ directory should become src/routes/`);
}
const components: PageComponent[] = [];
const pages: Page[] = [];
const server_routes: ServerRoute[] = [];
const default_layout: PageComponent = {
default: true,
name: '_default_layout',
file: null
};
function walk(
dir: string,
parent_segments: Part[][],
parent_params: string[],
stack: Array<{
component: PageComponent,
params: string[]
}>
) {
const items = fs.readdirSync(dir)
.map(basename => {
const resolved = path.join(dir, basename);
const file = path.relative(cwd, resolved);
const is_dir = fs.statSync(resolved).isDirectory();
const ext = path.extname(basename);
if (!is_dir && !/^\.[a-z]+$/i.test(ext)) return null; // filter out tmp files etc
const segment = is_dir
? basename
: basename.slice(0, -path.extname(basename).length);
const parts = get_parts(segment);
const is_index = is_dir ? false : basename.startsWith('index.');
const is_page = ext === '.html';
parts.forEach(part => {
if (/\]\[/.test(part.content)) {
throw new Error(`Invalid route ${file} — parameters must be separated`);
}
if (part.qualifier && /[\(\)\?\:]/.test(part.qualifier.slice(1, -1))) {
throw new Error(`Invalid route ${file} — cannot use (, ), ? or : in route qualifiers`);
}
});
return {
basename,
parts,
file: posixify(file),
is_dir,
is_index,
is_page
};
})
.filter(Boolean)
.sort(comparator);
items.forEach(item => {
if (item.basename[0] === '_') return;
if (item.basename[0] === '.') {
if (item.file !== '.well-known') return;
}
const segments = parent_segments.slice();
if (item.is_index && segments.length > 0) {
const last_segment = segments[segments.length - 1].slice();
const suffix = item.basename
.slice(0, -path.extname(item.basename).length).
replace('index', '');
if (suffix) {
const last_part = last_segment[last_segment.length - 1];
if (last_part.dynamic) {
last_segment.push({ dynamic: false, content: suffix });
} else {
last_segment[last_segment.length - 1] = {
dynamic: false,
content: `${last_part.content}${suffix}`
};
}
segments[segments.length - 1] = last_segment;
}
} else {
segments.push(item.parts);
}
const params = parent_params.slice();
params.push(...item.parts.filter(p => p.dynamic).map(p => p.content));
if (item.is_dir) {
const index = path.join(dir, item.basename, '_layout.html');
const component = fs.existsSync(index) && {
name: `${get_slug(item.file)}__layout`,
file: `${item.file}/_layout.html`
};
if (component) components.push(component);
walk(
path.join(dir, item.basename),
segments,
params,
component
? stack.concat({ component, params })
: stack.concat(null)
);
}
else if (item.is_page) {
const component = {
name: get_slug(item.file),
file: item.file
};
const parts = stack.concat({
component,
params
});
components.push(component);
if (item.basename === 'index.html') {
pages.push({
pattern: get_pattern(parent_segments, true),
parts
});
} else {
pages.push({
pattern: get_pattern(segments, true),
parts
});
}
}
else {
server_routes.push({
name: `route_${get_slug(item.file)}`,
pattern: get_pattern(segments, false),
file: item.file,
params: params
});
}
});
}
const root_file = path.join(cwd, '_layout.html');
const root = fs.existsSync(root_file)
? {
name: 'main',
file: '_layout.html'
}
: default_layout;
walk(cwd, [], [], []);
// check for clashes
const seen_pages: Map<string, Page> = new Map();
pages.forEach(page => {
const pattern = page.pattern.toString();
if (seen_pages.has(pattern)) {
const file = page.parts.pop().component.file;
const other_page = seen_pages.get(pattern);
const other_file = other_page.parts.pop().component.file;
throw new Error(`The ${other_file} and ${file} pages clash`);
}
seen_pages.set(pattern, page);
});
const seen_routes: Map<string, ServerRoute> = new Map();
server_routes.forEach(route => {
const pattern = route.pattern.toString();
if (seen_routes.has(pattern)) {
const other_route = seen_routes.get(pattern);
throw new Error(`The ${other_route.file} and ${route.file} routes clash`);
}
seen_routes.set(pattern, route);
});
return {
root,
components,
pages,
server_routes
};
}
type Part = {
content: string;
dynamic: boolean;
qualifier?: string;
};
function comparator(
a: { basename: string, parts: Part[], file: string, is_index: boolean },
b: { basename: string, parts: Part[], file: string, is_index: boolean }
) {
if (a.is_index !== b.is_index) return a.is_index ? -1 : 1;
const max = Math.max(a.parts.length, b.parts.length);
for (let i = 0; i < max; i += 1) {
const a_sub_part = a.parts[i];
const b_sub_part = b.parts[i];
if (!a_sub_part) return 1; // b is more specific, so goes first
if (!b_sub_part) return -1;
if (a_sub_part.dynamic !== b_sub_part.dynamic) {
return a_sub_part.dynamic ? 1 : -1;
}
if (!a_sub_part.dynamic && a_sub_part.content !== b_sub_part.content) {
return (
(b_sub_part.content.length - a_sub_part.content.length) ||
(a_sub_part.content < b_sub_part.content ? -1 : 1)
);
}
// If both parts dynamic, check for regexp patterns
if (a_sub_part.dynamic && b_sub_part.dynamic) {
const regexp_pattern = /\((.*?)\)/;
const a_match = regexp_pattern.exec(a_sub_part.content);
const b_match = regexp_pattern.exec(b_sub_part.content);
if (!a_match && b_match) {
return 1; // No regexp, so less specific than b
}
if (!b_match && a_match) {
return -1;
}
if (a_match && b_match && a_match[1] !== b_match[1]) {
return b_match[1].length - a_match[1].length;
}
}
}
}
function get_parts(part: string): Part[] {
return part.split(/\[(.+)\]/)
.map((str, i) => {
if (!str) return null;
const dynamic = i % 2 === 1;
const [, content, qualifier] = dynamic
? /([^(]+)(\(.+\))?$/.exec(str)
: [, str, null];
return {
content,
dynamic,
qualifier
};
})
.filter(Boolean);
}
function get_slug(file: string) {
let name = file
.replace(/[\\\/]index/, '')
.replace(/_default([\/\\index])?\.html$/, 'index')
.replace(/[\/\\]/g, '_')
.replace(/\.\w+$/, '')
.replace(/\[([^(]+)(?:\([^(]+\))?\]/, '$$$1')
.replace(/[^a-zA-Z0-9_$]/g, c => {
return c === '.' ? '_' : `$${c.charCodeAt(0)}`
});
if (reserved_words.has(name)) name += '_';
return name;
}
function get_pattern(segments: Part[][], add_trailing_slash: boolean) {
return new RegExp(
`^` +
segments.map(segment => {
return '\\/' + segment.map(part => {
return part.dynamic
? part.qualifier || '([^\\/]+?)'
: encodeURI(part.content.normalize())
.replace(/\?/g, '%3F')
.replace(/#/g, '%23')
.replace(/%5B/g, '[')
.replace(/%5D/g, ']');
}).join('');
}).join('') +
(add_trailing_slash ? '\\\/?$' : '$')
);
}

View File

@@ -1,125 +1,243 @@
import * as fs from 'fs';
import * as path from 'path';
import * as glob from 'glob';
import { posixify, write_if_changed } from './utils';
import { dev, locations } from '../config';
import { Route } from '../interfaces';
import { posixify, stringify, walk, write_if_changed } from '../utils';
import { Page, PageComponent, ManifestData } from '../interfaces';
export function create_main_manifests({ routes, dev_port }: {
routes: Route[];
export function create_main_manifests({
bundler,
manifest_data,
dev_port,
dev,
cwd,
src,
dest,
routes,
output
}: {
bundler: string,
manifest_data: ManifestData;
dev_port?: number;
dev: boolean;
cwd: string;
src: string;
dest: string;
routes: string;
output: string
}) {
const path_to_routes = path.relative(`${locations.app()}/manifest`, locations.routes());
if (!fs.existsSync(output)) fs.mkdirSync(output);
const client_manifest = generate_client(routes, path_to_routes, dev_port);
const server_manifest = generate_server(routes, path_to_routes);
const path_to_routes = path.relative(output, routes);
write_if_changed(`${locations.app()}/manifest/client.js`, client_manifest);
write_if_changed(`${locations.app()}/manifest/server.js`, server_manifest);
const client_manifest = generate_client(manifest_data, path_to_routes, bundler, dev, dev_port);
const server_manifest = generate_server(manifest_data, path_to_routes, cwd, src, dest, dev);
write_if_changed(
`${output}/_layout.html`,
`<svelte:component this={child.component} {...child.props}/>`
);
write_if_changed(`${output}/client.js`, client_manifest);
write_if_changed(`${output}/server.js`, server_manifest);
}
export function create_serviceworker_manifest({ routes, client_files }: {
routes: Route[];
export function create_serviceworker_manifest({ manifest_data, output, client_files, static_files }: {
manifest_data: ManifestData;
output: string;
client_files: string[];
static_files: string;
}) {
const assets = glob.sync('**', { cwd: 'assets', nodir: true });
let files: string[] = ['/service-worker-index.html'];
if (fs.existsSync(static_files)) {
files = files.concat(walk(static_files));
} else {
// TODO remove in a future version
if (fs.existsSync('assets')) {
throw new Error(`As of Sapper 0.21, the assets/ directory should become static/`);
}
}
let code = `
// This file is generated by Sapper — do not edit it!
export const timestamp = ${Date.now()};
export const assets = [\n\t${assets.map((x: string) => `"${x}"`).join(',\n\t')}\n];
export const files = [\n\t${files.map((x: string) => stringify(x)).join(',\n\t')}\n];
export { files as assets }; // legacy
export const shell = [\n\t${client_files.map((x: string) => `"${x}"`).join(',\n\t')}\n];
export const shell = [\n\t${client_files.map((x: string) => stringify(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];
export const routes = [\n\t${manifest_data.pages.map((r: Page) => `{ pattern: ${r.pattern} }`).join(',\n\t')}\n];
`.replace(/^\t\t/gm, '').trim();
write_if_changed(`${locations.app()}/manifest/service-worker.js`, code);
write_if_changed(`${output}/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 => {
const page = route.handlers.find(({ type }) => type === 'page');
function generate_client(
manifest_data: ManifestData,
path_to_routes: string,
bundler: string,
dev: boolean,
dev_port?: number
) {
const template_file = path.resolve(__dirname, '../templates/client.js');
const template = fs.readFileSync(template_file, 'utf-8');
if (!page) {
return `{ pattern: ${route.pattern}, ignore: true }`;
const page_ids = new Set(manifest_data.pages.map(page =>
page.pattern.toString()));
const server_routes_to_ignore = manifest_data.server_routes.filter(route =>
!page_ids.has(route.pattern.toString()));
const component_indexes: Record<string, number> = {};
const components = `[
${manifest_data.components.map((component, i) => {
const annotation = bundler === 'webpack'
? `/* webpackChunkName: "${component.name}" */ `
: '';
const source = get_file(path_to_routes, component);
component_indexes[component.name] = i;
return `{
js: () => import(${annotation}${stringify(source)}),
css: "__SAPPER_CSS_PLACEHOLDER:${stringify(component.file, false)}__"
}`;
}).join(',\n\t\t')}
]`.replace(/^\t/gm, '').trim();
let needs_decode = false;
let pages = `[
${manifest_data.pages.map(page => `{
// ${page.parts[page.parts.length - 1].component.file}
pattern: ${page.pattern},
parts: [
${page.parts.map(part => {
if (part === null) return 'null';
if (part.params.length > 0) {
needs_decode = true;
const props = part.params.map((param, i) => `${param}: d(match[${i + 1}])`);
return `{ i: ${component_indexes[part.component.name]}, params: match => ({ ${props.join(', ')} }) }`;
}
const file = posixify(`${path_to_routes}/${page.file}`);
return `{ i: ${component_indexes[part.component.name]} }`;
}).join(',\n\t\t\t\t')}
]
}`).join(',\n\n\t\t')}
]`.replace(/^\t/gm, '').trim();
if (route.id === '_error') {
return `{ error: true, load: () => import(/* webpackChunkName: "${route.id}" */ '${file}') }`;
}
if (needs_decode) {
pages = `(d => ${pages})(decodeURIComponent)`
}
const params = route.params.length === 0
? '{}'
: `{ ${route.params.map((part, i) => `${part}: match[${i + 1}]`).join(', ')} }`;
let footer = '';
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()) {
if (dev) {
const sapper_dev_client = posixify(
path.resolve(__dirname, '../sapper-dev-client.js')
);
code += `
footer = `
if (module.hot) {
import('${sapper_dev_client}').then(client => {
if (typeof window !== 'undefined') {
import(${stringify(sapper_dev_client)}).then(client => {
client.connect(${dev_port});
});
}`.replace(/^\t{3}/gm, '');
}
return code;
return `// This file is generated by Sapper — do not edit it!\n` + template
.replace('__ROOT__', stringify(get_file(path_to_routes, manifest_data.root), false))
.replace('__ERROR__', stringify(posixify(`${path_to_routes}/_error.html`), false))
.replace('__IGNORE__', `[${server_routes_to_ignore.map(route => route.pattern).join(', ')}]`)
.replace('__COMPONENTS__', components)
.replace('__PAGES__', pages) +
footer;
}
function generate_server(routes: Route[], path_to_routes: string) {
function generate_server(
manifest_data: ManifestData,
path_to_routes: string,
cwd: string,
src: string,
dest: string,
dev: boolean
) {
const template_file = path.resolve(__dirname, '../templates/server.js');
const template = fs.readFileSync(template_file, 'utf-8');
const imports = [].concat(
manifest_data.server_routes.map(route =>
`import * as __${route.name} from ${stringify(posixify(`${path_to_routes}/${route.file}`))};`),
manifest_data.components.map(component =>
`import __${component.name} from ${stringify(get_file(path_to_routes, component))};`),
`import root from ${stringify(get_file(path_to_routes, manifest_data.root))};`,
`import error from ${stringify(posixify(`${path_to_routes}/_error.html`))};`
);
let code = `
// This file is generated by Sapper — do not edit it!
${routes
.map(route =>
route.handlers
.map(({ type, file }, index) => {
const module = posixify(`${path_to_routes}/${file}`);
${imports.join('\n')}
return type === 'page'
? `import ${route.id}${index} from '${module}';`
: `import * as ${route.id}${index} from '${module}';`;
})
.join('\n')
)
.join('\n')}
const d = decodeURIComponent;
export const routes = [
${routes
.map(route => {
const handlers = route.handlers
.map(({ type }, index) =>
`{ type: '${type}', module: ${route.id}${index} }`)
.join(', ');
export const manifest = {
server_routes: [
${manifest_data.server_routes.map(route => `{
// ${route.file}
pattern: ${route.pattern},
handlers: __${route.name},
params: ${route.params.length > 0
? `match => ({ ${route.params.map((param, i) => `${param}: d(match[${i + 1}])`).join(', ')} })`
: `() => ({})`}
}`).join(',\n\n\t\t\t\t')}
],
if (route.id === '_error') {
return `{ error: true, handlers: [${handlers}] }`;
}
pages: [
${manifest_data.pages.map(page => `{
// ${page.parts[page.parts.length - 1].component.file}
pattern: ${page.pattern},
parts: [
${page.parts.map(part => {
if (part === null) return 'null';
const params = route.params.length === 0
? '{}'
: `{ ${route.params.map((part, i) => `${part}: match[${i + 1}]`).join(', ')} }`;
const props = [
`name: "${part.component.name}"`,
`file: ${stringify(part.component.file)}`,
`component: __${part.component.name}`
];
return `{ id: '${route.id}', pattern: ${route.pattern}, params: ${route.params.length > 0 ? `match` : `()`} => (${params}), handlers: [${handlers}] }`;
})
.join(',\n\t')
}
];`.replace(/^\t\t/gm, '').trim();
if (part.params.length > 0) {
const params = part.params.map((param, i) => `${param}: d(match[${i + 1}])`);
props.push(`params: match => ({ ${params.join(', ')} })`);
}
return code;
}
return `{ ${props.join(', ')} }`;
}).join(',\n\t\t\t\t\t\t')}
]
}`).join(',\n\n\t\t\t\t')}
],
root,
error
};`.replace(/^\t\t/gm, '').trim();
const build_dir = posixify(path.relative(cwd, dest));
const src_dir = posixify(path.relative(cwd, src));
return `// This file is generated by Sapper — do not edit it!\n` + template
.replace('__BUILD__DIR__', JSON.stringify(build_dir))
.replace('__SRC__DIR__', JSON.stringify(src_dir))
.replace('__DEV__', dev ? 'true' : 'false')
.replace(/const manifest = __MANIFEST__;/, code);
}
function get_file(path_to_routes: string, component: PageComponent) {
if (component.default) {
return `./_layout.html`;
}
return posixify(`${path_to_routes}/${component.file}`);
}

View File

@@ -1,159 +0,0 @@
import * as path from 'path';
import glob from 'glob';
import { locations } from '../config';
import { Route } from '../interfaces';
export default function create_routes({ files } = { files: glob.sync('**/*.*', { cwd: locations.routes(), dot: true, nodir: true }) }) {
const routes: Route[] = files
.filter((file: string) => !/(^|\/|\\)(_(?!error\.html)|\.(?!well-known))/.test(file))
.map((file: string) => {
if (/]\[/.test(file)) {
throw new Error(`Invalid route ${file} — parameters must be separated`);
}
if (file === '4xx.html' || file === '5xx.html') {
throw new Error('As of Sapper 0.14, 4xx.html and 5xx.html should be replaced with _error.html');
}
const base = file.replace(/\.[^/.]+$/, '');
const parts = base.split('/'); // glob output is always posix-style
if (/^index(\..+)?/.test(parts[parts.length - 1])) {
const part = parts.pop();
if (parts.length > 0) parts[parts.length - 1] += part.slice(5);
}
return {
files: [file],
base,
parts
};
})
.filter(Boolean)
.filter((a, index, array) => {
const found = array.slice(index + 1).find(b => a.base === b.base);
if (found) found.files.push(...a.files);
return !found;
})
.sort((a, b) => {
if (a.parts[0] === '_error') return -1;
if (b.parts[0] === '_error') 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.base} and ${b.base} routes clash`);
})
.map(({ files, base, parts }) => {
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 = encodeURI(parts[i].normalize()).replace(/\?/g, '%3F').replace(/#/g, '%23').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,
handlers: files.map(file => ({
type: path.extname(file) === '.html' ? 'page' : 'route',
file
})).sort((a, b) => {
if (a.type === 'page' && b.type === 'route') {
return 1;
}
if (a.type === 'route' && b.type === 'page') {
return -1;
}
return 0;
}),
pattern,
test,
exec,
parts,
params
};
});
return routes;
}
function get_sub_parts(part: string) {
return part.split(/[\[\]]/)
.map((content, i) => {
if (!content) return null;
return {
content,
dynamic: i % 2 === 1
};
})
.filter(Boolean);
}

16
src/core/read_template.ts Normal file
View File

@@ -0,0 +1,16 @@
import * as fs from 'fs';
export default function read_template(dir: string) {
try {
return fs.readFileSync(`${dir}/template.html`, 'utf-8');
} catch (err) {
if (fs.existsSync(`app/template.html`)) {
throw new Error(`As of Sapper 0.21, the default folder structure has been changed:
app/ --> src/
routes/ --> src/routes/
assets/ --> static/`);
}
throw err;
}
}

View File

@@ -1,25 +0,0 @@
import * as sander from 'sander';
const previous_contents = new Map();
export function write_if_changed(file: string, code: string) {
if (code !== previous_contents.get(file)) {
previous_contents.set(file, code);
sander.writeFileSync(file, code);
fudge_mtime(file);
}
}
export function posixify(file: string) {
return file.replace(/[/\\]/g, '/');
}
export function fudge_mtime(file: string) {
// need to fudge the mtime so that webpack doesn't go doolally
const { atime, mtime } = sander.statSync(file);
sander.utimesSync(
file,
new Date(atime.getTime() - 999999),
new Date(mtime.getTime() - 999999)
);
}

View File

@@ -1,3 +1,6 @@
import * as child_process from 'child_process';
import { CompileResult } from './core/create_compilers/interfaces';
export type Route = {
id: string;
handlers: {
@@ -18,4 +21,80 @@ export type Template = {
export type Store = {
get: () => any;
};
};
export type PageComponent = {
default?: boolean;
name: string;
file: string;
};
export type Page = {
pattern: RegExp;
parts: Array<{
component: PageComponent;
params: string[];
}>
};
export type ServerRoute = {
name: string;
pattern: RegExp;
file: string;
params: string[];
};
export type Dirs = {
dest: string,
src: string,
routes: string
};
export type ManifestData = {
root: PageComponent;
components: PageComponent[];
pages: Page[];
server_routes: ServerRoute[];
};
export type ReadyEvent = {
port: number;
process: child_process.ChildProcess;
};
export type ErrorEvent = {
type: string;
message: string;
};
export type FatalEvent = {
message: string;
};
export type InvalidEvent = {
changed: string[];
invalid: {
client: boolean;
server: boolean;
serviceworker: boolean;
}
};
export type BuildEvent = {
type: string;
errors: Array<{ file: string, message: string, duplicate: boolean }>;
warnings: Array<{ file: string, message: string, duplicate: boolean }>;
duration: number;
result: CompileResult;
};
export type FileEvent = {
file: string;
size: number;
};
export type FailureEvent = {
};
export type DoneEvent = {};

View File

@@ -1,438 +0,0 @@
import * as fs from 'fs';
import * as path from 'path';
import { URL } from 'url';
import { ClientRequest, ServerResponse } from 'http';
import cookie from 'cookie';
import devalue from 'devalue';
import fetch from 'node-fetch';
import { lookup } from './middleware/mime';
import { locations, dev } from './config';
import sourceMapSupport from 'source-map-support';
sourceMapSupport.install();
type RouteObject = {
id: string;
type: 'page' | 'route';
pattern: RegExp;
params: (match: RegExpMatchArray) => Record<string, string>;
module: Component;
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>;
}
interface Component {
render: (data: any, opts: { store: Store }) => {
head: string;
css: { code: string, map: any };
html: string
},
preload: (data: any) => any | Promise<any>
}
export default function middleware({ App, routes, store }: {
App: Component,
routes: RouteObject[],
store: (req: Req) => Store
}) {
if (!App) {
throw new Error(`As of 0.12, you must supply an App component to Sapper — see https://sapper.svelte.technology/guide#0-11-to-0-12 for more information`);
}
const output = locations.dest();
let emitted_basepath = false;
const middleware = compose_handlers([
(req: Req, res: ServerResponse, next: () => void) => {
if (req.baseUrl === undefined) {
let { originalUrl } = req;
if (req.url === '/' && originalUrl[originalUrl.length - 1] !== '/') {
originalUrl += '/';
}
req.baseUrl = originalUrl
? originalUrl.slice(0, -req.url.length)
: '';
}
if (!emitted_basepath && process.send) {
process.send({
__sapper__: true,
event: 'basepath',
basepath: req.baseUrl
});
emitted_basepath = true;
}
if (req.path === undefined) {
req.path = req.url.replace(/\?.*/, '');
}
next();
},
fs.existsSync(path.join(output, 'index.html')) && serve({
pathname: '/index.html',
cache_control: 'max-age=600'
}),
fs.existsSync(path.join(output, 'service-worker.js')) && serve({
pathname: '/service-worker.js',
cache_control: 'max-age=600'
}),
fs.existsSync(path.join(output, 'service-worker.js.map')) && serve({
pathname: '/service-worker.js.map',
cache_control: 'max-age=600'
}),
serve({
prefix: '/client/',
cache_control: 'max-age=31536000'
}),
get_route_handler(App, 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();
}
};
}
function get_route_handler(App: Component, routes: RouteObject[], store_getter: (req: Req) => Store) {
const output = locations.dest();
const get_chunks = dev()
? () => JSON.parse(fs.readFileSync(path.join(output, 'client_assets.json'), 'utf-8'))
: (assets => () => assets)(JSON.parse(fs.readFileSync(path.join(output, 'client_assets.json'), 'utf-8')));
const template = dev()
? () => fs.readFileSync(`${locations.app()}/template.html`, 'utf-8')
: (str => () => str)(fs.readFileSync(`${locations.dest()}/template.html`, 'utf-8'));
const error_route = routes.find((route: RouteObject) => route.error);
function handle_route(route: RouteObject, req: Req, res: ServerResponse, status = 200, error: Error | string = null) {
req.params = error
? {}
: route.params(route.pattern.exec(req.path));
const handlers = route.handlers[Symbol.iterator]();
function next() {
const chunks: Record<string, string> = get_chunks();
try {
const { value: handler, done } = handlers.next();
if (done) {
if (route.error) {
// there was an error rendering the error page!
res.statusCode = status;
res.end(error instanceof Error ? error.message : error);
} else {
handle_route(error_route, req, res, 404, 'Not found');
}
return;
}
const mod = handler.module;
if (handler.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] || chunks._error) // TODO this is gross
.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 props = { params: req.params, query: req.query, path: req.path };
if (route.error) {
props.error = error instanceof Error ? error : { message: error };
props.status = status;
}
let redirect: { statusCode: number, location: string };
let preload_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) => {
preload_error = { statusCode, message };
},
fetch: (url: string, opts?: any) => {
const parsed = new URL(url, `http://127.0.0.1:${process.env.PORT}${req.baseUrl ? req.baseUrl + '/' :''}`);
if (opts) {
opts = Object.assign({}, opts);
const include_cookies = (
opts.credentials === 'include' ||
opts.credentials === 'same-origin' && parsed.origin === `http://127.0.0.1:${process.env.PORT}`
);
if (include_cookies) {
const cookies: Record<string, string> = {};
if (!opts.headers) opts.headers = {};
const str = []
.concat(
cookie.parse(req.headers.cookie || ''),
cookie.parse(opts.headers.cookie || ''),
cookie.parse(res.getHeader('Set-Cookie') || '')
)
.map(cookie => {
return Object.keys(cookie)
.map(name => `${name}=${encodeURIComponent(cookie[name])}`)
.join('; ');
})
.filter(Boolean)
.join(', ');
opts.headers.cookie = str;
}
}
return fetch(parsed.href, opts);
},
store
}, req) : {}
).catch(err => {
preload_error = { statusCode: 500, message: err };
}).then(preloaded => {
if (redirect) {
res.statusCode = redirect.statusCode;
res.setHeader('Location', `${req.baseUrl}/${redirect.location}`);
res.end();
return;
}
if (preload_error) {
handle_route(error_route, req, res, preload_error.statusCode, preload_error.message);
return;
}
const serialized = {
preloaded: mod.preload && try_serialize(preloaded),
store: store && try_serialize(store.get())
};
Object.assign(props, preloaded);
const { html, head, css } = App.render({ Page: mod, props }, {
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.statusCode = status;
res.end(page);
if (process.send) {
process.send({
__sapper__: true,
event: 'file',
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 handle_method = mod[method_export];
if (handle_method) {
if (process.env.SAPPER_EXPORT) {
const { write, end, setHeader } = res;
const chunks: any[] = [];
const headers: Record<string, string> = {};
// intercept data so that it can be exported
res.write = function(chunk: any) {
chunks.push(new Buffer(chunk));
write.apply(res, arguments);
};
res.setHeader = function(name: string, value: string) {
headers[name.toLowerCase()] = value;
setHeader.apply(res, arguments);
};
res.end = function(chunk?: any) {
if (chunk) chunks.push(new Buffer(chunk));
end.apply(res, arguments);
process.send({
__sapper__: true,
event: 'file',
url: req.url,
method: req.method,
status: res.statusCode,
type: headers['content-type'],
body: Buffer.concat(chunks).toString()
});
};
}
const handle_bad_result = (err?: Error) => {
if (err) {
console.error(err.stack);
res.statusCode = 500;
res.end(err.message);
} else {
process.nextTick(next);
}
};
try {
handle_method(req, res, handle_bad_result);
} catch (err) {
handle_bad_result(err);
}
} else {
// no matching handler for method
process.nextTick(next);
}
}
} catch (error) {
if (route.error) {
// there was an error rendering the error page!
res.statusCode = status;
res.end(error instanceof Error ? error.message : error);
} else {
handle_route(error_route, req, res, 500, error || 'Internal server error');
}
}
}
next();
}
return function find_route(req: Req, res: ServerResponse) {
for (const route of routes) {
if (!route.error && route.pattern.test(req.path)) return handle_route(route, req, res);
}
handle_route(error_route, req, res, 404, 'Not found');
};
}
function compose_handlers(handlers: Handler[]) {
return (req: Req, res: ServerResponse, next: () => void) => {
let i = 0;
function go() {
const handler = handlers[i];
if (handler) {
handler(req, res, () => {
i += 1;
go();
});
} else {
next();
}
}
go();
};
}
function try_serialize(data: any) {
try {
return devalue(data);
} catch (err) {
return null;
}
}

View File

@@ -1,330 +0,0 @@
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 App: ComponentConstructor;
export let component: Component;
let target: Node;
let store: Store;
let routes: Route[];
let error_route: 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 path = url.pathname.slice(manifest.baseUrl.length);
for (const route of routes) {
const match = route.pattern.exec(path);
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, props: { params, query, path } };
}
}
}
let current_token: {};
function render(Page: ComponentConstructor, props: any, scroll: ScrollPosition, token: {}) {
if (current_token !== token) return;
const data = {
Page,
props,
preloading: false
};
if (component) {
component.set(data);
} else {
// first load — remove SSR'd <head> contents
const start = document.querySelector('#sapper-head-start');
const end = document.querySelector('#sapper-head-end');
if (start && end) {
while (start.nextSibling !== end) detach(start.nextSibling);
detach(start);
detach(end);
}
component = new App({
target,
data,
store,
hydrate: true
});
}
if (scroll) {
window.scrollTo(scroll.x, scroll.y);
}
}
function prepare_route(Page: ComponentConstructor, props: RouteData) {
let redirect: { statusCode: number, location: string } = null;
let error: { statusCode: number, message: Error | string } = null;
if (!Page.preload) {
return { Page, props, redirect, error };
}
if (!component && manifest.preloaded) {
return { Page, props: Object.assign(props, manifest.preloaded), redirect, error };
}
if (component) {
component.set({
preloading: true
});
}
return Promise.resolve(Page.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 };
}
}, props)).catch(err => {
error = { statusCode: 500, message: err };
}).then(preloaded => {
if (error) {
return error_route.load().then(({ default: Page }: { default: ComponentConstructor }) => {
const err = error.message instanceof Error ? error.message : new Error(error.message);
Object.assign(props, { status: error.statusCode, error: err });
return { Page, props, redirect: null };
});
}
Object.assign(props, preloaded)
return { Page, props, redirect };
});
}
function navigate(target: Target, id: number): Promise<any> {
if (id) {
// popstate or initial navigation
cid = id;
} else {
// clicked on a link. preserve scroll state
scroll_history[cid] = scroll_state();
id = cid = ++uid;
scroll_history[cid] = { x: 0, y: 0 };
}
cid = id;
const loaded = prefetching && prefetching.href === target.url.href ?
prefetching.promise :
target.route.load().then(mod => prepare_route(mod.default, target.props));
prefetching = null;
const token = current_token = {};
return loaded.then(({ Page, props, redirect }) => {
if (redirect) {
return goto(redirect.location, { replaceState: true });
}
render(Page, props, scroll_history[id], token);
document.activeElement.blur();
});
}
function handle_click(event: MouseEvent) {
// Adapted from https://github.com/visionmedia/page.js
// MIT license https://github.com/visionmedia/page.js#license
if (which(event) !== 1) return;
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
if (event.defaultPrevented) return;
const a: HTMLAnchorElement | SVGAElement = <HTMLAnchorElement | SVGAElement>findAnchor(<Node>event.target);
if (!a) return;
// check if link is inside an svg
// in this case, both href and target are always inside an object
const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
const href = String(svg ? (<SVGAElement>a).href.baseVal : a.href);
if (href === window.location.href) {
event.preventDefault();
return;
}
// Ignore if tag has
// 1. 'download' attribute
// 2. rel='external' attribute
if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
// Ignore if <a> has a target
if (svg ? (<SVGAElement>a).target.baseVal : a.target) return;
const url = new URL(href);
// Don't handle hash changes
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
const target = select_route(url);
if (target) {
navigate(target, null);
event.preventDefault();
history.pushState({ id: cid }, '', url.href);
}
}
function handle_popstate(event: PopStateEvent) {
scroll_history[cid] = scroll_state();
if (event.state) {
const url = new URL(window.location.href);
const target = select_route(url);
if (target) {
navigate(target, event.state.id);
} else {
window.location.href = window.location.href;
}
} else {
// hashchange
cid = ++uid;
history.replaceState({ id: cid }, '', window.location.href);
}
}
let prefetching: {
href: string;
promise: Promise<{ Page: ComponentConstructor, props: any }>;
} = null;
export function prefetch(href: string) {
const selected = select_route(new URL(href, document.baseURI));
if (selected && (!prefetching || href !== prefetching.href)) {
prefetching = {
href,
promise: selected.route.load().then(mod => prepare_route(mod.default, selected.props))
};
}
}
let mousemove_timeout: NodeJS.Timer;
function handle_mousemove(event: MouseEvent) {
clearTimeout(mousemove_timeout);
mousemove_timeout = setTimeout(() => {
trigger_prefetch(event);
}, 20);
}
function trigger_prefetch(event: MouseEvent | TouchEvent) {
const a: HTMLAnchorElement = <HTMLAnchorElement>findAnchor(<Node>event.target);
if (!a || a.rel !== 'prefetch') return;
prefetch(a.href);
}
let inited: boolean;
export function init(opts: { App: ComponentConstructor, target: Node, routes: Route[], store?: (data: any) => Store }) {
if (opts instanceof HTMLElement) {
throw new Error(`The signature of init(...) has changed — see https://sapper.svelte.technology/guide#0-11-to-0-12 for more information`);
}
App = opts.App;
target = opts.target;
routes = opts.routes.filter(r => !r.error);
error_route = opts.routes.find(r => r.error);
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', trigger_prefetch);
window.addEventListener('mousemove', handle_mousemove);
inited = true;
}
return Promise.resolve().then(() => {
const { hash, href } = window.location;
const deep_linked = hash && document.getElementById(hash.slice(1));
scroll_history[uid] = deep_linked ?
{ x: 0, y: deep_linked.getBoundingClientRect().top } :
scroll_state();
history.replaceState({ id: uid }, '', href);
const target = select_route(new URL(window.location.href));
if (target) return navigate(target, uid);
});
}
export function goto(href: string, opts = { replaceState: false }) {
const target = select_route(new URL(href, document.baseURI));
let promise;
if (target) {
promise = navigate(target, null);
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
} else {
window.location.href = href;
promise = new Promise(f => {}); // never resolves
}
return promise;
}
export function prefetchRoutes(pathnames: string[]) {
if (!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 };

View File

@@ -1,34 +0,0 @@
import { Store } from '../interfaces';
export { Store };
export type Params = Record<string, string>;
export type Query = Record<string, string | true>;
export type RouteData = { params: Params, query: Query, path: string };
export interface ComponentConstructor {
new (options: { target: Node, data: any, store: Store, hydrate: boolean }): Component;
preload: (props: { params: Params, query: Query }) => Promise<any>;
};
export interface Component {
destroy: () => void;
}
export type Route = {
pattern: RegExp;
load: () => Promise<{ default: ComponentConstructor }>;
error?: boolean;
params?: (match: RegExpExecArray) => Record<string, string>;
ignore?: boolean;
};
export type ScrollPosition = {
x: number;
y: number;
};
export type Target = {
url: URL;
route: Route;
props: RouteData;
};

View File

@@ -1,19 +0,0 @@
export function detach(node: Node) {
node.parentNode.removeChild(node);
}
export function findAnchor(node: Node) {
while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG <a> elements have a lowercase name
return node;
}
export function which(event: MouseEvent) {
return event.which === null ? event.button : event.which;
}
export function scroll_state() {
return {
x: window.scrollX,
y: window.scrollY
};
}

119
src/utils.ts Normal file
View File

@@ -0,0 +1,119 @@
import * as fs from 'fs';
import * as path from 'path';
export function left_pad(str: string, len: number) {
while (str.length < len) str = ` ${str}`;
return str;
}
export function repeat(str: string, i: number) {
let result = '';
while (i--) result += str;
return result;
}
export function format_milliseconds(ms: number) {
if (ms < 1000) return `${ms}ms`;
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
const minutes = ~~(ms / 60000);
const seconds = Math.round((ms % 60000) / 1000);
return `${minutes}m${seconds < 10 ? '0' : ''}${seconds}s`;
}
export function elapsed(start: number) {
return format_milliseconds(Date.now() - start);
}
export function walk(cwd: string, dir = cwd, files: string[] = []) {
fs.readdirSync(dir).forEach(file => {
const resolved = path.resolve(dir, file);
if (fs.statSync(resolved).isDirectory()) {
walk(cwd, resolved, files);
} else {
files.push(posixify(path.relative(cwd, resolved)));
}
});
return files;
}
export function posixify(str: string) {
return str.replace(/\\/g, '/');
}
const previous_contents = new Map();
export function write_if_changed(file: string, code: string) {
if (code !== previous_contents.get(file)) {
previous_contents.set(file, code);
fs.writeFileSync(file, code);
fudge_mtime(file);
}
}
export function stringify(string: string, includeQuotes: boolean = true) {
const quoted = JSON.stringify(string);
return includeQuotes ? quoted : quoted.slice(1, -1);
}
export function fudge_mtime(file: string) {
// need to fudge the mtime so that webpack doesn't go doolally
const { atime, mtime } = fs.statSync(file);
fs.utimesSync(
file,
new Date(atime.getTime() - 999999),
new Date(mtime.getTime() - 999999)
);
}
export const reserved_words = new Set([
'arguments',
'await',
'break',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'enum',
'eval',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'implements',
'import',
'in',
'instanceof',
'interface',
'let',
'new',
'null',
'package',
'private',
'protected',
'public',
'return',
'static',
'super',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
'yield',
]);

395
templates/src/client/app.ts Normal file
View File

@@ -0,0 +1,395 @@
import RootComponent from '__ROOT__';
import ErrorComponent from '__ERROR__';
import {
Target,
ScrollPosition,
Component,
Redirect,
ComponentLoader,
ComponentConstructor,
RootProps,
Page
} from './types';
import goto from './goto';
const ignore = __IGNORE__;
export const components: ComponentLoader[] = __COMPONENTS__;
export const pages: Page[] = __PAGES__;
let ready = false;
let root_component: Component;
let segments: string[] = [];
let current_token: {};
let root_preload: Promise<any>;
let root_data: any;
const root_props: RootProps = {
path: null,
params: null,
query: null,
child: {
segment: null,
component: null,
props: {}
}
};
export let prefetching: {
href: string;
promise: Promise<{ redirect?: Redirect, data?: any, nullable_depth?: number, new_segments?: any }>;
} = null;
export function set_prefetching(href, promise) {
prefetching = { href, promise };
}
export let store;
export function set_store(fn) {
store = fn(initial_data.store);
}
export let target: Node;
export function set_target(element) {
target = element;
}
export let uid = 1;
export function set_uid(n) {
uid = n;
}
export let cid: number;
export function set_cid(n) {
cid = n;
}
export const initial_data = typeof __SAPPER__ !== 'undefined' && __SAPPER__;
const _history = typeof history !== 'undefined' ? history : {
pushState: (state: any, title: string, href: string) => {},
replaceState: (state: any, title: string, href: string) => {},
scrollRestoration: ''
};
export { _history as history };
export const scroll_history: Record<string, ScrollPosition> = {};
export function select_route(url: URL): Target {
if (url.origin !== location.origin) return null;
if (!url.pathname.startsWith(initial_data.baseUrl)) return null;
const path = url.pathname.slice(initial_data.baseUrl.length);
// avoid accidental clashes between server routes and pages
if (ignore.some(pattern => pattern.test(path))) return;
for (let i = 0; i < pages.length; i += 1) {
const page = pages[i];
const match = page.pattern.exec(path);
if (match) {
const query: Record<string, string | string[]> = Object.create(null);
if (url.search.length > 0) {
url.search.slice(1).split('&').forEach(searchParam => {
let [, key, value] = /([^=]*)(?:=(.*))?/.exec(decodeURIComponent(searchParam));
value = (value || '').replace(/\+/g, ' ');
if (typeof query[key] === 'string') query[key] = [<string>query[key]];
if (typeof query[key] === 'object') query[key].push(value);
else query[key] = value;
});
}
return { url, path, page, match, query };
}
}
}
export function scroll_state() {
return {
x: pageXOffset,
y: pageYOffset
};
}
export function navigate(target: Target, id: number, noscroll?: boolean, hash?: string): Promise<any> {
let scroll: ScrollPosition;
if (id) {
// popstate or initial navigation
cid = id;
} else {
const current_scroll = scroll_state();
// clicked on a link. preserve scroll state
scroll_history[cid] = current_scroll;
id = cid = ++uid;
scroll_history[cid] = noscroll ? current_scroll : { x: 0, y: 0 };
}
cid = id;
if (root_component) {
root_component.set({ preloading: true });
}
const loaded = prefetching && prefetching.href === target.url.href ?
prefetching.promise :
prepare_page(target);
prefetching = null;
const token = current_token = {};
return loaded.then(({ redirect, data, nullable_depth, new_segments }) => {
if (redirect) {
return goto(redirect.location, { replaceState: true });
}
if (new_segments) {
segments = new_segments;
}
render(data, nullable_depth, scroll_history[id], noscroll, hash, token);
if (document.activeElement) document.activeElement.blur();
});
}
function render(data: any, nullable_depth: number, scroll: ScrollPosition, noscroll: boolean, hash: string, token: {}) {
if (current_token !== token) return;
if (root_component) {
// first, clear out highest-level root component
let level = data.child;
for (let i = 0; i < nullable_depth; i += 1) {
if (i === nullable_depth) break;
level = level.props.child;
}
const { component } = level;
level.component = null;
root_component.set({ child: data.child });
// then render new stuff
level.component = component;
root_component.set(data);
} else {
// first load — remove SSR'd <head> contents
const start = document.querySelector('#sapper-head-start');
const end = document.querySelector('#sapper-head-end');
if (start && end) {
while (start.nextSibling !== end) detach(start.nextSibling);
detach(start);
detach(end);
}
Object.assign(data, root_data);
root_component = new RootComponent({
target,
data,
store,
hydrate: true
});
}
if (!noscroll) {
if (hash) {
// scroll is an element id (from a hash), we need to compute y.
const deep_linked = document.querySelector(hash);
if (deep_linked) {
scroll = {
x: 0,
y: deep_linked.getBoundingClientRect().top
};
}
}
scroll_history[cid] = scroll;
if (scroll) scrollTo(scroll.x, scroll.y);
}
Object.assign(root_props, data);
ready = true;
}
export function prepare_page(target: Target): Promise<{
redirect?: Redirect;
data?: any;
nullable_depth?: number;
}> {
const { page, path, query } = target;
const new_segments = path.split('/').filter(Boolean);
let changed_from = 0;
while (
segments[changed_from] &&
new_segments[changed_from] &&
segments[changed_from] === new_segments[changed_from]
) changed_from += 1;
if (changed_from === new_segments.length) {
changed_from -= 1;
}
let redirect: Redirect = null;
let error: { statusCode: number, message: Error | string } = null;
const preload_context = {
store,
fetch: (url: string, opts?: any) => fetch(url, opts),
redirect: (statusCode: number, location: string) => {
if (redirect && (redirect.statusCode !== statusCode || redirect.location !== location)) {
throw new Error(`Conflicting redirects`);
}
redirect = { statusCode, location };
},
error: (statusCode: number, message: Error | string) => {
error = { statusCode, message };
}
};
if (!root_preload) {
root_preload = RootComponent.preload
? initial_data.preloaded[0] || RootComponent.preload.call(preload_context, {
path,
query,
params: {}
})
: {};
}
return Promise.all(page.parts.map((part, i) => {
if (i < changed_from) return null;
if (!part) return null;
return load_component(components[part.i]).then(Component => {
const req = {
path,
query,
params: part.params ? part.params(target.match) : {}
};
let preloaded;
if (ready || !initial_data.preloaded[i + 1]) {
preloaded = Component.preload
? Component.preload.call(preload_context, req)
: {};
} else {
preloaded = initial_data.preloaded[i + 1];
}
return Promise.resolve(preloaded).then(preloaded => {
return { Component, preloaded };
});
});
})).catch(err => {
error = { statusCode: 500, message: err };
return [];
}).then(results => {
if (root_data) {
return results;
} else {
return Promise.resolve(root_preload).then(value => {
root_data = value;
return results;
});
}
}).then(results => {
if (redirect) {
return { redirect, new_segments };
}
const get_params = page.parts[page.parts.length - 1].params || (() => ({}));
const params = get_params(target.match);
if (error) {
const props = {
path,
query,
params,
error: typeof error.message === 'string' ? new Error(error.message) : error.message,
status: error.statusCode
};
return {
new_segments,
data: Object.assign({}, props, {
preloading: false,
child: {
component: ErrorComponent,
props
}
})
};
}
const props = { path, query, error: null, status: null };
const data = {
path,
preloading: false,
child: Object.assign({}, root_props.child, {
segment: new_segments[0]
})
};
if (changed(query, root_props.query)) data.query = query;
if (changed(params, root_props.params)) data.params = params;
let level = data.child;
let nullable_depth = 0;
for (let i = 0; i < page.parts.length; i += 1) {
const part = page.parts[i];
if (!part) continue;
const get_params = part.params || (() => ({}));
if (i < changed_from) {
level.props.path = path;
level.props.query = query;
level.props.child = Object.assign({}, level.props.child);
nullable_depth += 1;
} else {
level.component = results[i].Component;
level.props = Object.assign({}, level.props, props, {
params: get_params(target.match),
}, results[i].preloaded);
level.props.child = {};
}
level = level.props.child;
level.segment = new_segments[i + 1];
}
return { data, nullable_depth, new_segments };
});
}
function load_css(chunk: string) {
const href = `client/${chunk}`;
if (document.querySelector(`link[href="${href}"]`)) return;
return new Promise((fulfil, reject) => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
link.onload = () => fulfil();
link.onerror = reject;
document.head.appendChild(link);
});
}
export function load_component(component: ComponentLoader): Promise<ComponentConstructor> {
// TODO this is temporary — once placeholders are
// always rewritten, scratch the ternary
const promises: Array<Promise<any>> = (typeof component.css === 'string' ? [] : component.css.map(load_css));
promises.unshift(component.js());
return Promise.all(promises).then(values => values[0].default);
}
function detach(node: Node) {
node.parentNode.removeChild(node);
}
function changed(a: Record<string, string | true>, b: Record<string, string | true>) {
return JSON.stringify(a) !== JSON.stringify(b);
}

View File

@@ -0,0 +1,13 @@
import { history, select_route, navigate, cid } from '../app';
export default function goto(href: string, opts = { replaceState: false }) {
const target = select_route(new URL(href, document.baseURI));
if (target) {
history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
return navigate(target, null).then(() => {});
}
location.href = href;
return new Promise(f => {}); // never resolves
}

View File

@@ -0,0 +1,4 @@
export { default as start } from './start/index';
export { default as goto } from './goto/index';
export { default as prefetch } from './prefetch/index';
export { default as prefetchRoutes } from './prefetchRoutes/index';

View File

@@ -0,0 +1,14 @@
import { select_route, prefetching, set_prefetching, prepare_page } from '../app';
import { Target } from '../types';
export default function prefetch(href: string) {
const target: Target = select_route(new URL(href, document.baseURI));
if (target) {
if (!prefetching || href !== prefetching.href) {
set_prefetching(href, prepare_page(target));
}
return prefetching.promise;
}
}

View File

@@ -0,0 +1,12 @@
import { components, pages, load_component } from "../app";
export default function prefetchRoutes(pathnames: string[]) {
return pages
.filter(route => {
if (!pathnames) return true;
return pathnames.some(pathname => route.pattern.test(pathname));
})
.reduce((promise: Promise<any>, route) => promise.then(() => {
return Promise.all(route.parts.map(part => part && load_component(components[part.i])));
}), Promise.resolve());
}

View File

@@ -0,0 +1,134 @@
import {
cid,
history,
initial_data,
navigate,
scroll_history,
scroll_state,
select_route,
set_store,
set_target,
uid,
set_uid,
set_cid
} from '../app';
import prefetch from '../prefetch/index';
import { Store, ScrollPosition } from '../types';
export default function start(opts: {
target: Node,
store?: (data: any) => Store
}) {
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
set_target(opts.target);
if (opts.store) set_store(opts.store);
addEventListener('click', handle_click);
addEventListener('popstate', handle_popstate);
// prefetch
addEventListener('touchstart', trigger_prefetch);
addEventListener('mousemove', handle_mousemove);
return Promise.resolve().then(() => {
const { hash, href } = location;
history.replaceState({ id: uid }, '', href);
if (!initial_data.error) {
const target = select_route(new URL(location.href));
if (target) return navigate(target, uid, false, hash);
}
});
}
let mousemove_timeout: NodeJS.Timer;
function handle_mousemove(event: MouseEvent) {
clearTimeout(mousemove_timeout);
mousemove_timeout = setTimeout(() => {
trigger_prefetch(event);
}, 20);
}
function trigger_prefetch(event: MouseEvent | TouchEvent) {
const a: HTMLAnchorElement = <HTMLAnchorElement>find_anchor(<Node>event.target);
if (!a || a.rel !== 'prefetch') return;
prefetch(a.href);
}
function handle_click(event: MouseEvent) {
// Adapted from https://github.com/visionmedia/page.js
// MIT license https://github.com/visionmedia/page.js#license
if (which(event) !== 1) return;
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
if (event.defaultPrevented) return;
const a: HTMLAnchorElement | SVGAElement = <HTMLAnchorElement | SVGAElement>find_anchor(<Node>event.target);
if (!a) return;
if (!a.href) return;
// check if link is inside an svg
// in this case, both href and target are always inside an object
const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
const href = String(svg ? (<SVGAElement>a).href.baseVal : a.href);
if (href === location.href) {
if (!location.hash) event.preventDefault();
return;
}
// Ignore if tag has
// 1. 'download' attribute
// 2. rel='external' attribute
if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
// Ignore if <a> has a target
if (svg ? (<SVGAElement>a).target.baseVal : a.target) return;
const url = new URL(href);
// Don't handle hash changes
if (url.pathname === location.pathname && url.search === location.search) return;
const target = select_route(url);
if (target) {
const noscroll = a.hasAttribute('sapper-noscroll');
navigate(target, null, noscroll, url.hash);
event.preventDefault();
history.pushState({ id: cid }, '', url.href);
}
}
function which(event: MouseEvent) {
return event.which === null ? event.button : event.which;
}
function find_anchor(node: Node) {
while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG <a> elements have a lowercase name
return node;
}
function handle_popstate(event: PopStateEvent) {
scroll_history[cid] = scroll_state();
if (event.state) {
const url = new URL(location.href);
const target = select_route(url);
if (target) {
navigate(target, event.state.id);
} else {
location.href = location.href;
}
} else {
// hashchange
set_uid(uid + 1);
set_cid(uid);
history.replaceState({ id: cid }, '', location.href);
}
}

View File

@@ -0,0 +1,68 @@
export type Params = Record<string, string>;
export type Query = Record<string, string | true>;
export type RouteData = { params: Params, query: Query, path: string };
type Child = {
segment?: string;
props?: any;
component?: Component;
};
export type RootProps = {
path: string;
params: Record<string, string>;
query: Record<string, string>;
child: Child;
};
export interface ComponentConstructor {
new (options: { target: Node, data: any, store: Store, hydrate: boolean }): Component;
preload: (props: { params: Params, query: Query }) => Promise<any>;
};
export interface Component {
set: (data: any) => void;
destroy: () => void;
}
export type ComponentLoader = {
js: () => Promise<{ default: ComponentConstructor }>,
css: string[]
};
export type Page = {
pattern: RegExp;
parts: Array<{
i: number;
params?: (match: RegExpExecArray) => Record<string, string>;
}>;
};
export type Manifest = {
ignore: RegExp[];
root: ComponentConstructor;
error: () => Promise<{ default: ComponentConstructor }>;
pages: Page[]
};
export type ScrollPosition = {
x: number;
y: number;
};
export type Target = {
url: URL;
path: string;
page: Page;
match: RegExpExecArray;
query: Record<string, string | string[]>;
};
export type Redirect = {
statusCode: number;
location: string;
};
export type Store = {
get: () => any;
}

View File

@@ -0,0 +1 @@
export { default as middleware } from './middleware/index';

View File

@@ -0,0 +1,359 @@
import * as fs from 'fs';
import * as path from 'path';
import cookie from 'cookie';
import devalue from 'devalue';
import fetch from 'node-fetch';
import { URL, resolve } from 'url';
import { build_dir, dev, src_dir, IGNORE } from '../placeholders';
import { Manifest, Page, Props, Req, Res, Store } from './types';
export function get_page_handler(
manifest: Manifest,
store_getter: (req: Req, res: Res) => Store
) {
const get_build_info = dev
? () => JSON.parse(fs.readFileSync(path.join(build_dir, 'build.json'), 'utf-8'))
: (assets => () => assets)(JSON.parse(fs.readFileSync(path.join(build_dir, 'build.json'), 'utf-8')));
const template = dev
? () => read_template(src_dir)
: (str => () => str)(read_template(build_dir));
const has_service_worker = fs.existsSync(path.join(build_dir, 'service-worker.js'));
const { server_routes, pages } = manifest;
const error_route = manifest.error;
function handle_error(req: Req, res: Res, statusCode: number, error: Error | string) {
handle_page({
pattern: null,
parts: [
{ name: null, component: error_route }
]
}, req, res, statusCode, error || new Error('Unknown error in preload function'));
}
async function handle_page(page: Page, req: Req, res: Res, status = 200, error: Error | string = null) {
const isSWIndexHtml = req.path === '/service-worker-index.html';
const build_info: {
bundler: 'rollup' | 'webpack',
shimport: string | null,
assets: Record<string, string | string[]>,
legacy_assets?: Record<string, string>
} = get_build_info();
res.setHeader('Content-Type', 'text/html');
res.setHeader('Cache-Control', dev ? 'no-cache' : 'max-age=600');
// preload main.js and current route
// TODO detect other stuff we can preload? images, CSS, fonts?
let preloaded_chunks = Array.isArray(build_info.assets.main) ? build_info.assets.main : [build_info.assets.main];
if (!error && !isSWIndexHtml) {
page.parts.forEach(part => {
if (!part) return;
// using concat because it could be a string or an array. thanks webpack!
preloaded_chunks = preloaded_chunks.concat(build_info.assets[part.name]);
});
}
if (build_info.bundler === 'rollup') {
// TODO add dependencies and CSS
const link = preloaded_chunks
.filter(file => file && !file.match(/\.map$/))
.map(file => `<${req.baseUrl}/client/${file}>;rel="modulepreload"`)
.join(', ');
res.setHeader('Link', link);
} else {
const link = preloaded_chunks
.filter(file => file && !file.match(/\.map$/))
.map((file) => {
const as = /\.css$/.test(file) ? 'style' : 'script';
return `<${req.baseUrl}/client/${file}>;rel="preload";as="${as}"`;
})
.join(', ');
res.setHeader('Link', link);
}
const store = store_getter ? store_getter(req, res) : null;
let redirect: { statusCode: number, location: string };
let preload_error: { statusCode: number, message: Error | string };
const preload_context = {
redirect: (statusCode: number, location: string) => {
if (redirect && (redirect.statusCode !== statusCode || redirect.location !== location)) {
throw new Error(`Conflicting redirects`);
}
location = location.replace(/^\//g, ''); // leading slash (only)
redirect = { statusCode, location };
},
error: (statusCode: number, message: Error | string) => {
preload_error = { statusCode, message };
},
fetch: (url: string, opts?: any) => {
const parsed = new URL(url, `http://127.0.0.1:${process.env.PORT}${req.baseUrl ? req.baseUrl + '/' :''}`);
if (opts) {
opts = Object.assign({}, opts);
const include_cookies = (
opts.credentials === 'include' ||
opts.credentials === 'same-origin' && parsed.origin === `http://127.0.0.1:${process.env.PORT}`
);
if (include_cookies) {
opts.headers = Object.assign({}, opts.headers);
const cookies = Object.assign(
{},
cookie.parse(req.headers.cookie || ''),
cookie.parse(opts.headers.cookie || '')
);
const set_cookie = res.getHeader('Set-Cookie');
(Array.isArray(set_cookie) ? set_cookie : [set_cookie]).forEach(str => {
const match = /([^=]+)=([^;]+)/.exec(<string>str);
if (match) cookies[match[1]] = match[2];
});
const str = Object.keys(cookies)
.map(key => `${key}=${cookies[key]}`)
.join('; ');
opts.headers.cookie = str;
}
}
return fetch(parsed.href, opts);
},
store
};
let preloaded;
let match;
try {
const root_preloaded = manifest.root.preload
? manifest.root.preload.call(preload_context, {
path: req.path,
query: req.query,
params: {}
})
: {};
match = error ? null : page.pattern.exec(req.path);
let toPreload = [root_preloaded];
if (!isSWIndexHtml) {
toPreload = toPreload.concat(page.parts.map(part => {
if (!part) return null;
return part.component.preload
? part.component.preload.call(preload_context, {
path: req.path,
query: req.query,
params: part.params ? part.params(match) : {}
})
: {};
}))
}
preloaded = await Promise.all(toPreload);
} catch (err) {
preload_error = { statusCode: 500, message: err };
preloaded = []; // appease TypeScript
}
try {
if (redirect) {
const location = resolve(req.baseUrl || '/', redirect.location);
res.statusCode = redirect.statusCode;
res.setHeader('Location', location);
res.end();
return;
}
if (preload_error) {
handle_error(req, res, preload_error.statusCode, preload_error.message);
return;
}
const serialized = {
preloaded: `[${preloaded.map(data => try_serialize(data)).join(',')}]`,
store: store && try_serialize(store.get())
};
const segments = req.path.split('/').filter(Boolean);
const props: Props = {
path: req.path,
query: req.query,
params: {},
child: null
};
if (error) {
props.error = error instanceof Error ? error : { message: error };
props.status = status;
}
const data = Object.assign({}, props, preloaded[0], {
params: {},
child: {
segment: segments[0]
}
});
let level = data.child;
if (isSWIndexHtml) {
level.props = Object.assign({}, props, {
params: {}
})
} else {
for (let i = 0; i < page.parts.length; i += 1) {
const part = page.parts[i];
if (!part) continue;
const get_params = part.params || (() => ({}));
Object.assign(level, {
component: part.component,
props: Object.assign({}, props, {
params: get_params(match)
}, preloaded[i + 1])
});
level.props.child = <Props["child"]>{
segment: segments[i + 1]
};
level = level.props.child;
}
}
const { html, head, css } = manifest.root.render(data, {
store
});
let script = `__SAPPER__={${[
error && `error:1`,
`baseUrl:"${req.baseUrl}"`,
serialized.preloaded && `preloaded:${serialized.preloaded}`,
serialized.store && `store:${serialized.store}`
].filter(Boolean).join(',')}};`;
if (has_service_worker) {
script += `if('serviceWorker' in navigator)navigator.serviceWorker.register('${req.baseUrl}/service-worker.js');`;
}
const file = [].concat(build_info.assets.main).filter(file => file && /\.js$/.test(file))[0];
const main = `${req.baseUrl}/client/${file}`;
if (build_info.bundler === 'rollup') {
if (build_info.legacy_assets) {
const legacy_main = `${req.baseUrl}/client/legacy/${build_info.legacy_assets.main}`;
script += `(function(){try{eval("async function x(){}");var main="${main}"}catch(e){main="${legacy_main}"};var s=document.createElement("script");try{new Function("if(0)import('')")();s.src=main;s.type="module";s.crossOrigin="use-credentials";}catch(e){s.src="${req.baseUrl}/client/shimport@${build_info.shimport}.js";s.setAttribute("data-main",main);}document.head.appendChild(s);}());`;
} else {
script += `var s=document.createElement("script");try{new Function("if(0)import('')")();s.src="${main}";s.type="module";s.crossOrigin="use-credentials";}catch(e){s.src="${req.baseUrl}/client/shimport@${build_info.shimport}.js";s.setAttribute("data-main","${main}")}document.head.appendChild(s)`;
}
} else {
script += `</script><script src="${main}">`;
}
let styles: string;
// TODO make this consistent across apps
// TODO embed build_info in placeholder.ts
if (build_info.css && build_info.css.main) {
const css_chunks = new Set();
if (build_info.css.main) css_chunks.add(build_info.css.main);
page.parts.forEach(part => {
if (!part) return;
const css_chunks_for_part = build_info.css.chunks[part.file];
if (css_chunks_for_part) {
css_chunks_for_part.forEach(file => {
css_chunks.add(file);
});
}
});
styles = Array.from(css_chunks)
.map(href => `<link rel="stylesheet" href="client/${href}">`)
.join('')
} else {
styles = (css && css.code ? `<style>${css.code}</style>` : '');
}
// users can set a CSP nonce using res.locals.nonce
const nonce_attr = (res.locals && res.locals.nonce) ? ` nonce="${res.locals.nonce}"` : '';
const body = template()
.replace('%sapper.base%', () => `<base href="${req.baseUrl}/">`)
.replace('%sapper.scripts%', () => `<script${nonce_attr}>${script}</script>`)
.replace('%sapper.html%', () => html)
.replace('%sapper.head%', () => `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`)
.replace('%sapper.styles%', () => styles);
res.statusCode = status;
res.end(body);
} catch(err) {
if (error) {
// we encountered an error while rendering the error page — oops
res.statusCode = 500;
res.end(`<pre>${escape_html(err.message)}</pre>`);
} else {
handle_error(req, res, 500, err);
}
}
}
return function find_route(req: Req, res: Res, next: () => void) {
if (req[IGNORE]) return next();
if (req.path === '/service-worker-index.html') {
const homePage = pages.find(page => page.pattern.test('/'));
handle_page(homePage, req, res);
return;
}
if (!server_routes.some(route => route.pattern.test(req.path))) {
for (const page of pages) {
if (page.pattern.test(req.path)) {
handle_page(page, req, res);
return;
}
}
}
handle_error(req, res, 404, 'Not found');
};
}
function read_template(dir = build_dir) {
return fs.readFileSync(`${dir}/template.html`, 'utf-8');
}
function try_serialize(data: any) {
try {
return devalue(data);
} catch (err) {
return null;
}
}
function escape_html(html: string) {
const chars: Record<string, string> = {
'"' : 'quot',
"'": '#39',
'&': 'amp',
'<' : 'lt',
'>' : 'gt'
};
return html.replace(/["'&<>]/g, c => `&${chars[c]};`);
}

View File

@@ -0,0 +1,78 @@
import { IGNORE } from '../placeholders';
import { Req, Res, ServerRoute } from './types';
export function get_server_route_handler(routes: ServerRoute[]) {
async function handle_route(route: ServerRoute, req: Req, res: Res, next: () => void) {
req.params = route.params(route.pattern.exec(req.path));
const method = req.method.toLowerCase();
// 'delete' cannot be exported from a module because it is a keyword,
// so check for 'del' instead
const method_export = method === 'delete' ? 'del' : method;
const handle_method = route.handlers[method_export];
if (handle_method) {
if (process.env.SAPPER_EXPORT) {
const { write, end, setHeader } = res;
const chunks: any[] = [];
const headers: Record<string, string> = {};
// intercept data so that it can be exported
res.write = function(chunk: any) {
chunks.push(Buffer.from(chunk));
write.apply(res, arguments);
};
res.setHeader = function(name: string, value: string) {
headers[name.toLowerCase()] = value;
setHeader.apply(res, arguments);
};
res.end = function(chunk?: any) {
if (chunk) chunks.push(Buffer.from(chunk));
end.apply(res, arguments);
process.send({
__sapper__: true,
event: 'file',
url: req.url,
method: req.method,
status: res.statusCode,
type: headers['content-type'],
body: Buffer.concat(chunks).toString()
});
};
}
const handle_next = (err?: Error) => {
if (err) {
res.statusCode = 500;
res.end(err.message);
} else {
process.nextTick(next);
}
};
try {
await handle_method(req, res, handle_next);
} catch (err) {
handle_next(err);
}
} else {
// no matching handler for method
process.nextTick(next);
}
}
return function find_route(req: Req, res: Res, next: () => void) {
if (req[IGNORE]) return next();
for (const route of routes) {
if (route.pattern.test(req.path)) {
handle_route(route, req, res, next);
return;
}
}
next();
};
}

View File

@@ -0,0 +1,143 @@
import * as fs from 'fs';
import * as path from 'path';
import { build_dir, dev, manifest, IGNORE } from '../placeholders';
import { Handler, Req, Res, Store } from './types';
import { get_server_route_handler } from './get_server_route_handler';
import { get_page_handler } from './get_page_handler';
import { lookup } from './mime';
export default function middleware(opts: {
store?: (req: Req, res: Res) => Store,
ignore?: any
} = {}) {
const { store, ignore } = opts;
let emitted_basepath = false;
return compose_handlers([
ignore && ((req: Req, res: Res, next: () => void) => {
req[IGNORE] = should_ignore(req.path, ignore);
next();
}),
(req: Req, res: Res, next: () => void) => {
if (req[IGNORE]) return next();
if (req.baseUrl === undefined) {
let { originalUrl } = req;
if (req.url === '/' && originalUrl[originalUrl.length - 1] !== '/') {
originalUrl += '/';
}
req.baseUrl = originalUrl
? originalUrl.slice(0, -req.url.length)
: '';
}
if (!emitted_basepath && process.send) {
process.send({
__sapper__: true,
event: 'basepath',
basepath: req.baseUrl
});
emitted_basepath = true;
}
if (req.path === undefined) {
req.path = req.url.replace(/\?.*/, '');
}
next();
},
fs.existsSync(path.join(build_dir, 'index.html')) && serve({
pathname: '/index.html',
cache_control: dev ? 'no-cache' : 'max-age=600'
}),
fs.existsSync(path.join(build_dir, 'service-worker.js')) && serve({
pathname: '/service-worker.js',
cache_control: 'no-cache, no-store, must-revalidate'
}),
fs.existsSync(path.join(build_dir, 'service-worker.js.map')) && serve({
pathname: '/service-worker.js.map',
cache_control: 'no-cache, no-store, must-revalidate'
}),
serve({
prefix: '/client/',
cache_control: dev ? 'no-cache' : 'max-age=31536000, immutable'
}),
get_server_route_handler(manifest.server_routes),
get_page_handler(manifest, store)
].filter(Boolean));
}
export function compose_handlers(handlers: Handler[]) {
return (req: Req, res: Res, next: () => void) => {
let i = 0;
function go() {
const handler = handlers[i];
if (handler) {
handler(req, res, () => {
i += 1;
go();
});
} else {
next();
}
}
go();
};
}
export function should_ignore(uri: string, val: any) {
if (Array.isArray(val)) return val.some(x => should_ignore(uri, x));
if (val instanceof RegExp) return val.test(uri);
if (typeof val === 'function') return val(uri);
return uri.startsWith(val.charCodeAt(0) === 47 ? val : `/${val}`);
}
export function serve({ prefix, pathname, cache_control }: {
prefix?: string,
pathname?: string,
cache_control: string
}) {
const filter = pathname
? (req: Req) => req.path === pathname
: (req: Req) => req.path.startsWith(prefix);
const cache: Map<string, Buffer> = new Map();
const read = dev
? (file: string) => fs.readFileSync(path.resolve(build_dir, file))
: (file: string) => (cache.has(file) ? cache : cache.set(file, fs.readFileSync(path.resolve(build_dir, file)))).get(file)
return (req: Req, res: Res, next: () => void) => {
if (req[IGNORE]) return next();
if (filter(req)) {
const type = lookup(req.path);
try {
const file = decodeURIComponent(req.path.slice(1));
const data = read(file);
res.setHeader('Content-Type', type);
res.setHeader('Cache-Control', cache_control);
res.end(data);
} catch (err) {
res.statusCode = 404;
res.end('not found');
}
} else {
next();
}
};
}

View File

@@ -0,0 +1,69 @@
import { ClientRequest, ServerResponse } from 'http';
export type ServerRoute = {
pattern: RegExp;
handlers: Record<string, Handler>;
params: (match: RegExpMatchArray) => Record<string, string>;
};
export type Page = {
pattern: RegExp;
parts: Array<{
name: string;
component: Component;
params?: (match: RegExpMatchArray) => Record<string, string>;
}>
};
export type Manifest = {
server_routes: ServerRoute[];
pages: Page[];
root: Component;
error: Component;
}
export type Handler = (req: Req, res: Res, next: () => void) => void;
export type Store = {
get: () => any
};
export type Props = {
path: string;
query: Record<string, string>;
params: Record<string, string>;
error?: { message: string };
status?: number;
child: {
segment: string;
component: Component;
props: Props;
};
[key: string]: any;
};
export interface Req extends ClientRequest {
url: string;
baseUrl: string;
originalUrl: string;
method: string;
path: string;
params: Record<string, string>;
query: Record<string, string>;
headers: Record<string, string>;
}
export interface Res extends ServerResponse {
write: (data: any) => void;
}
export { ServerResponse };
interface Component {
render: (data: any, opts: { store: Store }) => {
head: string;
css: { code: string, map: any };
html: string
},
preload: (data: any) => any | Promise<any>
}

View File

@@ -0,0 +1,11 @@
import { Manifest } from './types';
export const manifest: Manifest = __MANIFEST__;
export const build_dir = __BUILD__DIR__;
export const src_dir = __SRC__DIR__;
export const dev = __DEV__;
export const IGNORE = '__SAPPER__IGNORE__';

7
test/app/.gitignore vendored
View File

@@ -1,7 +0,0 @@
.DS_Store
node_modules
.sapper
yarn.lock
cypress/screenshots
templates/.*
dist

View File

@@ -1,81 +0,0 @@
# sapper-template
The default [Sapper](https://github.com/sveltejs/sapper) template. To clone it and get started:
```bash
npx degit sveltejs/sapper-template my-app
cd my-app
npm install # or yarn!
npm run dev
```
Open up [localhost:3000](http://localhost:3000) and start clicking around.
## Structure
Sapper expects to find three directories in the root of your project — `assets`, `routes` and `templates`.
### assets
The [assets](assets) directory contains any static assets that should be available. These are served using [serve-static](https://github.com/expressjs/serve-static).
In your [service-worker.js](templates/service-worker.js) file, Sapper makes these files available as `__assets__` so that you can cache them (though you can choose not to, for example if you don't want to cache very large files).
### routes
This is the heart of your Sapper app. There are two kinds of routes — *pages*, and *server routes*.
**Pages** are Svelte components written in `.html` files. When a user first visits the application, they will be served a server-rendered version of the route in question, plus some JavaScript that 'hydrates' the page and initialises a client-side router. From that point forward, navigating to other pages is handled entirely on the client for a fast, app-like feel. (Sapper will preload and cache the code for these subsequent pages, so that navigation is instantaneous.)
**Server routes** are modules written in `.js` files, that export functions corresponding to HTTP methods. Each function receives Express `request` and `response` objects as arguments, plus a `next` function. This is useful for creating a JSON API, for example.
There are three simple rules for naming the files that define your routes:
* A file called `routes/about.html` corresponds to the `/about` route. A file called `routes/blog/[slug].html` corresponds to the `/blog/:slug` route, in which case `params.slug` is available to the route
* The file `routes/index.html` (or `routes/index.js`) corresponds to the root of your app. `routes/about/index.html` is treated the same as `routes/about.html`.
* Files and directories with a leading underscore do *not* create routes. This allows you to colocate helper modules and components with the routes that depend on them — for example you could have a file called `routes/_helpers/datetime.js` and it would *not* create a `/_helpers/datetime` route
### templates
This directory should contain the following files at a minimum:
* [2xx.html](templates/2xx.html) — a template for the page to serve for valid requests
* [4xx.html](templates/4xx.html) — a template for 4xx-range errors (such as 404 Not Found)
* [5xx.html](templates/5xx.html) — a template for 5xx-range errors (such as 500 Internal Server Error)
* [main.js](templates/main.js) — this module initialises Sapper
* [service-worker.js](templates/service-worker.js) — your app's service worker
Inside the HTML templates, Sapper will inject various values as indicated by `%sapper.xxxx%` tags. Inside JavaScript files, Sapper will replace strings like `__dev__` with the appropriate value.
In lieu of documentation (bear with us), consult the files to see what variables are available and how they're used.
## Webpack config
Sapper uses webpack to provide code-splitting, dynamic imports and hot module reloading, as well as compiling your Svelte components. As long as you don't do anything daft, you can edit the configuration files to add whatever loaders and plugins you'd like.
## Production mode and deployment
To start a production version of your app, run `npm start`. This will disable hot module replacement, and activate the appropriate webpack plugins.
You can deploy your application to any environment that supports Node 8 or above. As an example, to deploy to [Now](https://zeit.co/now), run these commands:
```bash
npm install -g now
now
```
## Bugs and feedback
Sapper is in early development, and may have the odd rough edge here and there. Please be vocal over on the [Sapper issue tracker](https://github.com/sveltejs/sapper/issues).
## License
[LIL](LICENSE)

View File

@@ -1,6 +0,0 @@
{#if preloading}
<progress class='preloading-progress' value=0.5/>
{/if}
<svelte:component this={Page} {...props}/>

View File

@@ -1,15 +0,0 @@
import { init, prefetchRoutes } from '../../../runtime.js';
import { Store } from 'svelte/store.js';
import { routes } from './manifest/client.js';
import App from './App.html';
window.init = () => {
return init({
target: document.querySelector('#sapper'),
App,
routes,
store: data => new Store(data)
});
};
window.prefetchRoutes = prefetchRoutes;

View File

@@ -1,106 +0,0 @@
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';
import App from './App.html'
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({
App,
routes,
store: () => {
return new Store({
title: 'Stored title'
});
}
})
];
if (BASEPATH) {
app.use(BASEPATH, ...middlewares);
} else {
app.use(...middlewares);
}
app.listen(PORT);

View File

@@ -1,33 +0,0 @@
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width'>
<meta name='theme-color' content='#aa1e1e'>
%sapper.base%
<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
lazily loaded when it precaches secondary pages -->
%sapper.styles%
<!-- This contains the contents of the <:Head> component, if
the current page has one -->
%sapper.head%
</head>
<body>
<!-- The application will be rendered inside this element,
because `templates/main.js` references it -->
<div id='sapper'>%sapper.html%</div>
<!-- Sapper creates a <script> tag containing `templates/main.js`
and anything else it needs to hydrate the app and
initialise the router -->
%sapper.scripts%
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,45 +0,0 @@
body {
margin: 0;
font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
font-size: 14px;
line-height: 1.5;
color: #333;
}
main {
position: relative;
max-width: 56em;
background-color: white;
padding: 2em;
margin: 0 auto;
box-sizing: border-box;
}
h1, h2, h3, h4, h5, h6 {
margin: 0 0 0.5em 0;
font-weight: 400;
line-height: 1.2;
}
h1 {
font-size: 2em;
}
a {
color: inherit;
}
code {
font-family: menlo, inconsolata, monospace;
font-size: calc(1em - 2px);
color: #555;
background-color: #f0f0f0;
padding: 0.2em 0.4em;
border-radius: 2px;
}
@media (min-width: 400px) {
body {
font-size: 16px;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -1,20 +0,0 @@
{
"background_color": "#ffffff",
"theme_color": "#aa1e1e",
"name": "TODO",
"short_name": "TODO",
"display": "minimal-ui",
"start_url": "/",
"icons": [
{
"src": "svelte-logo-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "svelte-logo-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -1,6 +0,0 @@
<svelte:head>
<title>{status}</title>
</svelte:head>
<h1>{status}</h1>
<p>{error.message}</p>

View File

@@ -1,27 +0,0 @@
<svelte:head>
<title>About</title>
</svelte:head>
<h1>About this site</h1>
<p>This is the 'about' page. There's not much here.</p>
<button class='prefetch' on:click='prefetch("blog/why-the-name")'>Why the name?</button>
<script>
import { goto, prefetch } from '../../../runtime.js';
export default {
oncreate() {
window.goto = goto;
},
ondestroy() {
window.goto = null;
},
methods: {
prefetch
}
};
</script>

View File

@@ -1,36 +0,0 @@
<svelte:head>
<title>{post.title}</title>
</svelte:head>
<h1>{post.title}</h1>
<div class='content'>
{@html post.html}
</div>
<script>
export default {
preload({ params, query }) {
// the `slug` parameter is available because this file
// is called [slug].html
const { slug } = params;
if (slug === 'throw-an-error') {
return this.error(500, 'something went wrong');
}
return fetch(`blog/${slug}.json`).then(r => {
if (r.status === 200) {
return r.json().then(post => ({ post }));
this.error(r.status, '')
}
if (r.status === 404) {
this.error(404, 'Not found');
} else {
throw new Error('Something went wrong');
}
});
}
};
</script>

View File

@@ -1,23 +0,0 @@
import posts from './_posts.js';
const lookup = {};
posts.forEach(post => {
lookup[post.slug] = JSON.stringify(post);
});
export function get(req, res, next) {
// the `slug` parameter is available because this file
// is called [slug].js
const { slug } = req.params;
if (slug in lookup) {
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': `no-cache`
});
res.end(lookup[slug]);
} else {
next();
}
}

View File

@@ -1,116 +0,0 @@
// Ordinarily, you'd generate this data from markdown files in your
// repo, or fetch them from a database of some kind. But in order to
// avoid unnecessary dependencies in the starter template, and in the
// service of obviousness, we're just going to leave it here.
// This file is called `_posts.js` rather than `posts.js`, because
// we don't want to create an `/api/blog/posts` route — the leading
// underscore tells Sapper not to do that.
const posts = [
{
title: 'What is Sapper?',
slug: 'what-is-sapper',
html: `
<p>First, you have to know what <a href='https://svelte.technology'>Svelte</a> is. Svelte is a UI framework with a bold new idea: rather than providing a library that you write code with (like React or Vue, for example), it's a compiler that turns your components into highly optimized vanilla JavaScript. If you haven't already read the <a href='https://svelte.technology/blog/frameworks-without-the-framework'>introductory blog post</a>, you should!</p>
<p>Sapper is a Next.js-style framework (<a href='blog/how-is-sapper-different-from-next'>more on that here</a>) built around Svelte. It makes it embarrassingly easy to create extremely high performance web apps. Out of the box, you get:</p>
<ul>
<li>Code-splitting, dynamic imports and hot module replacement, powered by webpack</li>
<li>Server-side rendering (SSR) with client-side hydration</li>
<li>Service worker for offline support, and all the PWA bells and whistles</li>
<li>The nicest development experience you've ever had, or your money back</li>
</ul>
<p>It's implemented as Express middleware. Everything is set up and waiting for you to get started, but you keep complete control over the server, service worker, webpack config and everything else, so it's as flexible as you need it to be.</p>
`
},
{
title: 'How to use Sapper',
slug: 'how-to-use-sapper',
html: `
<h2>Step one</h2>
<p>Create a new project, using <a href='https://github.com/Rich-Harris/degit'>degit</a>:</p>
<pre><code>npx degit sveltejs/sapper-template my-app
cd my-app
npm install # or yarn!
npm run dev
</code></pre>
<h2>Step two</h2>
<p>Go to <a href='http://localhost:3000'>localhost:3000</a>. Open <code>my-app</code> in your editor. Edit the files in the <code>routes</code> directory or add new ones.</p>
<h2>Step three</h2>
<p>...</p>
<h2>Step four</h2>
<p>Resist overdone joke formats.</p>
`
},
{
title: 'Why the name?',
slug: 'why-the-name',
html: `
<p>In war, the soldiers who build bridges, repair roads, clear minefields and conduct demolitions — all under combat conditions — are known as <em>sappers</em>.</p>
<p>For web developers, the stakes are generally lower than those for combat engineers. But we face our own hostile environment: underpowered devices, poor network connections, and the complexity inherent in front-end engineering. Sapper, which is short for <strong>S</strong>velte <strong>app</strong> mak<strong>er</strong>, is your courageous and dutiful ally.</p>
`
},
{
title: 'How is Sapper different from Next.js?',
slug: 'how-is-sapper-different-from-next',
html: `
<p><a href='https://github.com/zeit/next.js/'>Next.js</a> is a React framework from <a href='https://zeit.co'>Zeit</a>, and is the inspiration for Sapper. There are a few notable differences, however:</p>
<ul>
<li>It's powered by <a href='https://svelte.technology'>Svelte</a> instead of React, so it's faster and your apps are smaller</li>
<li>Instead of route masking, we encode route parameters in filenames. For example, the page you're looking at right now is <code>routes/blog/[slug].html</code></li>
<li>As well as pages (Svelte components, which render on server or client), you can create <em>server routes</em> in your <code>routes</code> directory. These are just <code>.js</code> files that export functions corresponding to HTTP methods, and receive Express <code>request</code> and <code>response</code> objects as arguments. This makes it very easy to, for example, add a JSON API such as the one <a href='blog/how-is-sapper-different-from-next.json'>powering this very page</a></li>
<li>Links are just <code>&lt;a&gt;</code> elements, rather than framework-specific <code>&lt;Link&gt;</code> components. That means, for example, that <a href='blog/how-can-i-get-involved'>this link right here</a>, despite being inside a blob of HTML, works with the router as you'd expect.</li>
</ul>
`
},
{
title: 'How can I get involved?',
slug: 'how-can-i-get-involved',
html: `
<p>We're so glad you asked! Come on over to the <a href='https://github.com/sveltejs/svelte'>Svelte</a> and <a href='https://github.com/sveltejs/sapper'>Sapper</a> repos, and join us in the <a href='https://gitter.im/sveltejs/svelte'>Gitter chatroom</a>. Everyone is welcome, especially you!</p>
`
},
{
title: 'A very long post with deep links',
slug: 'a-very-long-post',
html: `
<h2 id='one'>One</h2>
<p>I'll have a vodka rocks. (Mom, it's breakfast time.) And a piece of toast. Let me out that Queen. Fried cheese… with club sauce.</p>
<p>Her lawyers are claiming the seal is worth $250,000. And that's not even including Buster's Swatch. This was a big get for God. What, so the guy we are meeting with can't even grow his own hair? COME ON! She's always got to wedge herself in the middle of us so that she can control everything. Yeah. Mom's awesome. It's, like, Hey, you want to go down to the whirlpool? Yeah, I don't have a husband. I call it Swing City. The CIA should've just Googled for his hideout, evidently. There are dozens of us! DOZENS! Yeah, like I'm going to take a whiz through this $5,000 suit. COME ON.</p>
<h2 id='two'>Two</h2>
<p>Tobias Fünke costume. Heart attack never stopped old big bear.</p>
<p>Nellie is blowing them all AWAY. I will be a bigger and hairier mole than the one on your inner left thigh! I'll sacrifice anything for my children.</p>
<p>Up yours, granny! You couldn't handle it! Hey, Dad. Look at you. You're a year older…and a year closer to death. Buster: Oh yeah, I guess that's kind of funny. Bob Loblaw Law Blog. The guy runs a prison, he can have any piece of ass he wants.</p>
<h2 id='three'>Three</h2>
<p>I prematurely shot my wad on what was supposed to be a dry run, so now I'm afraid I have something of a mess on my hands. Dead Dove DO NOT EAT. Never once touched my per diem. I'd go to Craft Service, get some raw veggies, bacon, Cup-A-Soup…baby, I got a stew goin'. You're losing blood, aren't you? Gob: Probably, my socks are wet. Sure, let the little fruit do it. HUZZAH! Although George Michael had only got to second base, he'd gone in head first, like Pete Rose. I will pack your sweet pink mouth with so much ice cream you'll be the envy of every Jerry and Jane on the block!</p>
<p>Gosh Mom… after all these years, God's not going to take a call from you. Come on, this is a Bluth family celebration. It's no place for children.</p>
<p>And I wouldn't just lie there, if that's what you're thinking. That's not what I WAS thinking. Who? i just dont want him to point out my cracker ass in front of ann. When a man needs to prove to a woman that he's actually… When a man loves a woman… Heyyyyyy Uncle Father Oscar. [Stabbing Gob] White power! Gob: I'm white! Let me take off my assistant's skirt and put on my Barbra-Streisand-in-The-Prince-of-Tides ass-masking therapist pantsuit. In the mid '90s, Tobias formed a folk music band with Lindsay and Maebe which he called Dr. Funke's 100 Percent Natural Good Time Family Band Solution. The group was underwritten by the Natural Food Life Company, a division of Chem-Grow, an Allen Crayne acqusition, which was part of the Squimm Group. Their motto was simple: We keep you alive.</p>
<h2 id='four'>Four</h2>
<p>If you didn't have adult onset diabetes, I wouldn't mind giving you a little sugar. Everybody dance NOW. And the soup of the day is bread. Great, now I'm gonna smell to high heaven like a tuna melt!</p>
<p>That's how Tony Wonder lost a nut. She calls it a Mayonegg. Go ahead, touch the Cornballer. There's a new daddy in town. A discipline daddy.</p>
`
}
];
posts.forEach(post => {
post.html = post.html.replace(/^\t{3}/gm, '');
});
export default posts;

View File

@@ -1,25 +0,0 @@
<svelte:head>
<title>Blog</title>
</svelte:head>
<h1>Recent posts</h1>
<ul>
{#each posts as post}
<!-- we're using the non-standard `rel=prefetch` attribute to
tell Sapper to load the data for the page as soon as
the user hovers over the link or taps it, instead of
waiting for the 'click' event -->
<li><a rel='prefetch' href='blog/{post.slug}'>{post.title}</a></li>
{/each}
</ul>
<script>
export default {
preload({ params, query }) {
return fetch(`blog.json`).then(r => r.json()).then(posts => {
return { posts };
});
}
};
</script>

View File

@@ -1,17 +0,0 @@
import posts from './_posts.js';
const contents = JSON.stringify(posts.map(post => {
return {
title: post.title,
slug: post.slug
};
}));
export function get(req, res) {
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': `max-age=${30 * 60 * 1e3}` // cache for 30 minutes
});
res.end(contents);
}

View File

@@ -1,28 +0,0 @@
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'
}));
}
}

View File

@@ -1 +0,0 @@
<h1>I'm afraid I just blue myself</h1>

View File

@@ -1,26 +0,0 @@
<svelte:head>
<title>Sapper project template</title>
</svelte:head>
<h1>Great success!</h1>
<a href='.'>home</a>
<a href='about'>about</a>
<a href='slow-preload'>slow preload</a>
<a href='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>
<div class='hydrate-test'></div>
<style>
h1 {
text-align: center;
font-size: 2.8em;
text-transform: uppercase;
font-weight: 700;
margin: 0 0 0.5em 0;
}
</style>

View File

@@ -1,9 +0,0 @@
<p>URL is {url}</p>
<script>
export default {
preload({ url }) {
if (url) return { url };
}
};
</script>

View File

@@ -1,3 +0,0 @@
export function get() {
throw new Error('nope');
}

View File

@@ -1,34 +0,0 @@
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'
};

Some files were not shown because too many files have changed in this diff Show More