Compare commits

...

266 Commits

Author SHA1 Message Date
Rich Harris
d9cb572271 -> v0.7.6 2018-02-28 17:14:38 -05:00
Rich Harris
34c28f36cd Merge pull request #147 from sveltejs/gh-138
don't serve error page for unhandled server route errors
2018-02-28 17:11:52 -05:00
Rich Harris
5dd04eb35c tidy up - next is unused 2018-02-28 16:59:39 -05:00
Rich Harris
b1d072d43a dont serve error page for unhandled server route errors - fixes #138 2018-02-28 16:54:38 -05:00
Rich Harris
5ad3f3f1d5 Merge pull request #146 from sveltejs/gh-145
prevent client-side navigation to server routes
2018-02-28 16:20:42 -05:00
Rich Harris
58754c6d15 use blog.json instead of blog/list.json 2018-02-28 16:17:07 -05:00
Rich Harris
c36780fdc8 prevent client-side navigation to server routes - fixes #145 2018-02-28 14:23:19 -05:00
Rich Harris
9bebb56bd6 -> v0.7.5 2018-02-28 13:40:42 -05:00
Rich Harris
f475634d8d Merge pull request #144 from sveltejs/gh-139
allow dynamic parameters inside route parts
2018-02-28 12:26:00 -05:00
Rich Harris
58c1eb9fa8 allow dynamic parameters inside route parts - fixes #139 2018-02-28 09:39:21 -05:00
Rich Harris
631afbbfe4 -> v0.7.4 2018-02-27 20:10:48 -05:00
Rich Harris
1cc9acb4f1 Merge pull request #142 from sveltejs/gh-141
force NODE_ENV=production in build/export
2018-02-27 20:09:05 -05:00
Rich Harris
19005110f1 huh, not sure why that changed 2018-02-27 19:58:49 -05:00
Rich Harris
21ee8ad39d force NODE_ENV=production in build/export 2018-02-27 19:49:58 -05:00
Rich Harris
906b0c7ad5 Merge pull request #134 from sveltejs/source-map-support
install source-map-support
2018-02-27 19:46:17 -05:00
Rich Harris
896fd410d1 install source-map-support 2018-02-20 14:01:25 -05:00
Rich Harris
c0cc877456 -> v0.7.3 2018-02-20 12:23:32 -05:00
Rich Harris
3ed9ce27a1 Merge pull request #131 from sveltejs/webpack-insanity
handle case where webpack asset is an array instead of a string
2018-02-20 12:18:29 -05:00
Rich Harris
edba45b809 Merge pull request #129 from sveltejs/robustify-hmr
ensure old server is killed before listening for port on new server
2018-02-20 12:18:10 -05:00
Rich Harris
43c1890235 handle case where webpack asset is an array instead of a string 2018-02-20 11:11:55 -05:00
Rich Harris
605929053c ensure old server is killed before listening for port on new server 2018-02-20 11:11:02 -05:00
Rich Harris
2752c73ebb -> v0.7.2 2018-02-18 17:24:12 -05:00
Rich Harris
2547db39ac -> v0.7.1 2018-02-18 17:09:55 -05:00
Rich Harris
1285739cc5 -> v0.7.1 2018-02-18 17:08:35 -05:00
Rich Harris
14d64e854a -> v0.7.0 2018-02-18 13:00:36 -05:00
Rich Harris
c419c73550 fix tests 2018-02-18 12:55:56 -05:00
Rich Harris
835b94175d workaround weird webpack(?) bug by not deleting old files 2018-02-18 12:49:37 -05:00
Rich Harris
25bdcf9957 Merge branch 'gh-83' 2018-02-18 12:12:45 -05:00
Rich Harris
792ccf5c6a robustify dev server port selection 2018-02-18 12:12:25 -05:00
Rich Harris
4ca8195037 Merge pull request #127 from sveltejs/gh-83
implement this.redirect in preload
2018-02-18 10:36:05 -05:00
Rich Harris
cb12231053 implement this.error 2018-02-18 09:48:32 -05:00
Rich Harris
d55401d45b see if this fixes windows tests 2018-02-18 08:27:07 -05:00
Rich Harris
99d4eafb0b argh these flaky tests are killing me 2018-02-18 08:18:14 -05:00
Rich Harris
bff6f550be implement this.redirect in preload (#83) 2018-02-17 22:56:47 -05:00
Rich Harris
f8ea9ebda1 Merge pull request #126 from sveltejs/gh-91
[WIP] restructure everything
2018-02-17 19:49:12 -05:00
Rich Harris
181d7b4a61 need to come up with a more robust approach to this... 2018-02-17 19:27:34 -05:00
Rich Harris
beb415c65d argh 2018-02-17 19:22:05 -05:00
Rich Harris
5bbd7ead17 and again 2018-02-17 19:16:06 -05:00
Rich Harris
e11405d555 try increasing timeout 2018-02-17 19:12:47 -05:00
Rich Harris
9fe0ca2c22 remove 4xx and 5xx pages when initing 2018-02-17 19:00:06 -05:00
Rich Harris
f2eb95d546 various minor fixes 2018-02-17 18:40:20 -05:00
Rich Harris
ab1ca60363 basic sapper dev task, with HMR 2018-02-17 17:44:57 -05:00
Rich Harris
d95f52f8e9 fix tests, add WIP dev task 2018-02-17 11:15:26 -05:00
Rich Harris
b02183af53 fix exporting 2018-02-16 14:25:53 -05:00
Rich Harris
f9828f9fd2 work in progress 2018-02-16 12:01:55 -05:00
Rich Harris
9a760c570f move webpack configs in test app 2018-02-14 12:25:40 -05:00
Rich Harris
0f390920a8 fix some typescript stuff 2018-02-14 12:25:24 -05:00
Rich Harris
9adb6ca7e6 Merge pull request #120 from sveltejs/gh-92
implement sapper upgrade
2018-02-14 10:28:01 -05:00
Rich Harris
24980651c0 that was a weird typo 2018-02-13 22:08:29 -05:00
Rich Harris
7c6436a99c implement sapper upgrade 2018-02-13 18:46:53 -05:00
Rich Harris
f6b26f1b07 Merge pull request #119 from sveltejs/better-cli
Better CLI
2018-02-13 17:30:08 -05:00
Rich Harris
55b60369f9 add --help to CLI 2018-02-13 17:12:55 -05:00
Rich Harris
2be9dd1883 Move build.ts and export.ts into src/cli - fixes #115 2018-02-13 15:57:02 -05:00
Rich Harris
b29700f725 update electron to squelch vuln warning 2018-02-13 15:50:04 -05:00
Rich Harris
7188ce0d0d -> v0.6.4 2018-02-06 10:51:24 -05:00
Rich Harris
4f8ce19fe1 Merge pull request #114 from lukeed/fix/prod-hmr
Fix: Remove HMR attempts in Prod~!
2018-02-06 10:39:19 -05:00
Rich Harris
a85f2921e8 Merge pull request #113 from lukeed/feat/map-config
Consolidate Rollup config file
2018-02-06 10:37:34 -05:00
Luke Edwards
7a2ed16884 fix HMR attempts in production 2018-02-05 14:09:24 -08:00
Luke Edwards
08e575fee0 shrink repetitive rollup.config file 2018-02-05 13:43:10 -08:00
Rich Harris
7dbcab74d3 remove logging 2018-02-03 16:35:10 -05:00
Rich Harris
9b1b545194 -> v0.6.3 2018-02-03 16:26:53 -05:00
Rich Harris
7b01242f3e always build in prod mode 2018-02-03 16:26:47 -05:00
Rich Harris
15b1fbf8a6 ignore non-html responses when crawling, and close middleware when done 2018-02-03 16:17:28 -05:00
Rich Harris
8f1d2e0a04 -> v0.6.2 2018-02-03 13:56:38 -05:00
Rich Harris
dfb8692d78 handle unspecified type in sapper export 2018-02-03 13:56:14 -05:00
Rich Harris
09d3c4d85e add prepublish script 2018-02-03 13:49:21 -05:00
Rich Harris
1e623dde29 -> v0.6.1 2018-02-03 13:48:30 -05:00
Rich Harris
5104abf329 -> v0.6.0 2018-02-03 13:09:31 -05:00
Rich Harris
6554fc8616 Merge branch 'restructure' 2018-02-03 13:07:36 -05:00
Rich Harris
cd01b7e6db Merge branch 'master' of github.com:sveltejs/sapper 2018-02-03 12:24:59 -05:00
Rich Harris
bfa3da6d3d Merge branch 'issue_109-patch-1' of https://github.com/samhatchett/sapper into samhatchett-issue_109-patch-1 2018-02-03 12:07:53 -05:00
Rich Harris
6ee092f8d4 Merge pull request #107 from lukastaegert/fix-search-param-handling
Fix query param handling
2018-02-03 12:05:57 -05:00
Rich Harris
ac70004f77 remove runtime.js.map (again?) 2018-02-03 12:04:39 -05:00
Rich Harris
3449f1eb37 add test for #105 2018-02-03 12:04:14 -05:00
Rich Harris
16cb1fccc6 Merge pull request #105 from thgh/patch-2
Fix route not found when url is /?
2018-02-03 12:03:36 -05:00
Rich Harris
b20c1c029f Merge branch 'thgh-patch-1' 2018-02-03 11:50:37 -05:00
Rich Harris
7abfb1aab1 Merge branch 'restructure' 2018-02-03 11:50:12 -05:00
Rich Harris
205c2defe4 Merge branch 'patch-1' of https://github.com/thgh/sapper into thgh-patch-1 2018-02-03 11:45:54 -05:00
Rich Harris
09a6eec83e Merge pull request #101 from sveltejs/restructure
Restructure codebase
2018-02-03 11:44:57 -05:00
Rich Harris
2cabf61ea7 ignore runtime.js.map 2018-02-03 11:41:33 -05:00
Sam Hatchett
71cfdd2907 fixes api route default content-type
fixes issue where api routes were being defaulted to text/html. Page routes should be text/html, but api routes could be json, zip files, etc., and express does some type-guessing to assist in case the user code does not specify the content-type.
2018-01-31 11:08:25 -05:00
Lukas Taegert
297f4276de Fix query param handling by
* not using a for-of loop on an iterator that is transpiled wrongly
* not using URL.searchParams which is only supported by rather new
  browsers
2018-01-26 23:29:34 +01:00
Thomas Ghysels
422e31e183 Fix route not found when url is /? 2018-01-26 02:48:56 +01:00
Thomas Ghysels
b53ee061c0 Fix DOMException when location.hash is invalid selector 2018-01-26 02:24:49 +01:00
Rich Harris
8bad37205d convert to typescript 2018-01-21 16:28:02 -05:00
Rich Harris
fd0dd4fe58 and again... 2018-01-21 16:14:16 -05:00
Rich Harris
4940644ae3 more tidying up 2018-01-21 16:11:46 -05:00
Rich Harris
fb8d952eeb more tidying up 2018-01-21 16:02:42 -05:00
Rich Harris
fc631c4866 make route handling more explicit 2018-01-21 15:41:13 -05:00
Rich Harris
03ce2ea998 tidy up a bit 2018-01-21 15:04:22 -05:00
Rich Harris
dd8deb2d8a wip 3 2018-01-21 14:41:11 -05:00
Rich Harris
7d721abb2a wip 2 2018-01-21 11:15:32 -05:00
Rich Harris
39b1fa89ce wip 2018-01-21 11:03:23 -05:00
Rich Harris
7a3506420f Merge pull request #100 from sveltejs/gh-99
return a promise from init
2018-01-20 20:16:46 -05:00
Rich Harris
72ae4a1c64 ugh still need to wait for requests to complete 2018-01-20 19:55:21 -05:00
Rich Harris
a09c33d6a5 return a promise from init - fixes #99 2018-01-20 19:49:41 -05:00
Rich Harris
4590aa313c Merge pull request #96 from mrkishi/master
Identify js and html route clashes
2018-01-20 18:36:50 -05:00
Rich Harris
d11bd954e0 seems tests weren't actually running in appveyor? (#98)
well this was harder than it should have been
2018-01-20 18:34:30 -05:00
Rich Harris
c15959710b huh. not sure where this broke 2018-01-20 13:39:49 -05:00
Rich Harris
bb8ff74f68 remove noise from tests 2018-01-20 13:21:53 -05:00
Rich Harris
2cbbe91490 Merge branch 'master' of github.com:sveltejs/sapper 2018-01-20 12:53:16 -05:00
Rich Harris
faeddd8add work around Svelte hydration failures 2018-01-20 12:53:06 -05:00
Rich Harris
d77722c042 Merge branch 'chexxor-patch-1' 2018-01-20 12:40:35 -05:00
Rich Harris
61daba7a64 add hydration test, add window.init function to make it possible 2018-01-20 12:40:31 -05:00
Rich Harris
54ff8cc2e6 Merge branch 'patch-1' of https://github.com/chexxor/sapper into chexxor-patch-1 2018-01-20 12:37:48 -05:00
Rich Harris
e6fcafe09b Merge pull request #95 from lukeed/fix/express-reliance
Remove Express Helpers
2018-01-20 12:10:43 -05:00
Rich Harris
a305d3cea1 Merge pull request #97 from sveltejs/gh-90
return service worker from generate_asset_cache
2018-01-20 12:09:11 -05:00
Rich Harris
75e70207b8 return service worker from generate_asset_cache - fixes #90 2018-01-20 12:02:32 -05:00
mrkishi
8a8526d9ed Identify js and html route clashes 2018-01-20 01:42:13 -02:00
Luke Edwards
9a76229bb6 test: shim setHeader method 2018-01-19 13:56:01 -08:00
Luke Edwards
f4e46e6e6c replace Express shorthands w/ native counterparts 2018-01-19 13:54:55 -08:00
Alex Berg
90cd347112 Fix typo 2018-01-19 01:25:20 -06:00
Alex Berg
5adfdd6fe0 Hydrate on first load, not destroy 2018-01-19 01:22:08 -06:00
Rich Harris
a6dc61a182 -> v0.5.1 2018-01-16 08:47:50 -05:00
Rich Harris
96666d05ec only write to filesystem in dev mode 2018-01-16 08:47:44 -05:00
Rich Harris
6390ba692b -> v0.5.0 2018-01-15 10:28:48 -05:00
Rich Harris
0e131cc81e Merge pull request #88 from sveltejs/gh-9
Exporting
2018-01-15 10:24:35 -05:00
Rich Harris
bd3d5713cb Merge pull request #64 from lukeed/lazy-chokidar
Lazily Require Chokidar
2018-01-15 10:04:37 -05:00
Rich Harris
9ec23c47ad update test 2018-01-15 10:04:24 -05:00
Rich Harris
b7bb69925e rename extract to export, for familiarity to next.js devs 2018-01-15 10:03:27 -05:00
Rich Harris
25124f6ee7 Merge branch 'master' into lazy-chokidar 2018-01-15 09:58:39 -05:00
Rich Harris
73d491cd19 Merge branch 'master' into lazy-chokidar 2018-01-15 09:58:03 -05:00
Rich Harris
e25fceb4b8 node 6 is the new IE 2018-01-15 09:36:12 -05:00
Rich Harris
3807147c57 add missing dependency 2018-01-14 22:45:59 -05:00
Rich Harris
a523ba58ff save pages as index.html, ignore hashes 2018-01-14 22:45:51 -05:00
Rich Harris
fe03fd3a52 update lockfile 2018-01-14 18:21:37 -05:00
Rich Harris
89c430a0cb slightly different approach to extracting 2018-01-14 18:19:51 -05:00
Rich Harris
8ef312849c always write service-worker.js and shell index.html file 2018-01-14 18:16:10 -05:00
Rich Harris
4200446684 ignore trailing slash in pathnames 2018-01-14 18:15:40 -05:00
Rich Harris
681ed005b8 remove unused deps 2018-01-14 18:15:12 -05:00
Rich Harris
d457af8d51 update test 2018-01-14 18:14:07 -05:00
Rich Harris
0c158b9e1f Merge branch 'master' of https://github.com/freedmand/sapper into freedmand-master 2018-01-14 13:48:07 -05:00
Rich Harris
50011e2077 always print stdout/stderr, to avoid wild goose chase debugging 2018-01-14 13:47:23 -05:00
Rich Harris
f27b7973e3 Build before extracting 2018-01-14 13:46:25 -05:00
Rich Harris
2af2ab3cb9 Ensure output dir exists, return Promise 2018-01-14 13:45:47 -05:00
Rich Harris
6a4dc1901c Enforce prod mode, return a Promise so it can be used programmatically 2018-01-14 13:45:01 -05:00
Rich Harris
fbbc0e9e19 Merge branch 'master' of https://github.com/freedmand/sapper into freedmand-master 2018-01-14 12:29:15 -05:00
Rich Harris
1213c3da46 fix lockfile mess 2018-01-14 12:27:35 -05:00
Rich Harris
4cc2104088 add back test that got lost in a merge conflict somewhere 2018-01-14 12:23:07 -05:00
Rich Harris
d6dda371ca typo 2018-01-14 12:22:41 -05:00
Rich Harris
304c06085e Merge branch 'master' into master 2018-01-14 12:19:47 -05:00
Rich Harris
33b6450e34 Merge branch 'master' into master 2018-01-14 12:16:09 -05:00
Rich Harris
8faa98af6a -> v0.4.0 2018-01-14 12:07:06 -05:00
Rich Harris
14df138528 Merge pull request #87 from sveltejs/gh-3
Serialize preloaded data for initial page and serve to client
2018-01-14 11:50:18 -05:00
Rich Harris
44285cdb2f serialized preloaded data and send to client as initial payload - fixes #3 2018-01-14 11:22:00 -05:00
Rich Harris
bd656cfd5b Merge branch 'fix-tests-without-async' into gh-3 2018-01-14 10:45:55 -05:00
Rich Harris
c4b4bd587d use %sapper.scripts% 2018-01-14 10:45:21 -05:00
Rich Harris
2abfdb03d5 wait for window.READY so that this tests we dont double-preload 2018-01-14 10:44:38 -05:00
Rich Harris
a80ac3a8b8 change tests back to non-async versions 2018-01-14 00:43:57 -05:00
Rich Harris
887cb09386 merge 2018-01-14 00:21:55 -05:00
Rich Harris
cfeeafded4 Merge pull request #84 from sveltejs/test-in-node-6
test in node 6
2018-01-14 00:18:23 -05:00
Rich Harris
2cae674033 Merge branch 'master' into test-in-node-6 2018-01-14 00:14:40 -05:00
Rich Harris
7c0f32662d remove async from new test 2018-01-14 00:10:20 -05:00
Rich Harris
b4fb1c3268 Merge branch 'master' of github.com:sveltejs/sapper 2018-01-14 00:02:32 -05:00
Rich Harris
ecd0f673a9 test in node 6 2018-01-14 00:01:51 -05:00
Rich Harris
40d16852f7 prevent unnecessary promise chains, and hoist handler function 2018-01-14 00:01:10 -05:00
Rich Harris
133be03791 Merge pull request #72 from nolanlawson/fix-style-loader
Explicitly load style-loader/css-loader deps
2018-01-13 23:33:38 -05:00
Rich Harris
727a76ebb5 fall through to 404 if no handler exists for method 2018-01-13 23:29:41 -05:00
Rich Harris
e3c047831a add test for #77 2018-01-13 23:29:16 -05:00
Emil Tholin
81b5e0d764 Handle DELETE requests with del export 2018-01-10 19:24:22 +01:00
Emil Tholin
98e904dcfc Remove async/await from test.js 2018-01-07 12:22:40 +01:00
Nolan Lawson
ca51372150 Explicitly load style-loader/css-loader deps
This appears to fix sveltejs/sapper-template#27
2018-01-06 17:21:40 -08:00
Luke Edwards
7cef1f1120 only return from show-url if in server context 2018-01-06 16:11:39 -08:00
Luke Edwards
1b73baabce [temp - debug]: log out the preload value 2018-01-06 15:40:40 -08:00
Luke Edwards
5aa01b922b add page.title() Nightmare action 2018-01-06 15:16:18 -08:00
Luke Edwards
f0bc68be88 use built-in for document.title 2018-01-06 15:07:43 -08:00
Luke Edwards
be7c53becc quick attempt at fixing test for travis 2018-01-06 14:53:14 -08:00
freedmand
9ea4137b87 Add option to extract server-side routes at directories other than /api.
Also clarifies some texts and documentation.
2018-01-05 19:21:25 -08:00
freedmand
7588911108 Removes all async/await from the extraction pipeline, and adds unit tests for extracted client pages that match a regular expression 2018-01-05 14:56:58 -08:00
freedmand
fc8280adea Fixes small issue with reading chunk files 2018-01-05 14:42:04 -08:00
freedmand
d08f9eb5a4 Fixes funky indentation in extraction unit test 2018-01-05 14:29:46 -08:00
Emil Tholin
2b3472b1b1 Remove async/await from lib 2018-01-05 23:19:40 +01:00
freedmand
30ddb3dd7e Adds a sapper extract CLI command, which scrapes the server to run as a static website starting at the site's root.
TESTED=Basic unit test ensuring relevant routes are added.
2018-01-05 14:16:30 -08:00
Luke Edwards
0c891ba79e require chokidar lazily; dev-mode only 2018-01-05 00:39:17 -08:00
Rich Harris
ee94f355d5 update marked due to vulnerability 2018-01-04 17:17:08 -05:00
Rich Harris
bea9b7965a update package-lock 2018-01-04 17:13:17 -05:00
Rich Harris
1312aede1f -> v0.3.2 2018-01-03 13:48:03 -05:00
Rich Harris
50e307e0c0 add test for programmatic prefetch 2018-01-03 13:46:07 -05:00
Emil Tholin
e87ac1f367 Export prefetch 2018-01-03 18:39:51 +01:00
Rich Harris
5da9d0926a update README, add license 2018-01-01 16:50:21 -05:00
Rich Harris
9538499d51 -> v0.3.1 2017-12-31 16:36:02 -05:00
Rich Harris
ff1e632057 -> v0.3.0 2017-12-31 16:10:36 -05:00
Rich Harris
aeeb231477 Merge branch 'master' of github.com:sveltejs/sapper 2017-12-31 16:08:14 -05:00
Rich Harris
d1940db8c0 Merge pull request #56 from sveltejs/gh-55
handle nested routes
2017-12-31 16:08:08 -05:00
Rich Harris
98f9a64b64 specify pkg.files 2017-12-31 16:05:26 -05:00
Rich Harris
b9bef802d3 lint 2017-12-31 16:05:18 -05:00
Rich Harris
a7024b3806 fix index route 2017-12-31 16:04:54 -05:00
Rich Harris
423e02aeae handle nested routes - fixes #55 2017-12-31 13:25:52 -05:00
Rich Harris
12b73ecebf Merge pull request #54 from sveltejs/preload-req
pass server request object to preload
2017-12-31 13:02:24 -05:00
Rich Harris
e1bc38b5a7 bump timeout 2017-12-31 12:49:17 -05:00
Rich Harris
b66f624f01 pass server request object to preload 2017-12-30 17:31:07 -05:00
Rich Harris
502dd547d1 only preload once - fixes #49 2017-12-25 18:06:27 -05:00
Rich Harris
4c343490d2 merge master -> gh-49 2017-12-25 17:34:27 -05:00
Rich Harris
b3027c5816 port runtime to typescript, move runtime/app.js to runtime.js 2017-12-25 17:14:39 -05:00
Rich Harris
c29e8022cc replace all occurences of variables 2017-12-25 16:18:19 -05:00
Rich Harris
e4cd4c9cb0 Merge pull request #52 from sveltejs/gh-48
cancel navigation if overtaken by a second navigation
2017-12-25 16:17:48 -05:00
Rich Harris
feddad42b2 failing test for #49 2017-12-24 21:34:28 -05:00
Rich Harris
3c4ebcda30 cancel navigation if overtaken by a second navigation - fixes #48 2017-12-24 20:37:00 -05:00
Rich Harris
75aedf4663 -> v0.2.10 2017-12-24 07:56:01 -05:00
Rich Harris
c8366dec74 handle deep links 2017-12-24 07:53:39 -05:00
Rich Harris
9a936669c6 -> v0.2.9 2017-12-23 15:01:49 -05:00
Rich Harris
0226bd90c6 dont write service-worker.js and index.html to disk in prod mode 2017-12-23 15:01:29 -05:00
Rich Harris
e1926e1bcb -> v0.2.8 2017-12-23 13:16:58 -05:00
Rich Harris
db1c1f332a Add goto function (#46)
* WIP

* programmatic navigation

* get tests working

* wait longer
2017-12-23 13:15:40 -05:00
Rich Harris
e8d510b261 dont use /tmp, it fails when now server wakes after a sleep 2017-12-23 12:27:51 -05:00
Rich Harris
f8e237b265 make invalidation message look less like an error 2017-12-23 10:55:55 -05:00
Rich Harris
68c2f2e388 linting 2017-12-22 09:19:32 -05:00
Rich Harris
0bcb61650b use nightmare for testing 2017-12-21 16:48:53 -05:00
Rich Harris
43a12a8331 -> v0.2.7 2017-12-21 13:40:10 -05:00
Rich Harris
f0feab5738 fix typo 2017-12-21 13:39:13 -05:00
Rich Harris
e9203b4d71 empty .sapper when building 2017-12-21 12:01:53 -05:00
Rich Harris
8e79e706e6 -> v0.2.6 2017-12-21 11:56:04 -05:00
Rich Harris
4b495f44fd Merge pull request #42 from sveltejs/gh-40
render main.js back to templates, to allow relative imports
2017-12-21 11:54:53 -05:00
Rich Harris
222a750b7b render main.js back to templates, to allow relative imports - fixes #40 2017-12-21 11:38:03 -05:00
Rich Harris
5b214c964c -> v0.2.5 2017-12-21 08:59:00 -05:00
Rich Harris
95f99fd378 Merge pull request #39 from sveltejs/windows-nested-routes
use /, not path.sep, when creating routes
2017-12-21 08:55:41 -05:00
Rich Harris
1bed4b0670 use /, not path.sep, when creating routes 2017-12-21 08:46:40 -05:00
Rich Harris
9d4890913a Merge pull request #37 from sveltejs/gh-33
error if routes clash
2017-12-20 21:00:04 -05:00
Rich Harris
f50d3c4262 error if routes clash - fixes #33 2017-12-20 20:46:28 -05:00
Rich Harris
8925e541d5 Merge pull request #36 from sveltejs/gh-19
stream responses
2017-12-20 18:50:06 -05:00
Rich Harris
a48afb77d3 stream responses - fixes #19 2017-12-20 18:39:39 -05:00
Rich Harris
45e845ee92 Merge pull request #35 from sveltejs/gh-2
add preload header for main.js and current route
2017-12-20 18:13:07 -05:00
Rich Harris
492f024d2a add preload header for main.js and current route - fixes #2 2017-12-20 18:04:11 -05:00
Rich Harris
8d40992cf1 Merge pull request #34 from sveltejs/gh-13
Rebundle when routes/templates change
2017-12-20 17:51:04 -05:00
Rich Harris
4232f75b19 rebundle when main.js changes 2017-12-20 17:36:22 -05:00
Rich Harris
fefb0d96d7 rebundle when routes and templates change 2017-12-20 17:34:23 -05:00
Rich Harris
cd91bf2ca4 -> v0.2.4 2017-12-20 16:33:54 -05:00
Rich Harris
7466e8da82 -> v0.2.3 2017-12-20 09:31:51 -05:00
Rich Harris
463307db86 pass URL to 404 handler 2017-12-20 08:29:28 -05:00
Rich Harris
2a68394dce Merge pull request #31 from sveltejs/possible-windows-fix-3
posixify import paths
2017-12-20 08:28:32 -05:00
Rich Harris
c8fe0679ae posixify import paths 2017-12-19 20:48:44 -05:00
Rich Harris
51d45cf38f -> v0.2.2 2017-12-19 08:55:40 -05:00
Rich Harris
92fbc28e11 make dest dir in buid as well as dev 2017-12-19 08:54:25 -05:00
Rich Harris
c1b1b3ed63 add some tests - closes #10 2017-12-18 21:39:17 -05:00
Rich Harris
cd8b9ddb14 -> v0.2.1 2017-12-18 17:11:17 -05:00
Rich Harris
924855d248 Merge pull request #26 from sveltejs/gh-14
inject HMR import
2017-12-18 17:08:08 -05:00
Rich Harris
8b516ef9bd inject HMR import - fixes #14 2017-12-18 17:07:17 -05:00
Rich Harris
8604088f3d -> v0.2.0 2017-12-18 15:48:28 -05:00
Rich Harris
8da3ca16ab Separate build step (closes #21) 2017-12-18 15:38:56 -05:00
Rich Harris
d0dd1d6cc9 Merge pull request #23 from sveltejs/remove-webpack-dev-middleware
remove webpack-dev-middleware
2017-12-18 12:02:27 -05:00
Rich Harris
8270463281 remove webpack-dev-middleware 2017-12-18 11:41:02 -05:00
Rich Harris
514331b5e3 -> v0.1.5 2017-12-17 21:39:55 -05:00
Rich Harris
63d39575be starting to lose my sense of humour 2017-12-17 21:39:43 -05:00
Rich Harris
3af5503009 -> v0.1.4 2017-12-17 21:34:28 -05:00
Rich Harris
c1de442dd1 argh not again 2017-12-17 21:34:16 -05:00
Rich Harris
0c6b7e3836 -> v0.1.3 2017-12-17 21:07:55 -05:00
Rich Harris
dc5e2543cb argh copypasta fail 2017-12-17 21:07:34 -05:00
Rich Harris
ecc7b80d91 -> v0.1.2 2017-12-17 20:43:52 -05:00
Rich Harris
40024e7d86 Merge pull request #18 from sveltejs/make-dest-dir
create dest dir immediately - should fix some CI failures
2017-12-17 20:41:55 -05:00
Rich Harris
6f71f7ad4d Merge pull request #17 from sveltejs/gh-11
use atime.getTime() and mtime.getTime() - hopefully fixes #11
2017-12-17 20:41:30 -05:00
Rich Harris
6eb99b195e use atime.getTime() and mtime.getTime() - hopefully fixes #11 2017-12-17 20:39:18 -05:00
Rich Harris
9e08fee9a1 create dest dir immediately - should fix some CI failures 2017-12-17 20:24:58 -05:00
Rich Harris
442ce366e2 -> v0.1.1 2017-12-17 15:18:03 -05:00
Rich Harris
dc929fcd83 inject app path, since dest might be /tmp causing resolution failure 2017-12-17 15:17:23 -05:00
Rich Harris
2dc246398b -> v0.1.0 2017-12-17 15:05:51 -05:00
Rich Harris
b7ac067459 always write to .sapper, not templates, since we cant guarantee fs access 2017-12-17 15:04:20 -05:00
Rich Harris
8b50ff34b8 named exports, for tree-shakeability when we add new stuff 2017-12-17 15:01:26 -05:00
Rich Harris
62abdb2a87 remove magic return values from server routes 2017-12-17 15:01:05 -05:00
Rich Harris
34d0bae4a1 -> v0.0.22 2017-12-16 20:31:37 -05:00
Rich Harris
4f0b336627 fix prod mode 2017-12-16 20:30:14 -05:00
Rich Harris
e71bf298fb inject __dev__ variable 2017-12-16 20:19:02 -05:00
Rich Harris
e4936375db support HMR 2017-12-16 20:10:49 -05:00
Rich Harris
08ff7ad234 shuffle things around 2017-12-16 17:19:28 -05:00
Rich Harris
5995b7ae6a run webpack in watch mode during dev 2017-12-16 16:50:46 -05:00
Rich Harris
71ed3864b7 run webpack in watch mode 2017-12-16 16:24:36 -05:00
Rich Harris
bd7f6e2b1a expose main.js 2017-12-16 15:40:22 -05:00
82 changed files with 5897 additions and 907 deletions

16
.gitignore vendored
View File

@@ -1,2 +1,16 @@
.DS_Store
node_modules
yarn.lock
node_modules
cypress/screenshots
test/app/.sapper
test/app/app/manifest
runtime.js
runtime.js.map
cli.js
cli.js.map
middleware.js
middleware.js.map
core.js
core.js.map
webpack/config.js
webpack/config.js.map

22
.travis.yml Normal file
View File

@@ -0,0 +1,22 @@
sudo: false
language: node_js
node_js:
- "6"
- "stable"
env:
global:
- BUILD_TIMEOUT=10000
addons:
apt:
packages:
- xvfb
install:
- export DISPLAY=':99.0'
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
- npm install
- (cd test/app && npm install)

165
CHANGELOG.md Normal file
View File

@@ -0,0 +1,165 @@
# sapper changelog
## 0.7.6
* Prevent client-side navigation to server route ([#145](https://github.com/sveltejs/sapper/issues/145))
* Don't serve error page for server route errors ([#138](https://github.com/sveltejs/sapper/issues/138))
## 0.7.5
* Allow dynamic parameters inside route parts ([#139](https://github.com/sveltejs/sapper/issues/139))
## 0.7.4
* Force `NODE_ENV='production'` when running `build` or `export` ([#141](https://github.com/sveltejs/sapper/issues/141))
* Use source-map-support ([#134](https://github.com/sveltejs/sapper/pull/134))
## 0.7.3
* Handle webpack assets that are arrays instead of strings ([#131](https://github.com/sveltejs/sapper/pull/131))
* Wait for new server to start before broadcasting HMR update ([#129](https://github.com/sveltejs/sapper/pull/129))
## 0.7.2
* Add `hmr-client.js` to package
* Wait until first successful client build before creating service-worker.js
## 0.7.1
* Add missing `tslib` dependency
## 0.7.0
* Restructure app layout (see [migration guide](https://sapper.svelte.technology/guide#0-6-to-0-7)) ([#126](https://github.com/sveltejs/sapper/pull/126))
* Support `this.redirect(status, location)` and `this.error(status, error)` in `preload` functions ([#127](https://github.com/sveltejs/sapper/pull/127))
* Add `sapper dev` command
* Add `sapper --help` command
## 0.6.4
* Prevent phantom HMR requests in production mode ([#114](https://github.com/sveltejs/sapper/pull/114))
## 0.6.3
* Ignore non-HTML responses when crawling during `export`
* Build in prod mode for `export`
## 0.6.2
* Handle unspecified type in `sapper export`
## 0.6.1
* Fix `pkg.files` and `pkg.bin`
## 0.6.0
* Hydrate on first load, and only on first load ([#93](https://github.com/sveltejs/sapper/pull/93))
* Identify clashes between page and server routes ([#96](https://github.com/sveltejs/sapper/pull/96))
* Remove Express-specific utilities, for compatbility with Polka et al ([#94](https://github.com/sveltejs/sapper/issues/94))
* Return a promise from `init` when first page has rendered ([#99](https://github.com/sveltejs/sapper/issues/99))
* Handle invalid hash links ([#104](https://github.com/sveltejs/sapper/pull/104))
* Avoid `URLSearchParams` ([#107](https://github.com/sveltejs/sapper/pull/107))
* Don't automatically set `Content-Type` for server routes ([#111](https://github.com/sveltejs/sapper/pull/111))
* Handle empty query string routes, e.g. `/?` ([#105](https://github.com/sveltejs/sapper/pull/105))
## 0.5.1
* Only write service-worker.js to filesystem in dev mode ([#90](https://github.com/sveltejs/sapper/issues/90))
## 0.5.0
* Experimental support for `sapper export` ([#9](https://github.com/sveltejs/sapper/issues/9))
* Lazily load chokidar, for faster startup ([#64](https://github.com/sveltejs/sapper/pull/64))
## 0.4.0
* `%sapper.main%` has been replaced with `%sapper.scripts%` ([#86](https://github.com/sveltejs/sapper/issues/86))
* Node 6 support ([#67](https://github.com/sveltejs/sapper/pull/67))
* Explicitly load css-loader and style-loader ([#72](https://github.com/sveltejs/sapper/pull/72))
* DELETE requests are handled with `del` exports ([#77](https://github.com/sveltejs/sapper/issues/77))
* Send preloaded data for first route to client, where possible ([#3](https://github.com/sveltejs/sapper/issues/3))
## 0.3.2
* Expose `prefetch` function ([#61](https://github.com/sveltejs/sapper/pull/61))
## 0.3.1
* Fix missing `runtime.js`
## 0.3.0
* Move `sapper/runtime/app.js` to `sapper/runtime.js`
* Cancel navigation if overtaken by second navigation ([#48](https://github.com/sveltejs/sapper/issues/48))
* Store preloaded data, to avoiding double prefetching ([#49](https://github.com/sveltejs/sapper/issues/49))
* Pass server request object to `preload` ([#54](https://github.com/sveltejs/sapper/pull/54))
* Nested routes ([#55](https://github.com/sveltejs/sapper/issues/55))
## 0.2.10
* Handle deep links correctly ([#44](https://github.com/sveltejs/sapper/issues/44))
## 0.2.9
* Don't write files to disk in prod mode
## 0.2.8
* Add `goto` function ([#29](https://github.com/sveltejs/sapper/issues/29))
* Don't use `/tmp` as destination in Now environments
## 0.2.7
* Fix streaming bug
## 0.2.6
* Render main.js back to templates, to allow relative imports ([#40](https://github.com/sveltejs/sapper/issues/40))
## 0.2.5
* Fix nested routes on Windows ([#39](https://github.com/sveltejs/sapper/pull/39))
* Rebundle when routes and main.js change ([#34](https://github.com/sveltejs/sapper/pull/34))
* Add `Link...preload` headers for JavaScript assets ([#2](https://github.com/sveltejs/sapper/issues/2))
* Stream document up to first dynamic content ([#19](https://github.com/sveltejs/sapper/issues/19))
* Error if routes clash ([#33](https://github.com/sveltejs/sapper/issues/33))
## 0.2.4
* Posixify path to HMR client
## 0.2.3
* Posixify import paths, even on Windows ([#31](https://github.com/sveltejs/sapper/pull/31))
* Pass `url` to 404 handler
## 0.2.2
* Create destination directory when building, don't assume it's already there from dev mode
* We have tests now!
## 0.2.1
* Inject HMR logic in dev mode
## 0.2.0
* Separate `sapper build` from prod server ([#21](https://github.com/sveltejs/sapper/issues/21))
## 0.1.3-5
* Fix typo
## 0.1.2
* Use `atime.getTime()` and `mtime.getTime()` instead of `atimeMs` and `mtimeMs` ([#11](https://github.com/sveltejs/sapper/issues/11))
* Make dest dir before anyone tries to write to it ([#18](https://github.com/sveltejs/sapper/pull/18))
## 0.1.1
* Expose resolved pathname to `sapper/runtime/app.js` as `__app__` inside main.js
## 0.1.0
* First public preview

9
LICENSE Normal file
View File

@@ -0,0 +1,9 @@
Copyright (c) 2017 [these people](https://github.com/sveltejs/sapper/graphs/contributors).
Permission is hereby granted by the authors of this software, to any person, to use the software for any purpose, free of charge, including the rights to run, read, copy, change, distribute and sell it, and including usage rights to any patents the authors may hold on it, subject to the following conditions:
This license, or a link to its text, must be included with all copies of the software and any derivative works.
Any modification to the software submitted to the authors may be incorporated into the software under the terms of this license.
The software is provided "as is", without warranty of any kind, including but not limited to the warranties of title, fitness, merchantability and non-infringement. The authors have no obligation to provide support or updates for the software, and may not be held liable for any damages, claims or other liability arising from its use.

151
README.md
View File

@@ -1,150 +1,37 @@
# sapper
Combat-ready apps, engineered by Svelte.
## This is not a thing yet
If you visit this README in a few weeks, hopefully it will have blossomed into the app development framework we deserve. Right now, it's just a set of ideas.
---
[Next.js](https://github.com/zeit/next.js/) introduced a beautiful idea — that you should be able to build your app as universal React components in a special `pages` directory, and the framework should take care of routing and rendering on both client and server. What if we did the same thing for Svelte?
High-level goals:
* Extreme ease of development
* Code-splitting and HMR out of the box (probably via webpack)
* Best-in-class performance
* As little magic as possible. Anyone should be able to understand how everything fits together, and e.g. make changes to the webpack config
* Links are just `<a>` tags, no special `<Link>` components
[Military-grade progressive web apps, powered by Svelte.](https://sapper.svelte.technology)
## Design
## What is Sapper?
A Sapper app is just an Express app (conventionally, `server.js`) that uses the `sapper` middleware:
Sapper is a framework for building high-performance universal web apps. [Read the guide](https://sapper.svelte.technology/guide) or the [introductory blog post](https://svelte.technology/blog/sapper-towards-the-ideal-web-app-framework) to learn more.
```js
const app = require('express')();
const sapper = require('sapper');
app.use(sapper());
## Get started
const { PORT = 3000 } = process.env;
app.listen(PORT, () => {
console.log(`listening on port ${PORT}`);
});
Clone the [starter project template](https://github.com/sveltejs/sapper-template) with [degit](https://github.com/rich-harris/degit)...
```bash
npx degit sveltejs/sapper-template my-app
```
The middleware serves pages that match files in the `routes` directory, and assets generated by webpack. In development mode, the middleware once activated watches `routes` to keep the app up-to-date.
...then install dependencies and start the dev server...
## Routing
Like Next, routes are defined by the project directory structure, but with some crucial differences:
* Files with an `.html` extension are treated as Svelte components. The `routes/about.html` (or `routes/about/index.html`) would create the `/about` route.
* Files with a `.js` or `.mjs` extension are more generic route handlers. These files should export functions corresponding to the HTTP methods they support (example below).
* Instead of route masking, we embed parameters in the filename. For example `post/[id].html` maps to `/post/:id`, and the component will be rendered with the appropriate parameter.
* Nested routes (read [this article](https://joshduff.com/2015-06-why-you-need-a-state-router.md)) can be handled by creating a file that matches the subroute — for example, `routes/app/settings/[submenu].html` would match `/app/settings/profile` *and* `app/settings`, but in the latter case the `submenu` parameter would be `null`.
An example of a generic route:
```js
// routes/api/post/[id].js
export async function get(req, res) {
try {
const data = await getPostFromDatabase(req.params.id);
const json = JSON.stringify(data);
res.set({
'Content-Type': 'application/json',
'Content-Length': json.length
});
res.send(json);
} catch (err) {
res.status(500).send(err.message);
}
}
```bash
cd my-app
npm install
npm run dev
```
Or, if you omit the `res` argument, it can use the return value:
...and navigate to [localhost:3000](http://localhost:3000). To build and run in production mode:
```js
// routes/api/post/[id].js
export async function get(req) {
return await getPostFromDatabase(req.params.id);
}
```bash
npm run build
npm start
```
## Client-side app
## License
Sapper will create (and in development mode, update) a barebones `main.js` file that dynamically imports individual routes and renders them — something like this:
```js
window.addEventListener('click', event => {
let a = event.target;
while (a && a.nodeName !== 'A') a = a.parentNode;
if (!a) return;
if (navigate(new URL(a.href))) event.preventDefault();
});
const target = document.querySelector('#sapper');
let component;
function navigate(url) {
if (url.origin !== window.location.origin) return;
let match;
let params = {};
const query = {};
function render(mod) {
if (component) {
component.destroy();
} else {
target.innerHTML = '';
}
component = new mod.default({
target,
data: { query, params },
hydrate: !!component
});
}
if (url.pathname === '/about') {
import('/about/index.html').then(render);
} else if (url.pathname === '/') {
import('/index.js').then(render);
} else if (match = /^\/post\/([^\/]+)$/.exec(url.pathname)) {
params.id = match[1];
import('/post/[id].html').then(render);
} else if (match = /^\/([^\/]+)$/.exec(url.pathname)) {
params.wildcard = match[1];
import('/[wildcard].html').then(render);
}
return true;
}
navigate(window.location);
```
We're glossing over a lot of important stuff here — e.g. handling `popstate` — but you get the idea. Knowledge of all the possible routes means we can generate optimal code, much in the same way that statically analysing Svelte templates allows the compiler to generate optimal code.
## Things to figure out
* How to customise the overall page template
* An equivalent of `getInitialProps`
* Critical CSS
* `store` integration
* Route transitions
* Equivalent of `next export`
* A good story for realtime/GraphQL stuff
* Service worker
* Using `Link...rel=preload` headers to push main.js/[route].js plus styles
* ...and lots of other things that haven't occurred to me yet.
[LIL](LICENSE)

21
appveyor.yml Normal file
View File

@@ -0,0 +1,21 @@
version: "{build}"
shallow_clone: true
init:
- git config --global core.autocrlf false
build: off
environment:
matrix:
# node.js
- nodejs_version: stable
install:
- ps: Install-Product node $env:nodejs_version
- npm install
test_script:
- node --version && npm --version
- npm test

View File

@@ -1,130 +0,0 @@
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const rimraf = require('rimraf');
const mkdirp = require('mkdirp');
const create_routes = require('./lib/utils/create_routes.js');
const templates = require('./lib/templates.js');
const create_app = require('./lib/utils/create_app.js');
const create_compiler = require('./lib/utils/create_compiler.js');
const escape_html = require('escape-html');
const { src, dest, dev } = require('./lib/config.js');
module.exports = function connect(opts) {
mkdirp(dest);
rimraf.sync(path.join(dest, '**/*'));
let routes = create_routes(
glob.sync('**/*.+(html|js|mjs)', { cwd: src })
);
create_app(src, dest, routes, opts);
const compiler = create_compiler(
dest,
routes,
dev
);
return async function(req, res, next) {
const url = req.url.replace(/\?.+/, '');
if (url === '/service-worker.js') {
await compiler.ready;
res.set({
'Content-Type': 'application/javascript',
'Cache-Control': dev ? 'no-cache' : 'max-age=600'
});
res.end(compiler.service_worker);
}
else if (url === '/index.html') {
await compiler.ready;
res.set({
'Content-Type': 'text/html',
'Cache-Control': dev ? 'no-cache' : 'max-age=600'
});
res.end(compiler.shell);
}
else if (url.startsWith('/client/')) {
await compiler.ready;
res.set({
'Content-Type': 'application/javascript',
'Cache-Control': 'max-age=31536000'
});
res.end(compiler.asset_cache[url]);
}
else {
// whatever happens, we're going to serve some HTML
res.set({
'Content-Type': 'text/html'
});
try {
for (const route of routes) {
if (route.test(url)) {
await compiler.ready;
req.params = route.exec(url);
const chunk = compiler.chunks[route.id];
const mod = require(path.resolve(dest, 'server', chunk));
if (route.type === 'page') {
let data = { params: req.params, query: req.query };
if (mod.default.preload) data = Object.assign(data, await mod.default.preload(data));
const { html, head, css } = mod.default.render(data);
const page = templates.render(200, {
main: compiler.client_main,
html,
head: `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`,
styles: (css && css.code ? `<style>${css.code}</style>` : '')
});
res.status(200);
res.end(page);
}
else {
const handler = mod[req.method.toLowerCase()];
if (handler) {
if (handler.length === 2) {
handler(req, res);
} else {
const data = await handler(req);
// TODO headers, error handling
if (typeof data === 'string') {
res.end(data);
} else {
res.end(JSON.stringify(data));
}
}
}
}
return;
}
}
res.status(404).end(templates.render(404, {
title: 'Not found',
status: 404,
method: req.method,
url
}));
} catch(err) {
res.status(500).end(templates.render(500, {
title: (err && err.name) || 'Internal server error',
url,
error: escape_html(err && (err.details || err.message || err) || 'Unknown error'),
stack: err && err.stack.split('\n').slice(1).join('\n')
}));
}
}
};
};

9
cypress.json Normal file
View File

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

28
hmr-client.js Normal file
View File

@@ -0,0 +1,28 @@
let source;
function check() {
if (module.hot.status() === 'idle') {
module.hot.check(true).then(modules => {
console.log(`HMR updated`);
});
}
}
export function connect(port) {
if (source || !window.EventSource) return;
source = new EventSource(`http://localhost:${port}/hmr`);
source.onopen = function(event) {
console.log(`HMR connected`);
};
source.onmessage = function(event) {
const data = JSON.parse(event.data);
if (!data) return; // just a heartbeat
if (data.status === 'completed') {
check();
}
};
}

View File

@@ -1,12 +0,0 @@
const path = require('path');
exports.dev = process.env.NODE_ENV !== 'production';
exports.templates = path.resolve(process.env.SAPPER_TEMPLATES || 'templates');
exports.src = path.resolve(process.env.SAPPER_ROUTES || 'routes');
exports.dest = path.resolve(
process.env.NOW ? '/tmp' :
process.env.SAPPER_DEST || '.sapper'
);

View File

@@ -1,16 +0,0 @@
const glob = require('glob');
const create_routes = require('./utils/create_routes.js');
const { src } = require('./config.js');
const route_manager = {
routes: create_routes(
glob.sync('**/*.+(html|js|mjs)', { cwd: src })
),
onchange(fn) {
// TODO in dev mode, keep this updated, and allow
// webpack compiler etc to hook into it
}
};
module.exports = route_manager;

View File

@@ -1,43 +0,0 @@
const fs = require('fs');
const glob = require('glob');
const templates = glob.sync('*.html', { cwd: 'templates' })
.map(file => {
const template = fs.readFileSync(`templates/${file}`, 'utf-8');
const status = file.replace('.html', '').toLowerCase();
if (!/^[0-9x]{3}$/.test(status)) {
throw new Error(`Bad template — should be a valid status code like 404.html, or a wildcard like 2xx.html`);
}
const specificity = (
(status[0] === 'x' ? 0 : 4) +
(status[1] === 'x' ? 0 : 2) +
(status[2] === 'x' ? 0 : 1)
);
const pattern = new RegExp(`^${status.split('').map(d => d === 'x' ? '\\d' : d).join('')}$`);
return {
test: status => pattern.test(status),
specificity,
render(data) {
return template.replace(/%sapper\.(\w+)%/g, (match, key) => {
return key in data ? data[key] : '';
});
}
}
})
.sort((a, b) => b.specificity - a.specificity);
exports.render = (status, data) => {
const template = templates.find(template => template.test(status));
if (template) return template.render(data);
return `Missing template for status code ${status}`;
};
exports.onchange = fn => {
// TODO in dev mode, keep this updated, and allow
// webpack compiler etc to hook into it
};

View File

@@ -1,26 +0,0 @@
const fs = require('fs');
const path = require('path');
const template = fs.readFileSync(path.resolve(__dirname, '../../templates/main.js'), 'utf-8');
module.exports = function create_app(src, dest, routes, options) {
// TODO in dev mode, watch files
const code = routes
.filter(route => route.type === 'page')
.map(route => {
const params = route.dynamic.length === 0 ?
'{}' :
`{ ${route.dynamic.map((part, i) => `${part}: match[${i + 1}]`).join(', ') } }`;
return `{ pattern: ${route.pattern}, params: match => (${params}), load: () => import(/* webpackChunkName: "${route.id}" */ '${src}/${route.file}') }`
})
.join(',\n\t');
const main = template
.replace('__app__', path.resolve(__dirname, '../../runtime/app.js'))
.replace('__selector__', options.selector || 'main')
.replace('__routes__', code);
fs.writeFileSync(path.join(dest, 'main.js'), main);
};

View File

@@ -1,104 +0,0 @@
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const webpack = require('webpack');
const templates = require('../templates.js');
module.exports = function create_webpack_compiler(dest, routes, dev) {
const compiler = {};
const client = webpack(
require(path.resolve('webpack.client.config.js'))
);
const server = webpack(
require(path.resolve('webpack.server.config.js'))
);
if (false) { // TODO watch in dev
// TODO how can we invalidate compiler.client_main when watcher restarts?
compiler.client_main = new Promise((fulfil, reject) => {
client.watch({}, (err, stats) => {
if (err || stats.hasErrors()) {
// TODO handle errors
}
const filename = stats.toJson().assetsByChunkName.main;
fulfil(`/client/${filename}`);
});
});
// TODO server
} else {
compiler.ready = Promise.all([
new Promise((fulfil, reject) => {
client.run((err, stats) => {
console.log(stats.toString({ colors: true }));
const info = stats.toJson();
if (err || stats.hasErrors()) {
reject(err || info.errors[0]);
}
compiler.client_main = `/client/${info.assetsByChunkName.main}`;
compiler.assets = info.assets.map(asset => `/client/${asset.name}`);
compiler.asset_cache = {};
compiler.assets.forEach(file => {
compiler.asset_cache[file] = fs.readFileSync(path.join(dest, file), 'utf-8');
});
fulfil();
});
}),
new Promise((fulfil, reject) => {
server.run((err, stats) => {
console.log(stats.toString({ colors: true }));
const info = stats.toJson();
if (err || stats.hasErrors()) {
reject(err || info.errors[0]);
}
compiler.chunks = info.assetsByChunkName;
fulfil();
});
})
]).then(() => {
const assets = glob.sync('**', { cwd: 'assets', nodir: true });
const route_code = `[${
routes
.filter(route => route.type === 'page')
.map(route => `{ pattern: ${route.pattern} }`)
.join(', ')
}]`;
compiler.service_worker = fs.readFileSync('templates/service-worker.js', 'utf-8')
.replace('__timestamp__', Date.now())
.replace('__assets__', JSON.stringify(assets))
.replace('__shell__', JSON.stringify(compiler.assets.concat('/index.html')))
.replace('__routes__', route_code);
compiler.shell = templates.render(200, {
styles: '',
head: '',
html: '<noscript>Please enable JavaScript!</noscript>',
main: compiler.client_main
});
// useful for debugging, but the files are served from memory
fs.writeFileSync(path.resolve(dest, 'service-worker.js'), compiler.service_worker);
fs.writeFileSync(path.resolve(dest, 'index.html'), compiler.shell);
});
compiler.get_chunk = async id => {
return path.resolve(dest, 'server', compiler.chunks[id]);
};
}
return compiler;
};

View File

@@ -1,53 +0,0 @@
const path = require('path');
module.exports = function create_matchers(files) {
return files
.map(file => {
if (/(^|\/|\\)_/.test(file)) return;
const parts = file.replace(/\.(html|js|mjs)$/, '').split(path.sep);
if (parts[parts.length - 1] === 'index') parts.pop();
const id = parts.join('_').replace(/[[\]]/g, '$') || '_';
const dynamic = parts
.filter(part => part[0] === '[')
.map(part => part.slice(1, -1));
const pattern = new RegExp(
`^\\/${parts.map(p => p[0] === '[' ? '([^/]+)' : p).join('\\/')}$`
);
const test = url => pattern.test(url);
const exec = url => {
const match = pattern.exec(url);
if (!match) return;
const params = {};
dynamic.forEach((param, i) => {
params[param] = match[i + 1];
});
return params;
};
return {
id,
type: path.extname(file) === '.html' ? 'page' : 'route',
file,
pattern,
test,
exec,
parts,
dynamic
};
})
.filter(Boolean)
.sort((a, b) => {
return (
(a.dynamic.length - b.dynamic.length) || // match static paths first
(b.parts.length - a.parts.length) // match longer paths first
);
});
}

View File

@@ -1,51 +0,0 @@
const path = require('path');
const assert = require('assert');
const create_matchers = require('./create_matchers.js');
describe('create_matchers', () => {
it('sorts routes correctly', () => {
const matchers = create_matchers(['index.html', 'about.html', '[wildcard].html', 'post/[id].html']);
assert.deepEqual(
matchers.map(m => m.file),
[
'about.html',
'index.html',
'post/[id].html',
'[wildcard].html'
]
);
});
it('generates params', () => {
const matchers = create_matchers(['index.html', 'about.html', '[wildcard].html', 'post/[id].html']);
let file;
let params;
for (let i = 0; i < matchers.length; i += 1) {
const matcher = matchers[i];
if (params = matcher.exec('/post/123')) {
file = matcher.file;
break;
}
}
assert.equal(file, 'post/[id].html');
assert.deepEqual(params, {
id: '123'
});
});
it('ignores files and directories with leading underscores', () => {
const matches = create_matchers(['index.html', '_foo.html', 'a/_b/c/d.html', 'e/f/g/h.html', 'i/_j.html']);
assert.deepEqual(
matches.map(m => m.file),
[
'e/f/g/h.html',
'index.html'
]
);
});
});

View File

@@ -1,2 +1,4 @@
--require source-map-support/register
--recursive
utils/**/*.test.js
test/unit/**/*.js
test/common/test.js

View File

@@ -1,22 +1,79 @@
{
"name": "sapper",
"version": "0.0.21",
"description": "Combat-ready apps, engineered by Svelte",
"main": "connect.js",
"version": "0.7.6",
"description": "Military-grade apps, engineered by Svelte",
"main": "middleware.js",
"bin": {
"sapper": "cli.js"
},
"files": [
"cli.js",
"core.js",
"middleware.js",
"runtime",
"runtime.js",
"hmr-client.js",
"webpack"
],
"directories": {
"test": "test"
},
"dependencies": {
"chalk": "^2.3.0",
"cheerio": "^1.0.0-rc.2",
"chokidar": "^1.7.0",
"code-frame": "^5.0.0",
"escape-html": "^1.0.3",
"express": "^4.16.2",
"get-port": "^3.2.0",
"glob": "^7.1.2",
"locate-character": "^2.0.5",
"mime": "^2.2.0",
"mkdirp": "^0.5.1",
"mri": "^1.1.0",
"node-fetch": "^1.7.3",
"relative": "^3.0.2",
"require-relative": "^0.8.7",
"rimraf": "^2.6.2",
"sander": "^0.6.0",
"serialize-javascript": "^1.4.0",
"source-map-support": "^0.5.3",
"tslib": "^1.8.1",
"url-parse": "^1.2.0",
"wait-port": "^0.2.2",
"walk-sync": "^0.3.2",
"webpack": "^3.10.0"
},
"devDependencies": {
"mocha": "^4.0.1"
"@std/esm": "^0.19.7",
"@types/glob": "^5.0.34",
"@types/mkdirp": "^0.5.2",
"@types/rimraf": "^2.0.2",
"compression": "^1.7.1",
"css-loader": "^0.28.7",
"electron": "^1.8.2",
"eslint": "^4.13.1",
"eslint-plugin-import": "^2.8.0",
"mocha": "^4.0.1",
"nightmare": "^2.10.0",
"npm-run-all": "^4.1.2",
"rollup": "^0.53.0",
"rollup-plugin-json": "^2.3.0",
"rollup-plugin-string": "^2.0.2",
"rollup-plugin-typescript": "^0.8.1",
"style-loader": "^0.19.1",
"svelte": "^1.49.1",
"svelte-loader": "^2.3.2",
"ts-node": "^4.1.0",
"typescript": "^2.6.2"
},
"scripts": {
"test": "mocha --opts mocha.opts"
"cy:open": "cypress open",
"test": "mocha --opts mocha.opts",
"pretest": "npm run build",
"build": "rollup -c",
"dev": "rollup -cw",
"prepublish": "npm test"
},
"repository": "https://github.com/sveltejs/sapper",
"keywords": [

43
rollup.config.js Normal file
View File

@@ -0,0 +1,43 @@
import typescript from 'rollup-plugin-typescript';
import string from 'rollup-plugin-string';
import json from 'rollup-plugin-json';
import pkg from './package.json';
const external = [].concat(
Object.keys(pkg.dependencies),
Object.keys(process.binding('natives')),
'sapper/core.js'
);
const paths = {
'sapper/core.js': './core.js'
};
const plugins = [
string({
include: '**/*.md'
}),
json(),
typescript({
typescript: require('typescript')
})
];
export default [
{ name: 'cli', banner: true },
{ name: 'core' },
{ name: 'middleware' },
{ name: 'runtime', format: 'es' },
{ name: 'webpack', file: 'webpack/config' }
].map(obj => ({
input: `src/${obj.name}/index.ts`,
output: {
file: `${obj.file || obj.name}.js`,
format: obj.format || 'cjs',
banner: obj.banner && '#!/usr/bin/env node',
paths,
sourcemap: true
},
external,
plugins
}));

1
runtime/README.md Normal file
View File

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

View File

@@ -1,181 +1,2 @@
const detach = node => {
node.parentNode.removeChild(node);
};
let component;
const scroll_history = {};
let uid = 1;
let cid;
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual'
}
const app = {
init(target, routes) {
function select_route(url) {
if (url.origin !== window.location.origin) return null;
for (const route of routes) {
const match = route.pattern.exec(url.pathname);
if (match) {
const params = route.params(match);
const query = {};
for (const [key, value] of url.searchParams) query[key] = value || true;
return { route, data: { params, query } };
}
}
}
function render(Component, data, scroll) {
Promise.resolve(
Component.preload ? Component.preload(data) : {}
).then(preloaded => {
if (component) {
component.destroy();
} else {
// first load — remove SSR'd <head> contents
const start = document.querySelector('#sapper-head-start');
let end = document.querySelector('#sapper-head-end');
if (start && end) {
while (start.nextSibling !== end) detach(start.nextSibling);
detach(start);
detach(end);
}
// preload additional routes
routes.reduce((promise, route) => promise.then(route.load), Promise.resolve());
}
component = new Component({
target,
data: Object.assign(data, preloaded),
hydrate: !!component
});
if (scroll) {
window.scrollTo(scroll.x, scroll.y);
}
});
}
function navigate(url, id) {
const selected = select_route(url);
if (selected) {
if (id) {
// popstate or initial navigation
cid = id;
} else {
// clicked on a link. preserve scroll state
scroll_history[cid] = scroll_state();
id = cid = ++uid;
scroll_history[cid] = { x: 0, y: 0 };
history.pushState({ id }, '', url.href);
}
selected.route.load().then(mod => {
render(mod.default, selected.data, scroll_history[id]);
});
cid = id;
return true;
}
}
function findAnchor(node) {
while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG <a> elements have a lowercase name
return node;
}
window.addEventListener('click', event => {
// Adapted from https://github.com/visionmedia/page.js
// MIT license https://github.com/visionmedia/page.js#license
if (which(event) !== 1) return;
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
if (event.defaultPrevented) return;
const a = findAnchor(event.target);
if (!a) return;
// check if link is inside an svg
// in this case, both href and target are always inside an object
const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
const href = svg ? a.href.baseVal : a.href;
if (href === window.location.href) {
event.preventDefault();
return;
}
// Ignore if tag has
// 1. 'download' attribute
// 2. rel='external' attribute
if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
// Ignore if <a> has a target
if (svg ? a.target.baseVal : a.target) return;
const url = new URL(href);
// Don't handle hash changes
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
if (navigate(url, null)) {
event.preventDefault();
}
});
function preload(event) {
const a = findAnchor(event.target);
if (!a || a.rel !== 'prefetch') return;
const selected = select_route(new URL(a.href));
if (selected) {
selected.route.load().then(mod => {
if (mod.default.preload) mod.default.preload(selected.data);
});
}
}
window.addEventListener('touchstart', preload);
window.addEventListener('mouseover', preload);
window.addEventListener('popstate', event => {
scroll_history[cid] = scroll_state();
if (event.state) {
navigate(new URL(window.location), event.state.id);
} else {
// hashchange
cid = ++uid;
history.replaceState({ id: cid }, '', window.location.href);
}
});
const scroll = scroll_history[uid] = scroll_state();
history.replaceState({ id: uid }, '', window.location.href);
navigate(new URL(window.location), uid);
}
};
function which(event) {
event = event || window.event;
return event.which === null ? event.button : event.which;
}
function scroll_state() {
return {
x: window.scrollX,
y: window.scrollY
};
}
export default app;
console.error('sapper/runtime/app.js has been deprecated in favour of sapper/runtime.js');
export * from '../runtime.js';

57
src/cli/build.ts Normal file
View File

@@ -0,0 +1,57 @@
import * as fs from 'fs';
import * as path from 'path';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import { create_compilers, create_app, create_routes, create_serviceworker } from 'sapper/core.js';
export default async function build({ src, dest, dev, entry }: {
src: string;
dest: string;
dev: boolean;
entry: { client: string, server: string }
}) {
mkdirp.sync(dest);
rimraf.sync(path.join(dest, '**/*'));
const routes = create_routes({ src });
// create app/manifest/client.js and app/manifest/server.js
create_app({ routes, src, dev });
const { client, server, serviceworker } = create_compilers();
const client_stats = await compile(client);
fs.writeFileSync(path.join(dest, 'client_info.json'), JSON.stringify(client_stats.toJson()));
await compile(server);
if (serviceworker) {
create_serviceworker({
routes,
client_files: client_stats.toJson().assets.map((chunk: { name: string }) => `/client/${chunk.name}`),
src
});
await compile(serviceworker);
}
}
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);
}
});
});
}

229
src/cli/dev.ts Normal file
View File

@@ -0,0 +1,229 @@
import * as fs from 'fs';
import * as path from 'path';
import * as net from 'net';
import * as chalk from 'chalk';
import * as child_process from 'child_process';
import * as http from 'http';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import { wait_for_port } from './utils';
import { create_compilers, create_app, create_routes, create_serviceworker, create_template } from 'sapper/core.js';
type Deferred = {
promise?: Promise<any>;
fulfil?: (value?: any) => void;
reject?: (err: Error) => void;
}
function deferred() {
const d: Deferred = {};
d.promise = new Promise((fulfil, reject) => {
d.fulfil = fulfil;
d.reject = reject;
});
return d;
}
function create_hot_update_server(port: number, interval = 10000) {
const clients = new Set();
const server = http.createServer((req, res) => {
if (req.url !== '/hmr') return;
req.socket.setKeepAlive(true);
res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/event-stream;charset=utf-8',
'Cache-Control': 'no-cache, no-transform',
'Connection': 'keep-alive',
// While behind nginx, event stream should not be buffered:
// http://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffering
'X-Accel-Buffering': 'no'
});
res.write('\n');
clients.add(res);
req.on('close', () => {
clients.delete(res);
});
});
server.listen(port);
function send(data: any) {
clients.forEach(client => {
client.write(`data: ${JSON.stringify(data)}\n\n`);
});
}
setInterval(() => {
send(null)
}, interval);
return { send };
}
export default async function dev(src: string, dir: string) {
rimraf.sync(dir);
mkdirp.sync(dir);
const chokidar = require('chokidar');
// initial build
const dev_port = await require('get-port')(10000);
const routes = create_routes({ src });
create_app({ routes, src, dev: true, dev_port });
const hot_update_server = create_hot_update_server(dev_port);
// TODO watch the configs themselves?
const compilers = create_compilers();
function watch_files(pattern: string, callback: () => void) {
const watcher = chokidar.watch(pattern, {
persistent: false
});
watcher.on('add', callback);
watcher.on('change', callback);
watcher.on('unlink', callback);
}
watch_files('routes/**/*.+(html|js|mjs)', () => {
const routes = create_routes({ src });
create_app({ routes, src, dev: true, dev_port });
});
watch_files('app/template.html', () => {
const template = create_template();
// TODO reload current page?
});
let proc: child_process.ChildProcess;
const deferreds = {
server: deferred(),
client: deferred()
};
const times = {
client_start: Date.now(),
server_start: Date.now(),
serviceworker_start: Date.now()
};
compilers.server.plugin('invalid', () => {
times.server_start = Date.now();
// TODO print message
deferreds.server = deferred();
});
compilers.server.watch({}, (err: Error, stats: any) => {
if (err) {
console.error(chalk.red(err.message));
} else if (stats.hasErrors()) {
// print errors. TODO notify client
stats.toJson().errors.forEach((error: Error) => {
console.error(error); // TODO make this look nice
});
} else {
console.log(`built server in ${Date.now() - times.server_start}ms`); // TODO prettify
const server_info = stats.toJson();
fs.writeFileSync(path.join(dir, 'server_info.json'), JSON.stringify(server_info, null, ' '));
deferreds.client.promise.then(() => {
function restart() {
wait_for_port(3000, deferreds.server.fulfil); // TODO control port
}
if (proc) {
proc.kill();
proc.on('exit', restart);
} else {
restart();
}
proc = child_process.fork(`${dir}/server.js`, [], {
cwd: process.cwd(),
env: Object.assign({}, process.env)
});
});
}
});
compilers.client.plugin('invalid', (filename: string) => {
times.client_start = Date.now();
deferreds.client = deferred();
// TODO we should delete old assets. due to a webpack bug
// i don't even begin to comprehend, this is apparently
// quite difficult
});
compilers.client.watch({}, (err: Error, stats: any) => {
if (err) {
console.error(chalk.red(err.message));
} else if (stats.hasErrors()) {
// print errors. TODO notify client
stats.toJson().errors.forEach((error: Error) => {
console.error(error); // TODO make this look nice
});
} else {
console.log(`built client in ${Date.now() - times.client_start}ms`); // TODO prettify
const client_info = stats.toJson();
fs.writeFileSync(path.join(dir, 'client_info.json'), JSON.stringify(client_info, null, ' '));
deferreds.client.fulfil();
const client_files = client_info.assets.map((chunk: { name: string }) => `/client/${chunk.name}`);
deferreds.server.promise.then(() => {
hot_update_server.send({
status: 'completed'
});
});
create_serviceworker({
routes: create_routes({ src }),
client_files,
src
});
watch_serviceworker();
}
});
let watch_serviceworker = compilers.serviceworker
? function() {
watch_serviceworker = noop;
compilers.serviceworker.plugin('invalid', (filename: string) => {
times.serviceworker_start = Date.now();
});
compilers.serviceworker.watch({}, (err: Error, stats: any) => {
if (err) {
// TODO notify client
} else if (stats.hasErrors()) {
// print errors. TODO notify client
stats.toJson().errors.forEach((error: Error) => {
console.error(error); // TODO make this look nice
});
} else {
console.log(`built service worker in ${Date.now() - times.serviceworker_start}ms`); // TODO prettify
const serviceworker_info = stats.toJson();
fs.writeFileSync(path.join(dir, 'serviceworker_info.json'), JSON.stringify(serviceworker_info, null, ' '));
}
});
}
: noop;
}
function noop() {}

91
src/cli/export.ts Normal file
View File

@@ -0,0 +1,91 @@
import * as child_process from 'child_process';
import * as path from 'path';
import * as sander from 'sander';
import express from 'express';
import cheerio from 'cheerio';
import URL from 'url-parse';
import fetch from 'node-fetch';
import { wait_for_port } from './utils';
const { OUTPUT_DIR = 'dist' } = process.env;
const app = express();
function read_json(file: string) {
return JSON.parse(sander.readFileSync(file, { encoding: 'utf-8' }));
}
export default async function exporter(dir: string) { // dir === '.sapper'
// Prep output directory
sander.rimrafSync(OUTPUT_DIR);
sander.copydirSync('assets').to(OUTPUT_DIR);
sander.copydirSync(dir, 'client').to(OUTPUT_DIR, 'client');
sander.copyFileSync(dir, 'service-worker.js').to(OUTPUT_DIR, 'service-worker.js');
const port = await require('get-port')(3000);
const origin = `http://localhost:${port}`;
const proc = child_process.fork(path.resolve(`${dir}/server.js`), [], {
cwd: process.cwd(),
env: {
PORT: port,
NODE_ENV: 'production',
SAPPER_EXPORT: 'true'
}
});
const seen = new Set();
const saved = new Set();
proc.on('message', message => {
if (!message.__sapper__) return;
const url = new URL(message.url, origin);
if (saved.has(url.pathname)) return;
saved.add(url.pathname);
if (message.type === 'text/html') {
const dest = `${OUTPUT_DIR}/${url.pathname}/index.html`;
sander.writeFileSync(dest, message.body);
} else {
const dest = `${OUTPUT_DIR}/${url.pathname}`;
sander.writeFileSync(dest, message.body);
}
});
function handle(url: URL) {
if (url.origin !== origin) return;
if (seen.has(url.pathname)) return;
seen.add(url.pathname);
return fetch(url.href)
.then(r => {
if (r.headers.get('Content-Type') === 'text/html') {
return r.text().then((body: string) => {
const $ = cheerio.load(body);
const hrefs: string[] = [];
$('a[href]').each((i: number, $a) => {
hrefs.push($a.attribs.href);
});
return hrefs.reduce((promise, href) => {
return promise.then(() => handle(new URL(href, url.href)));
}, Promise.resolve());
});
}
})
.catch((err: Error) => {
console.error(`Error rendering ${url.pathname}: ${err.message}`);
});
}
wait_for_port(port, () => {
handle(new URL(origin)) // TODO all static routes
.then(() => proc.kill())
});
}

20
src/cli/help.md Normal file
View File

@@ -0,0 +1,20 @@
# sapper v<@version@>
https://sapper.svelte.technology
> sapper dev
Start a development server
> sapper build
Creates a production-ready version of your app
> sapper export
If possible, exports your app as static files, suitable for hosting on
services like Netlify or Surge
> sapper --help
Shows this message

61
src/cli/index.ts Executable file
View File

@@ -0,0 +1,61 @@
import mri from 'mri';
import chalk from 'chalk';
import help from './help.md';
import build from './build';
import exporter from './export';
import dev from './dev';
import upgrade from './upgrade';
import { dest, entry, src } from '../config';
import * as pkg from '../../package.json';
const opts = mri(process.argv.slice(2), {
alias: {
h: 'help'
}
});
if (opts.help) {
const rendered = help
.replace('<@version@>', pkg.version)
.replace(/^(.+)/gm, (m: string, $1: string) => /[#>]/.test(m) ? $1 : ` ${$1}`)
.replace(/^# (.+)/gm, (m: string, $1: string) => chalk.bold.underline($1))
.replace(/^> (.+)/gm, (m: string, $1: string) => chalk.cyan($1));
console.log(`\n${rendered}\n`);
process.exit(0);
}
const [cmd] = opts._;
const start = Date.now();
if (cmd === 'build') {
process.env.NODE_ENV = 'production';
build({ dest, dev: false, entry, src })
.then(() => {
const elapsed = Date.now() - start;
console.error(`built in ${elapsed}ms`); // TODO beautify this, e.g. 'built in 4.7 seconds'
})
.catch(err => {
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
});
} else if (cmd === 'export') {
process.env.NODE_ENV = 'production';
build({ dest, dev: false, entry, src })
.then(() => exporter(dest))
.then(() => {
const elapsed = Date.now() - start;
console.error(`extracted in ${elapsed}ms`); // TODO beautify this, e.g. 'built in 4.7 seconds'
})
.catch(err => {
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
});
} else if (cmd === 'dev') {
dev(src, dest);
} else if (cmd === 'upgrade') {
upgrade();
} else {
console.log(`unrecognized command ${cmd} — try \`sapper --help\` for more information`);
}

53
src/cli/upgrade.ts Normal file
View File

@@ -0,0 +1,53 @@
import * as fs from 'fs';
import chalk from 'chalk';
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(chalk.red(`Could not replace %sapper.main% in ${file}`));
} else {
write(file, template.replace(pattern, `%sapper.scripts%`));
console.log(chalk.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);
}

24
src/cli/utils.ts Normal file
View File

@@ -0,0 +1,24 @@
import waitPort from 'wait-port';
export function wait_for_port(port: number, cb: () => void) {
waitPort({ port }).then(cb);
}
// import * as net from 'net';
// export function wait_for_port(port: number, cb: () => void) {
// const socket = net.createConnection(port, 'localhost', () => {
// cb();
// socket.destroy();
// });
// socket.on('error', err => {
// setTimeout(() => {
// wait_for_port(port, cb);
// }, 100);
// });
// setTimeout(() => {
// socket.destroy();
// }, 100);
// }

12
src/config.ts Normal file
View File

@@ -0,0 +1,12 @@
import * as path from 'path';
export const isDev = () => process.env.NODE_ENV !== 'production';
export const templates = path.resolve(process.env.SAPPER_TEMPLATES || 'templates');
export const src = path.resolve(process.env.SAPPER_ROUTES || 'routes');
export const dest = path.resolve(process.env.SAPPER_DEST || '.sapper');
export const entry = {
client: path.resolve(templates, '.main.rendered.js'),
server: path.resolve(dest, 'server-entry.js')
};

94
src/core/create_app.ts Normal file
View File

@@ -0,0 +1,94 @@
import * as fs from 'fs';
import * as path from 'path';
import mkdirp from 'mkdirp';
import create_routes from './create_routes';
import { fudge_mtime, posixify, write } from './utils';
import { Route } from '../interfaces';
export default function create_app({ routes, src, dev, dev_port }: {
routes: Route[];
src: string;
dev: boolean;
dev_port: number;
}) {
mkdirp.sync('app/manifest');
write('app/manifest/client.js', generate_client(routes, src, dev, dev_port));
write('app/manifest/server.js', generate_server(routes, src));
}
function generate_client(routes: Route[], src: string, dev: boolean, dev_port?: number) {
let code = `
// This file is generated by Sapper — do not edit it!
export const routes = [
${routes
.map(route => {
if (route.type !== 'page') {
return `{ pattern: ${route.pattern}, ignore: true }`;
}
const file = posixify(`../../routes/${route.file}`);
if (route.id === '_4xx' || route.id === '_5xx') {
return `{ error: '${route.id.slice(1)}', load: () => import(/* webpackChunkName: "${route.id}" */ '${file}') }`;
}
const params = route.params.length === 0
? '{}'
: `{ ${route.params.map((part, i) => `${part}: match[${i + 1}]`).join(', ')} }`;
return `{ pattern: ${route.pattern}, params: ${route.params.length > 0 ? `match` : `()`} => (${params}), load: () => import(/* webpackChunkName: "${route.id}" */ '${file}') }`;
})
.join(',\n\t')}
];`.replace(/^\t\t/gm, '').trim();
if (dev) {
const hmr_client = posixify(
path.resolve(__dirname, 'hmr-client.js')
);
code += `
if (module.hot) {
import('${hmr_client}').then(client => {
client.connect(${dev_port});
});
}`.replace(/^\t{3}/gm, '');
}
return code;
}
function generate_server(routes: Route[], src: string) {
let code = `
// This file is generated by Sapper — do not edit it!
${routes
.map(route => {
const file = posixify(`../../routes/${route.file}`);
return route.type === 'page'
? `import ${route.id} from '${file}';`
: `import * as ${route.id} from '${file}';`;
})
.join('\n')}
export const routes = [
${routes
.map(route => {
const file = posixify(`${src}/${route.file}`);
if (route.id === '_4xx' || route.id === '_5xx') {
return `{ error: '${route.id.slice(1)}', module: ${route.id} }`;
}
const params = route.params.length === 0
? '{}'
: `{ ${route.params.map((part, i) => `${part}: match[${i + 1}]`).join(', ')} }`;
return `{ id: '${route.id}', type: '${route.type}', pattern: ${route.pattern}, params: ${route.params.length > 0 ? `match` : `()`} => (${params}), module: ${route.id} }`;
})
.join(',\n\t')
}
];`.replace(/^\t\t/gm, '').trim();
return code;
}

View File

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

129
src/core/create_routes.ts Normal file
View File

@@ -0,0 +1,129 @@
import * as path from 'path';
import glob from 'glob';
import { Route } from '../interfaces';
export default function create_routes({ src, files = glob.sync('**/*.+(html|js|mjs)', { cwd: src }) }: {
src: string;
files?: string[];
}) {
const routes: Route[] = files
.map((file: string) => {
if (/(^|\/|\\)_/.test(file)) return;
if (/]\[/.test(file)) {
throw new Error(`Invalid route ${file} — parameters must be separated`);
}
const base = file.replace(/\.[^/.]+$/, '');
const parts = base.split('/'); // glob output is always posix-style
if (parts[parts.length - 1] === 'index') parts.pop();
const id = (
parts.join('_').replace(/[[\]]/g, '$').replace(/^\d/, '_$&').replace(/[^a-zA-Z0-9_$]/g, '_')
) || '_';
const params: string[] = [];
const param_pattern = /\[([^\]]+)\]/g;
let match;
while (match = param_pattern.exec(base)) {
params.push(match[1]);
}
// TODO can we do all this with sub-parts? or does
// nesting make that impossible?
let pattern_string = '';
let i = parts.length;
let nested = true;
while (i--) {
const part = parts[i];
const dynamic = part[0] === '[';
if (dynamic) {
const matcher = part.replace(param_pattern, `([^\/]+?)`);
pattern_string = nested ? `(?:\\/${matcher}${pattern_string})?` : `\\/${matcher}${pattern_string}`;
} else {
nested = false;
pattern_string = `\\/${part}${pattern_string}`;
}
}
const pattern = new RegExp(`^${pattern_string}\\/?$`);
const test = (url: string) => pattern.test(url);
const exec = (url: string) => {
const match = pattern.exec(url);
if (!match) return;
const result: Record<string, string> = {};
params.forEach((param, i) => {
result[param] = match[i + 1];
});
return result;
};
return {
id,
type: path.extname(file) === '.html' ? 'page' : 'route',
file,
pattern,
test,
exec,
parts,
params
};
})
.filter(Boolean)
.sort((a: Route, b: Route) => {
let same = true;
for (let i = 0; true; i += 1) {
const a_part = a.parts[i];
const b_part = b.parts[i];
if (!a_part && !b_part) {
if (same) throw new Error(`The ${a.file} and ${b.file} routes clash`);
return 0;
}
if (!a_part) return -1;
if (!b_part) return 1;
const a_sub_parts = get_sub_parts(a_part);
const b_sub_parts = get_sub_parts(b_part);
for (let i = 0; true; i += 1) {
const a_sub_part = a_sub_parts[i];
const b_sub_part = b_sub_parts[i];
if (!a_sub_part && !b_sub_part) break;
if (!a_sub_part) return 1; // note this is reversed from above — match [foo].json before [foo]
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;
}
}
}
});
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);
}

View File

@@ -0,0 +1,27 @@
import * as fs from 'fs';
import * as path from 'path';
import glob from 'glob';
import create_routes from './create_routes';
import { fudge_mtime, posixify, write } from './utils';
import { Route } from '../interfaces';
export default function create_serviceworker({ routes, client_files, src }: {
routes: Route[];
client_files: string[];
src: string;
}) {
const assets = glob.sync('**', { cwd: 'assets', nodir: true });
let code = `
// This file is generated by Sapper — do not edit it!
export const timestamp = ${Date.now()};
export const assets = [\n\t${assets.map((x: string) => `"${x}"`).join(',\n\t')}\n];
export const shell = [\n\t${client_files.map((x: string) => `"${x}"`).join(',\n\t')}\n];
export const routes = [\n\t${routes.filter((r: Route) => r.type === 'page' && !/^_[45]xx$/.test(r.id)).map((r: Route) => `{ pattern: ${r.pattern} }`).join(',\n\t')}\n];
`.replace(/^\t\t/gm, '').trim();
write('app/manifest/service-worker.js', code);
}

View File

@@ -0,0 +1,39 @@
import * as fs from 'fs';
import chalk from 'chalk';
import framer from 'code-frame';
import { locate } from 'locate-character';
function error(e: any) {
if (e.title) console.error(chalk.bold.red(e.title));
if (e.body) console.error(chalk.red(e.body));
if (e.url) console.error(chalk.cyan(e.url));
if (e.frame) console.error(chalk.grey(e.frame));
process.exit(1);
}
export default function create_templates() {
const template = fs.readFileSync(`app/template.html`, 'utf-8');
const index = template.indexOf('%sapper.main%');
if (index !== -1) {
// TODO remove this in a future version
const { line, column } = locate(template, index, { offsetLine: 1 });
const frame = framer(template, line, column);
error({
title: `app/template.html`,
body: `<script src='%sapper.main%'> is unsupported — use %sapper.scripts% (without the <script> tag) instead`,
url: 'https://github.com/sveltejs/sapper/issues/86',
frame
});
}
return {
render: (data: Record<string, string>) => {
return template.replace(/%sapper\.(\w+)%/g, (match, key) => {
return key in data ? data[key] : '';
});
}
};
}

5
src/core/index.ts Normal file
View File

@@ -0,0 +1,5 @@
export { default as create_app } from './create_app';
export { default as create_serviceworker } from './create_serviceworker';
export { default as create_compilers } from './create_compilers';
export { default as create_routes } from './create_routes';
export { default as create_template } from './create_template';

20
src/core/utils.ts Normal file
View File

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

15
src/interfaces.ts Normal file
View File

@@ -0,0 +1,15 @@
export type Route = {
id: string;
type: 'page' | 'route';
file: string;
pattern: RegExp;
test: (url: string) => boolean;
exec: (url: string) => Record<string, string>;
parts: string[];
params: string[];
};
export type Template = {
render: (data: Record<string, string>) => string;
stream: (req, res, data: Record<string, string | Promise<string>>) => void;
};

362
src/middleware/index.ts Normal file
View File

@@ -0,0 +1,362 @@
import * as fs from 'fs';
import * as path from 'path';
import { ClientRequest, ServerResponse } from 'http';
// import * as mime from 'mime';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import serialize from 'serialize-javascript';
import escape_html from 'escape-html';
import { create_routes, templates, create_compilers, create_template } from 'sapper/core.js';
import { dest, entry, isDev, src } from '../config';
import { Route, Template } from '../interfaces';
import sourceMapSupport from 'source-map-support';
sourceMapSupport.install();
const dev = isDev();
type RouteObject = {
id: string;
type: 'page' | 'route';
pattern: RegExp;
params: (match: RegExpMatchArray) => Record<string, string>;
module: {
render: (data: any) => {
head: string;
css: { code: string, map: any };
html: string
},
preload: (data: any) => any | Promise<any>
};
error?: string;
}
type Handler = (req: Req, res: ServerResponse, next: () => void) => void;
interface Req extends ClientRequest {
url: string;
method: string;
pathname: string;
params: Record<string, string>;
}
export default function middleware({ routes }: {
routes: RouteObject[]
}) {
const client_info = JSON.parse(fs.readFileSync(path.join(dest, 'client_info.json'), 'utf-8'));
const template = create_template();
const shell = try_read(path.join(dest, 'index.html'));
const serviceworker = try_read(path.join(dest, 'service-worker.js'));
const middleware = compose_handlers([
(req: Req, res: ServerResponse, next: () => void) => {
req.pathname = req.url.replace(/\?.*/, '');
next();
},
shell && get_asset_handler({
pathname: '/index.html',
type: 'text/html',
cache: 'max-age=600',
body: shell
}),
serviceworker && get_asset_handler({
pathname: '/service-worker.js',
type: 'application/javascript',
cache: 'max-age=600',
body: serviceworker
}),
(req: Req, res: ServerResponse, next: () => void) => {
if (req.pathname.startsWith('/client/')) {
// const type = mime.getType(req.pathname);
const type = 'application/javascript'; // TODO might not be, if using e.g. CSS plugin
// TODO cache?
const rs = fs.createReadStream(path.join(dest, req.pathname.slice(1)));
rs.on('error', error => {
res.statusCode = 404;
res.end('not found');
});
res.setHeader('Content-Type', type);
res.setHeader('Cache-Control', 'max-age=31536000');
rs.pipe(res);
} else {
next();
}
},
get_route_handler(client_info.assetsByChunkName, routes, template)
].filter(Boolean));
return middleware;
}
function get_asset_handler({ pathname, type, cache, body }: {
pathname: string;
type: string;
cache: string;
body: string;
}) {
return (req: Req, res: ServerResponse, next: () => void) => {
if (req.pathname !== pathname) return next();
res.setHeader('Content-Type', type);
res.setHeader('Cache-Control', cache);
res.end(body);
};
}
const resolved = Promise.resolve();
function get_route_handler(chunks: Record<string, string>, routes: RouteObject[], template: Template) {
function handle_route(route: RouteObject, req: Req, res: ServerResponse) {
req.params = route.params(route.pattern.exec(req.pathname));
const mod = route.module;
if (route.type === 'page') {
res.setHeader('Content-Type', 'text/html');
// preload main.js and current route
// TODO detect other stuff we can preload? images, CSS, fonts?
const link = []
.concat(chunks.main, chunks[route.id])
.map(file => `</client/${file}>;rel="preload";as="script"`)
.join(', ');
res.setHeader('Link', link);
const data = { params: req.params, query: req.query };
let redirect: { statusCode: number, location: string };
let error: { statusCode: number, message: Error | string };
Promise.resolve(
mod.preload ? mod.preload.call({
redirect: (statusCode: number, location: string) => {
redirect = { statusCode, location };
},
error: (statusCode: number, message: Error | string) => {
error = { statusCode, message };
}
}, req) : {}
).catch(err => {
error = { statusCode: 500, message: err };
}).then(preloaded => {
if (redirect) {
res.statusCode = redirect.statusCode;
res.setHeader('Location', redirect.location);
res.end();
return;
}
if (error) {
handle_error(req, res, error.statusCode, error.message);
return;
}
const serialized = try_serialize(preloaded); // TODO bail on non-POJOs
Object.assign(data, preloaded);
const { html, head, css } = mod.render(data);
let scripts = []
.concat(chunks.main) // chunks main might be an array. it might not! thanks, webpack
.map(file => `<script src='/client/${file}'></script>`)
.join('');
scripts = `<script>__SAPPER__ = { preloaded: ${serialized} };</script>${scripts}`;
const page = template.render({
scripts,
html,
head: `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`,
styles: (css && css.code ? `<style>${css.code}</style>` : '')
});
res.end(page);
if (process.send) {
process.send({
__sapper__: true,
url: req.url,
method: req.method,
status: 200,
type: 'text/html',
body: page
});
}
});
}
else {
const method = req.method.toLowerCase();
// 'delete' cannot be exported from a module because it is a keyword,
// so check for 'del' instead
const method_export = method === 'delete' ? 'del' : method;
const handler = mod[method_export];
if (handler) {
if (process.env.SAPPER_EXPORT) {
const { write, end, setHeader } = res;
const chunks: any[] = [];
const headers: Record<string, string> = {};
// intercept data so that it can be exported
res.write = function(chunk: any) {
chunks.push(new Buffer(chunk));
write.apply(res, arguments);
};
res.setHeader = function(name: string, value: string) {
headers[name.toLowerCase()] = value;
setHeader.apply(res, arguments);
};
res.end = function(chunk?: any) {
if (chunk) chunks.push(new Buffer(chunk));
end.apply(res, arguments);
process.send({
__sapper__: true,
url: req.url,
method: req.method,
status: res.statusCode,
type: headers['content-type'],
body: Buffer.concat(chunks).toString()
});
};
}
const handle_error = (err?: Error) => {
if (err) {
console.error(err.stack);
res.statusCode = 500;
res.end(err.message);
} else {
handle_not_found(req, res, 404, 'Not found');
}
};
try {
handler(req, res, handle_error);
} catch (err) {
handle_error(err);
}
} else {
// no matching handler for method — 404
handle_not_found(req, res, 404, 'Not found');
}
}
}
const not_found_route = routes.find((route: RouteObject) => route.error === '4xx');
function handle_not_found(req: Req, res: ServerResponse, statusCode: number, message: Error | string) {
res.statusCode = statusCode;
res.setHeader('Content-Type', 'text/html');
const error = message instanceof Error ? message : new Error(message);
const rendered = not_found_route ? not_found_route.module.render({
status: 404,
error
}) : { head: '', css: null, html: error.message };
const { head, css, html } = rendered;
res.end(template.render({
scripts: `<script src='/client/${chunks.main}'></script>`,
html,
head: `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`,
styles: (css && css.code ? `<style>${css.code}</style>` : '')
}));
}
const error_route = routes.find((route: RouteObject) => route.error === '5xx');
function handle_error(req: Req, res: ServerResponse, statusCode: number, message: Error | string) {
if (statusCode >= 400 && statusCode < 500) {
return handle_not_found(req, res, statusCode, message);
}
res.statusCode = statusCode;
res.setHeader('Content-Type', 'text/html');
const error = message instanceof Error ? message : new Error(message);
const rendered = error_route ? error_route.module.render({
status: 500,
error
}) : { head: '', css: null, html: `Internal server error: ${error.message}` };
const { head, css, html } = rendered;
res.end(template.render({
scripts: `<script src='/client/${chunks.main}'></script>`,
html,
head: `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`,
styles: (css && css.code ? `<style>${css.code}</style>` : '')
}));
}
return function find_route(req: Req, res: ServerResponse) {
const url = req.pathname;
try {
for (const route of routes) {
if (!route.error && route.pattern.test(url)) return handle_route(route, req, res);
}
handle_not_found(req, res, 404, 'Not found');
} catch (error) {
handle_error(req, res, 500, error);
}
};
}
function compose_handlers(handlers: Handler[]) {
return (req: Req, res: ServerResponse, next: () => void) => {
let i = 0;
function go() {
const handler = handlers[i];
if (handler) {
handler(req, res, () => {
i += 1;
go();
});
} else {
next();
}
}
go();
};
}
function read_json(file: string) {
return JSON.parse(fs.readFileSync(file, 'utf-8'));
}
function try_serialize(data: any) {
try {
return serialize(data);
} catch (err) {
return null;
}
}
function try_read(file: string) {
try {
return fs.readFileSync(file, 'utf-8');
} catch (err) {
return null;
}
}

270
src/runtime/index.ts Normal file
View File

@@ -0,0 +1,270 @@
import { detach, findAnchor, scroll_state, which } from './utils';
import { Component, ComponentConstructor, Params, Query, Route, RouteData, ScrollPosition, Target } from './interfaces';
export let component: Component;
let target: Node;
let routes: Route[];
let errors: { '4xx': Route, '5xx': Route };
const history = typeof window !== 'undefined' ? window.history : {
pushState: (state: any, title: string, href: string) => {},
replaceState: (state: any, title: string, href: string) => {},
scrollRestoration: ''
};
const scroll_history: Record<string, ScrollPosition> = {};
let uid = 1;
let cid: number;
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
function select_route(url: URL): Target {
if (url.origin !== window.location.origin) return null;
for (const route of routes) {
const match = route.pattern.exec(url.pathname);
if (match) {
if (route.ignore) return null;
const params = route.params(match);
const query: Record<string, string | true> = {};
if (url.search.length > 0) {
url.search.slice(1).split('&').forEach(searchParam => {
const [, key, value] = /([^=]+)=(.*)/.exec(searchParam);
query[key] = value || true;
})
}
return { url, route, data: { params, query } };
}
}
}
let current_token: {};
function render(Component: ComponentConstructor, data: any, scroll: ScrollPosition, token: {}) {
if (current_token !== token) return;
if (component) {
component.destroy();
} else {
// first load — remove SSR'd <head> contents
const start = document.querySelector('#sapper-head-start');
const end = document.querySelector('#sapper-head-end');
if (start && end) {
while (start.nextSibling !== end) detach(start.nextSibling);
detach(start);
detach(end);
}
// preload additional routes
routes.reduce((promise: Promise<any>, route) => promise.then(route.load), Promise.resolve());
}
component = new Component({
target,
data,
hydrate: !component
});
if (scroll) {
window.scrollTo(scroll.x, scroll.y);
}
}
function prepare_route(Component: ComponentConstructor, data: RouteData) {
let redirect: { statusCode: number, location: string } = null;
let error: { statusCode: number, message: Error | string } = null;
if (!Component.preload) {
return { Component, data, redirect, error };
}
if (!component && window.__SAPPER__ && window.__SAPPER__.preloaded) {
return { Component, data: Object.assign(data, window.__SAPPER__.preloaded), redirect, error };
}
return Promise.resolve(Component.preload.call({
redirect: (statusCode: number, location: string) => {
redirect = { statusCode, location };
},
error: (statusCode: number, message: Error | string) => {
error = { statusCode, message };
}
}, data)).catch(err => {
error = { statusCode: 500, message: err };
}).then(preloaded => {
if (error) {
const route = error.statusCode >= 400 && error.statusCode < 500
? errors['4xx']
: errors['5xx'];
return route.load().then(({ default: Component }: { default: ComponentConstructor }) => {
const err = error.message instanceof Error ? error.message : new Error(error.message);
Object.assign(data, { status: error.statusCode, error: err });
return { Component, data, redirect: null };
});
}
Object.assign(data, preloaded)
return { Component, data, redirect };
});
}
function navigate(target: Target, id: number) {
if (id) {
// popstate or initial navigation
cid = id;
} else {
// clicked on a link. preserve scroll state
scroll_history[cid] = scroll_state();
id = cid = ++uid;
scroll_history[cid] = { x: 0, y: 0 };
}
cid = id;
const loaded = prefetching && prefetching.href === target.url.href ?
prefetching.promise :
target.route.load().then(mod => prepare_route(mod.default, target.data));
prefetching = null;
const token = current_token = {};
return loaded.then(({ Component, data, redirect }) => {
if (redirect) {
return goto(redirect.location, { replaceState: true });
}
render(Component, data, scroll_history[id], token);
});
}
function handle_click(event: MouseEvent) {
// Adapted from https://github.com/visionmedia/page.js
// MIT license https://github.com/visionmedia/page.js#license
if (which(event) !== 1) return;
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
if (event.defaultPrevented) return;
const a: HTMLAnchorElement | SVGAElement = <HTMLAnchorElement | SVGAElement>findAnchor(<Node>event.target);
if (!a) return;
// check if link is inside an svg
// in this case, both href and target are always inside an object
const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
const href = String(svg ? (<SVGAElement>a).href.baseVal : a.href);
if (href === window.location.href) {
event.preventDefault();
return;
}
// Ignore if tag has
// 1. 'download' attribute
// 2. rel='external' attribute
if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
// Ignore if <a> has a target
if (svg ? (<SVGAElement>a).target.baseVal : a.target) return;
const url = new URL(href);
// Don't handle hash changes
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
const target = select_route(url);
if (target) {
navigate(target, null);
event.preventDefault();
history.pushState({ id: cid }, '', url.href);
}
}
function handle_popstate(event: PopStateEvent) {
scroll_history[cid] = scroll_state();
if (event.state) {
const url = new URL(window.location.href);
const target = select_route(url);
navigate(target, event.state.id);
} else {
// hashchange
cid = ++uid;
history.replaceState({ id: cid }, '', window.location.href);
}
}
let prefetching: {
href: string;
promise: Promise<{ Component: ComponentConstructor, data: any }>;
} = null;
export function prefetch(href: string) {
const selected = select_route(new URL(href));
if (selected) {
prefetching = {
href,
promise: selected.route.load().then(mod => prepare_route(mod.default, selected.data))
};
}
}
function handle_touchstart_mouseover(event: MouseEvent | TouchEvent) {
const a: HTMLAnchorElement = <HTMLAnchorElement>findAnchor(<Node>event.target);
if (!a || a.rel !== 'prefetch') return;
prefetch(a.href);
}
let inited: boolean;
export function init(_target: Node, _routes: Route[]) {
target = _target;
routes = _routes.filter(r => !r.error);
errors = {
'4xx': _routes.find(r => r.error === '4xx'),
'5xx': _routes.find(r => r.error === '5xx')
};
if (!inited) { // this check makes HMR possible
window.addEventListener('click', handle_click);
window.addEventListener('popstate', handle_popstate);
// prefetch
window.addEventListener('touchstart', handle_touchstart_mouseover);
window.addEventListener('mouseover', handle_touchstart_mouseover);
inited = true;
}
return Promise.resolve().then(() => {
const { hash, href } = window.location;
const deep_linked = hash && document.getElementById(hash.slice(1));
scroll_history[uid] = deep_linked ?
{ x: 0, y: deep_linked.getBoundingClientRect().top } :
scroll_state();
history.replaceState({ id: uid }, '', href);
const target = select_route(new URL(window.location.href));
return navigate(target, uid);
});
}
export function goto(href: string, opts = { replaceState: false }) {
const target = select_route(new URL(href, window.location.href));
if (target) {
navigate(target, null);
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
} else {
window.location.href = href;
}
}

30
src/runtime/interfaces.ts Normal file
View File

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

19
src/runtime/utils.ts Normal file
View File

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

44
src/webpack/index.ts Normal file
View File

@@ -0,0 +1,44 @@
import { dest, isDev, entry } from '../config';
export default {
dev: isDev(),
client: {
entry: () => {
return {
main: [
'./app/client.js',
// workaround for https://github.com/webpack-contrib/extract-text-webpack-plugin/issues/456
'style-loader/lib/addStyles',
'css-loader/lib/css-base'
]
};
},
output: () => {
return {
path: `${dest}/client`,
filename: '[hash]/[name].js',
chunkFilename: '[hash]/[name].[id].js',
publicPath: '/client/'
};
}
},
server: {
entry: () => {
return {
server: './app/server.js'
};
},
output: () => {
return {
path: `${dest}`,
filename: '[name].js',
chunkFilename: '[hash]/[name].[id].js',
libraryTarget: 'commonjs2'
};
}
}
};

View File

@@ -1,5 +0,0 @@
import app from '__app__';
app.init(document.querySelector('__selector__'), [
__routes__
]);

7
test/app/.gitignore vendored Normal file
View File

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

81
test/app/README.md Normal file
View File

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

6
test/app/app/client.js Normal file
View File

@@ -0,0 +1,6 @@
import { init } from '../../../runtime.js';
import { routes } from './manifest/client.js';
window.init = () => {
return init(document.querySelector('#sapper'), routes);
};

78
test/app/app/server.js Normal file
View File

@@ -0,0 +1,78 @@
import fs from 'fs';
import express from 'express';
import compression from 'compression';
import serve from 'serve-static';
import sapper from '../../../middleware';
import { routes } from './manifest/server.js';
let count;
let ended;
process.on('message', message => {
if (message.action === 'start') {
count = 0;
ended = false;
process.send({ type: 'ready' });
}
if (message.action === 'end') {
ended = true;
if (count === 0) process.send({ type: 'done' });
}
});
const app = express();
app.use((req, res, next) => {
count += 1;
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);
count -= 1;
process.send({
method: req.method,
url: req.url,
status: res.statusCode,
headers: res._headers,
body: Buffer.concat(chunks).toString()
});
if (count === 0 && ended) {
process.send({ type: 'done' });
}
};
next();
});
const { PORT = 3000 } = process.env;
// this allows us to do e.g. `fetch('/api/blog')` on the server
const fetch = require('node-fetch');
global.fetch = (url, opts) => {
if (url[0] === '/') url = `http://localhost:${PORT}${url}`;
return fetch(url, opts);
};
app.use(compression({ threshold: 0 }));
app.use(serve('assets'));
app.use(sapper({
routes
}));
app.listen(PORT, () => {
console.log(`listening on port ${PORT}`);
});

View File

@@ -0,0 +1,75 @@
import { assets, shell, timestamp, routes } from './manifest/service-worker.js';
const ASSETS = `cachetimestamp`;
// `shell` is an array of all the files generated by webpack,
// `assets` is an array of everything in the `assets` directory
const to_cache = shell.concat(assets);
const cached = new Set(to_cache);
self.addEventListener('install', event => {
event.waitUntil(
caches
.open(ASSETS)
.then(cache => cache.addAll(to_cache))
.then(() => {
self.skipWaiting();
})
);
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(async keys => {
// delete old caches
for (const key of keys) {
if (key !== ASSETS) await caches.delete(key);
}
await self.clients.claim();
})
);
});
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// don't try to handle e.g. data: URIs
if (!url.protocol.startsWith('http')) return;
// always serve assets and webpack-generated files from cache
if (cached.has(url.pathname)) {
event.respondWith(caches.match(event.request));
return;
}
// for pages, you might want to serve a shell `index.html` file,
// which Sapper has generated for you. It's not right for every
// app, but if it's right for yours then uncomment this section
/*
if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
event.respondWith(caches.match('/index.html'));
return;
}
*/
// for everything else, try the network first, falling back to
// cache if the user is offline. (If the pages never change, you
// might prefer a cache-first approach to a network-first one.)
event.respondWith(
caches
.open('offline')
.then(async cache => {
try {
const response = await fetch(event.request);
cache.put(event.request, response.clone());
return response;
} catch(err) {
const response = await cache.match(event.request);
if (response) return response;
throw err;
}
})
);
});

View File

@@ -0,0 +1,37 @@
<!doctype>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width'>
<meta name='theme-color' content='#aa1e1e'>
<link rel='stylesheet' href='/global.css'>
<link rel='manifest' href='/manifest.json'>
<link rel='icon' type='image/png' href='/favicon.png'>
<script>
// if ('serviceWorker' in navigator) {
// navigator.serviceWorker.register('/service-worker.js');
// }
</script>
<!-- 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>

BIN
test/app/assets/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,45 @@
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.

After

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -0,0 +1,20 @@
{
"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.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

File diff suppressed because it is too large Load Diff

28
test/app/package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "TODO",
"description": "TODO",
"version": "0.0.1",
"scripts": {
"dev": "node server.js",
"build": "sapper build",
"start": "cross-env NODE_ENV=production node server.js",
"prestart": "npm run build"
},
"dependencies": {
"compression": "^1.7.1",
"cross-env": "^5.1.1",
"css-loader": "^0.28.7",
"express": "^4.16.2",
"extract-text-webpack-plugin": "^3.0.2",
"glob": "^7.1.2",
"marked": "^0.3.9",
"node-fetch": "^1.7.3",
"npm-run-all": "^4.1.2",
"serve-static": "^1.13.1",
"style-loader": "^0.19.0",
"svelte": "^1.49.1",
"svelte-loader": "^2.2.1",
"uglifyjs-webpack-plugin": "^1.1.2",
"webpack": "^3.10.0"
}
}

18
test/app/routes/4xx.html Normal file
View File

@@ -0,0 +1,18 @@
<:Head>
<title>{{status}}</title>
</:Head>
<Layout page='home'>
<h1>Not found</h1>
<p>{{error.message}}</p>
</Layout>
<script>
import Layout from './_components/Layout.html';
export default {
components: {
Layout
}
};
</script>

18
test/app/routes/5xx.html Normal file
View File

@@ -0,0 +1,18 @@
<:Head>
<title>Internal server error</title>
</:Head>
<Layout page='home'>
<h1>Internal server error</h1>
<p>{{error.message}}</p>
</Layout>
<script>
import Layout from './_components/Layout.html';
export default {
components: {
Layout
}
};
</script>

View File

@@ -0,0 +1,15 @@
<Nav page={{page}}/>
<main>
<slot></slot>
</main>
<script>
import Nav from './Nav.html';
export default {
components: {
Nav
}
};
</script>

View File

@@ -0,0 +1,57 @@
<nav>
<ul>
<li><a href='/'>home</a></li>
<li><a href='/about'>about</a></li>
<li><a href='/slow-preload'>slow preload</a></li>
<li><a href='/redirect-from'>redirect</a></li>
<li><a href='/blog/nope'>broken link</a></li>
<li><a href='/blog/throw-an-error'>error link</a></li>
<li><a rel=prefetch class='{{page === "blog" ? "selected" : ""}}' href='/blog'>blog</a></li>
</ul>
</nav>
<style>
nav {
border-bottom: 1px solid rgba(170,30,30,0.1);
font-weight: 300;
padding: 0 1em;
}
ul {
margin: 0;
padding: 0;
}
/* clearfix */
ul::after {
content: '';
display: block;
clear: both;
}
li {
display: block;
float: left;
}
.selected {
position: relative;
display: inline-block;
}
.selected::after {
position: absolute;
content: '';
width: calc(100% - 1em);
height: 2px;
background-color: rgb(170,30,30);
display: block;
bottom: -1px;
}
a {
text-decoration: none;
padding: 1em 0.5em;
display: block;
}
</style>

View File

@@ -0,0 +1,28 @@
<:Head>
<title>About</title>
</:Head>
<Layout page='about'>
<h1>About this site</h1>
<p>This is the 'about' page. There's not much here.</p>
<button class='goto' on:click='goto("/blog/what-is-sapper")'>What is Sapper?</button>
<button class='prefetch' on:click='goto("/blog/why-the-name")'>Why the name?</button>
</Layout>
<script>
import Layout from './_components/Layout.html';
import { goto, prefetch } from '../../../runtime.js';
export default {
components: {
Layout
},
methods: {
goto,
prefetch
}
};
</script>

View File

@@ -0,0 +1,9 @@
export function del(req, res) {
res.set({
'Content-Type': 'application/json'
});
res.end(JSON.stringify({
id: req.params.id
}));
}

View File

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

View File

@@ -0,0 +1,80 @@
<:Head>
<title>{{post.title}}</title>
</:Head>
<Layout page='blog'>
<h1>{{post.title}}</h1>
<div class='content'>
{{{post.html}}}
</div>
</Layout>
<style>
/*
By default, CSS is locally scoped to the component,
and any unused styles are dead-code-eliminated.
In this page, Svelte can't know which elements are
going to appear inside the {{{post.html}}} block,
so we have to use the :global(...) modifier to target
all elements inside .content
*/
.content :global(h2) {
font-size: 1.4em;
font-weight: 500;
}
.content :global(pre) {
background-color: #f9f9f9;
box-shadow: inset 1px 1px 5px rgba(0,0,0,0.05);
padding: 0.5em;
border-radius: 2px;
overflow-x: auto;
}
.content :global(pre) :global(code) {
background-color: transparent;
padding: 0;
}
.content :global(ul) {
line-height: 1.5;
}
.content :global(li) {
margin: 0 0 0.5em 0;
}
</style>
<script>
import Layout from '../_components/Layout.html';
export default {
components: {
Layout
},
preload({ params, query }) {
// the `slug` parameter is available because this file
// is called [slug].html
const { slug } = params;
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

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

View File

@@ -0,0 +1,116 @@
// 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

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

View File

@@ -0,0 +1,15 @@
<button class='del' on:click='del()'>delete</button>
<script>
export default {
methods: {
del() {
fetch(`/api/delete/42`, { method: 'DELETE' })
.then(r => r.json())
.then(data => {
window.deleted = data;
});
}
}
};
</script>

View File

@@ -0,0 +1,60 @@
<:Head>
<title>Sapper project template</title>
</:Head>
<Layout page='home'>
<h1>Great success!</h1>
<figure>
<img alt='borat' src='/great-success.png'>
<figcaption>HIGH FIVE!</figcaption>
</figure>
<p><strong>Try editing this file (routes/index.html) to test hot module reloading.</strong></p>
</Layout>
<div class='hydrate-test'></div>
<style>
h1, figure, p {
text-align: center;
margin: 0 auto;
}
h1 {
font-size: 2.8em;
text-transform: uppercase;
font-weight: 700;
margin: 0 0 0.5em 0;
}
figure {
margin: 0 0 1em 0;
}
img {
width: 100%;
max-width: 400px;
margin: 0 0 1em 0;
}
p {
margin: 1em auto;
}
@media (min-width: 480px) {
h1 {
font-size: 4em;
}
}
</style>
<script>
import Layout from './_components/Layout.html';
export default {
components: {
Layout
}
};
</script>

View File

@@ -0,0 +1,7 @@
<script>
export default {
preload() {
this.redirect(301, '/redirect-to');
}
};
</script>

View File

@@ -0,0 +1 @@
<h1>redirected</h1>

View File

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

View File

@@ -0,0 +1,15 @@
<h1>This page should never render</h1>
<script>
export default {
preload() {
return new Promise(fulfil => {
if (typeof window !== 'undefined') {
window.fulfil = fulfil;
} else {
fulfil({});
}
});
}
};
</script>

View File

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

View File

@@ -0,0 +1,39 @@
const config = require('../../../webpack/config.js');
const webpack = require('webpack');
module.exports = {
entry: './app/client.js',
output: config.client.output(),
resolve: {
extensions: ['.js', '.html']
},
module: {
rules: [
{
test: /\.html$/,
exclude: /node_modules/,
use: {
loader: 'svelte-loader',
options: {
hydratable: true,
emitCss: !config.dev,
cascade: false,
store: true
}
}
},
{
test: /\.css$/,
use: [
{ loader: "style-loader" },
{ loader: "css-loader" }
]
}
].filter(Boolean)
},
plugins: [
config.dev && new webpack.HotModuleReplacementPlugin(),
!config.dev && new webpack.optimize.ModuleConcatenationPlugin()
].filter(Boolean),
devtool: config.dev ? 'inline-source-map' : false
};

View File

@@ -0,0 +1,32 @@
const config = require('../../../webpack/config.js');
const pkg = require('../package.json');
const sapper_pkg = require('../../../package.json');
module.exports = {
entry: {
'server': './app/server.js'
},
output: config.server.output(),
target: 'node',
resolve: {
extensions: ['.js', '.html']
},
externals: Object.keys(pkg.dependencies).concat(Object.keys(sapper_pkg.dependencies)),
module: {
rules: [
{
test: /\.html$/,
exclude: /node_modules/,
use: {
loader: 'svelte-loader',
options: {
css: false,
cascade: false,
store: true,
generate: 'ssr'
}
}
}
]
}
};

View File

@@ -0,0 +1,17 @@
const path = require('path');
const config = require('../../../webpack/config.js');
const webpack = require('webpack');
module.exports = {
entry: {
'service-worker': './app/service-worker.js'
},
output: {
path: path.resolve(`.sapper`),
filename: '[name].js',
chunkFilename: '[name].[id].[hash].js'
},
plugins: [
!config.dev && new webpack.optimize.ModuleConcatenationPlugin()
].filter(Boolean)
};

508
test/common/test.js Normal file
View File

@@ -0,0 +1,508 @@
const path = require('path');
const assert = require('assert');
const Nightmare = require('nightmare');
const express = require('express');
const serve = require('serve-static');
const walkSync = require('walk-sync');
const fetch = require('node-fetch');
run('production');
run('development');
Nightmare.action('page', {
title(done) {
this.evaluate_now(() => document.querySelector('h1').textContent, done);
},
text(done) {
this.evaluate_now(() => document.body.textContent, done);
}
});
Nightmare.action('init', function(done) {
this.evaluate_now(() => window.init(), done);
});
function run(env) {
describe(`env=${env}`, function () {
this.timeout(20000);
let PORT;
let proc;
let nightmare;
let capture;
let base;
before(() => {
process.chdir(path.resolve(__dirname, '../app'));
let exec_promise = Promise.resolve();
if (env === 'production') {
const cli = path.resolve(__dirname, '../../cli.js');
exec_promise = exec(`node ${cli} export`);
}
return exec_promise.then(() => {
const resolved = require.resolve('../../middleware.js');
delete require.cache[resolved];
delete require.cache[require.resolve('../../core.js')]; // TODO remove this
return require('get-port')();
}).then(port => {
base = `http://localhost:${port}`;
proc = require('child_process').fork('.sapper/server.js', {
cwd: process.cwd(),
env: {
NODE_ENV: env,
PORT: port
}
});
let handler;
proc.on('message', message => {
if (message.__sapper__) return;
if (handler) handler(message);
});
capture = fn => {
return new Promise((fulfil, reject) => {
const captured = [];
let start = Date.now();
handler = message => {
if (message.type === 'ready') {
fn().then(() => {
proc.send({
action: 'end'
});
}, reject);
}
else if (message.type === 'done') {
fulfil(captured);
handler = null;
}
else {
captured.push(message);
}
};
proc.send({
action: 'start'
});
});
};
});
});
after(() => {
proc.kill();
// give a chance to clean up
return new Promise(fulfil => setTimeout(fulfil, 500));
});
beforeEach(() => {
nightmare = new Nightmare();
nightmare.on('console', (type, ...args) => {
console[type](...args);
});
nightmare.on('page', (type, ...args) => {
if (type === 'error') {
console.error(args[1]);
} else {
console.warn(type, args);
}
});
});
afterEach(() => {
return nightmare.end();
});
describe('basic functionality', () => {
it('serves /', () => {
return nightmare.goto(base).page.title().then(title => {
assert.equal(title, 'Great success!');
});
});
it('serves /?', () => {
return nightmare.goto(`${base}?`).page.title().then(title => {
assert.equal(title, 'Great success!');
});
});
it('serves static route', () => {
return nightmare.goto(`${base}/about`).page.title().then(title => {
assert.equal(title, 'About this site');
});
});
it('serves dynamic route', () => {
return nightmare.goto(`${base}/blog/what-is-sapper`).page.title().then(title => {
assert.equal(title, 'What is Sapper?');
});
});
it('navigates to a new page without reloading', () => {
return capture(() => nightmare.goto(base).init().wait(400))
.then(() => {
return capture(() => nightmare.click('a[href="/about"]'));
})
.then(requests => {
assert.deepEqual(requests.map(r => r.url), []);
return nightmare.path();
})
.then(path => {
assert.equal(path, '/about');
return nightmare.title();
})
.then(title => {
assert.equal(title, 'About');
});
});
it('navigates programmatically', () => {
return nightmare
.goto(`${base}/about`)
.init()
.click('.goto')
.wait(() => window.location.pathname === '/blog/what-is-sapper')
.wait(100)
.title()
.then(title => {
assert.equal(title, 'What is Sapper?');
});
});
it('prefetches programmatically', () => {
return nightmare
.goto(`${base}/about`)
.init()
.wait(200)
.then(() => {
return capture(() => {
return nightmare
.click('.prefetch')
.wait(200);
});
})
.then(requests => {
assert.ok(!!requests.find(r => r.url === '/blog/why-the-name.json'));
});
});
it('scrolls to active deeplink', () => {
return nightmare
.goto(`${base}/blog/a-very-long-post#four`)
.init()
.evaluate(() => window.scrollY)
.then(scrollY => {
assert.ok(scrollY > 0, scrollY);
});
});
it('reuses prefetch promise', () => {
return nightmare
.goto(`${base}/blog`)
.init().wait(300)
.then(() => {
return capture(() => {
return nightmare
.mouseover('[href="/blog/what-is-sapper"]')
.wait(200);
});
})
.then(mouseover_requests => {
assert.ok(mouseover_requests.findIndex(r => r.url === '/blog/what-is-sapper.json') !== -1);
return capture(() => {
return nightmare
.click('[href="/blog/what-is-sapper"]')
.wait(200);
});
})
.then(click_requests => {
assert.ok(click_requests.findIndex(r => r.url === '/blog/what-is-sapper.json') === -1);
});
});
it('cancels navigation if subsequent navigation occurs during preload', () => {
return nightmare
.goto(base)
.init()
.click('a[href="/slow-preload"]')
.wait(100)
.click('a[href="/about"]')
.wait(100)
.then(() => nightmare.path())
.then(path => {
assert.equal(path, '/about');
return nightmare.title();
})
.then(title => {
assert.equal(title, 'About');
return nightmare.evaluate(() => window.fulfil({})).wait(100);
})
.then(() => nightmare.path())
.then(path => {
assert.equal(path, '/about');
return nightmare.title();
})
.then(title => {
assert.equal(title, 'About');
});
});
it('passes entire request object to preload', () => {
return nightmare
.goto(`${base}/show-url`)
.init()
.evaluate(() => document.querySelector('p').innerHTML)
.end().then(html => {
assert.equal(html, `URL is /show-url`);
});
});
it('calls a delete handler', () => {
return nightmare
.goto(`${base}/delete-test`)
.init()
.click('.del')
.wait(() => window.deleted)
.evaluate(() => window.deleted.id)
.then(id => {
assert.equal(id, 42);
});
});
it('hydrates initial route', () => {
return nightmare.goto(base)
.wait('.hydrate-test')
.evaluate(() => {
window.el = document.querySelector('.hydrate-test');
})
.init()
.evaluate(() => {
return document.querySelector('.hydrate-test') === window.el;
})
.then(matches => {
assert.ok(matches);
});
});
it('redirects on server', () => {
return nightmare.goto(`${base}/redirect-from`)
.path()
.then(path => {
assert.equal(path, '/redirect-to');
})
.then(() => nightmare.page.title())
.then(title => {
assert.equal(title, 'redirected');
});
});
it('redirects in client', () => {
return nightmare.goto(base)
.wait('[href="/redirect-from"]')
.click('[href="/redirect-from"]')
.wait(200)
.path()
.then(path => {
assert.equal(path, '/redirect-to');
})
.then(() => nightmare.page.title())
.then(title => {
assert.equal(title, 'redirected');
});
});
it('handles 4xx error on server', () => {
return nightmare.goto(`${base}/blog/nope`)
.path()
.then(path => {
assert.equal(path, '/blog/nope');
})
.then(() => nightmare.page.title())
.then(title => {
assert.equal(title, 'Not found')
});
});
it('handles 4xx error in client', () => {
return nightmare.goto(base)
.init()
.click('[href="/blog/nope"]')
.wait(200)
.path()
.then(path => {
assert.equal(path, '/blog/nope');
})
.then(() => nightmare.page.title())
.then(title => {
assert.equal(title, 'Not found');
});
});
it('handles non-4xx error on server', () => {
return nightmare.goto(`${base}/blog/throw-an-error`)
.path()
.then(path => {
assert.equal(path, '/blog/throw-an-error');
})
.then(() => nightmare.page.title())
.then(title => {
assert.equal(title, 'Internal server error')
});
});
it('handles non-4xx error in client', () => {
return nightmare.goto(base)
.init()
.click('[href="/blog/throw-an-error"]')
.wait(200)
.path()
.then(path => {
assert.equal(path, '/blog/throw-an-error');
})
.then(() => nightmare.page.title())
.then(title => {
assert.equal(title, 'Internal server error');
});
});
it('does not attempt client-side navigation to server routes', () => {
return nightmare.goto(`${base}/blog/how-is-sapper-different-from-next`)
.init()
.click(`[href="/blog/how-is-sapper-different-from-next.json"]`)
.wait(200)
.page.text()
.then(text => {
JSON.parse(text);
});
});
it('does not serve error page for non-page errors', () => {
return nightmare.goto(`${base}/throw-an-error`)
.page.text()
.then(text => {
assert.equal(text, 'nope');
});
});
});
describe('headers', () => {
it('sets Content-Type and Link...preload headers', () => {
return capture(() => nightmare.goto(base).end()).then(requests => {
const { headers } = requests[0];
assert.equal(
headers['content-type'],
'text/html'
);
assert.ok(
/<\/client\/[^/]+\/main\.js>;rel="preload";as="script", <\/client\/[^/]+\/_\.\d+\.js>;rel="preload";as="script"/.test(headers['link']),
headers['link']
);
});
});
});
if (env === 'production') {
describe('export', () => {
it('export all pages', () => {
const dest = path.resolve(__dirname, '../app/dist');
// Pages that should show up in the extraction directory.
const expectedPages = [
'index.html',
'about/index.html',
'slow-preload/index.html',
'blog/index.html',
'blog/a-very-long-post/index.html',
'blog/how-can-i-get-involved/index.html',
'blog/how-is-sapper-different-from-next/index.html',
'blog/how-to-use-sapper/index.html',
'blog/what-is-sapper/index.html',
'blog/why-the-name/index.html',
'blog.json',
'blog/a-very-long-post.json',
'blog/how-can-i-get-involved.json',
'blog/how-is-sapper-different-from-next.json',
'blog/how-to-use-sapper.json',
'blog/what-is-sapper.json',
'blog/why-the-name.json',
'favicon.png',
'global.css',
'great-success.png',
'manifest.json',
'service-worker.js',
'svelte-logo-192.png',
'svelte-logo-512.png',
];
// Client scripts that should show up in the extraction directory.
const expectedClientRegexes = [
/client\/[^/]+\/_(\.\d+)?\.js/,
/client\/[^/]+\/about(\.\d+)?\.js/,
/client\/[^/]+\/blog_\$slug\$(\.\d+)?\.js/,
/client\/[^/]+\/blog(\.\d+)?\.js/,
/client\/[^/]+\/main(\.\d+)?\.js/,
/client\/[^/]+\/show_url(\.\d+)?\.js/,
/client\/[^/]+\/slow_preload(\.\d+)?\.js/,
];
const allPages = walkSync(dest);
expectedPages.forEach((expectedPage) => {
assert.ok(allPages.includes(expectedPage),`Could not find page matching ${expectedPage}`);
});
expectedClientRegexes.forEach((expectedRegex) => {
// Ensure each client page regular expression matches at least one
// generated page.
let matched = false;
for (const page of allPages) {
if (expectedRegex.test(page)) {
matched = true;
break;
}
}
assert.ok(matched, `Could not find client page matching ${expectedRegex}`);
});
});
});
}
});
}
function exec(cmd) {
return new Promise((fulfil, reject) => {
const parts = cmd.split(' ');
const proc = require('child_process').spawn(parts.shift(), parts);
proc.stdout.on('data', data => {
process.stdout.write(data);
});
proc.stderr.on('data', data => {
process.stderr.write(data);
});
proc.on('error', reject);
proc.on('close', () => fulfil());
});
}

View File

@@ -0,0 +1,167 @@
const assert = require('assert');
const { create_routes } = require('../../core.js');
describe('create_routes', () => {
it('sorts routes correctly', () => {
const routes = create_routes({
files: ['index.html', 'about.html', 'post/f[xx].html', '[wildcard].html', 'post/foo.html', 'post/[id].html', 'post/bar.html', 'post/[id].json.js']
});
assert.deepEqual(
routes.map(r => r.file),
[
'index.html',
'about.html',
'post/foo.html',
'post/bar.html',
'post/f[xx].html',
'post/[id].json.js',
'post/[id].html',
'[wildcard].html'
]
);
});
it('generates params', () => {
const routes = create_routes({
files: ['index.html', 'about.html', '[wildcard].html', 'post/[id].html']
});
let file;
let params;
for (let i = 0; i < routes.length; i += 1) {
const route = routes[i];
if (params = route.exec('/post/123')) {
file = route.file;
break;
}
}
assert.equal(file, 'post/[id].html');
assert.deepEqual(params, {
id: '123'
});
});
it('ignores files and directories with leading underscores', () => {
const routes = create_routes({
files: ['index.html', '_foo.html', 'a/_b/c/d.html', 'e/f/g/h.html', 'i/_j.html']
});
assert.deepEqual(
routes.map(r => r.file),
[
'index.html',
'e/f/g/h.html'
]
);
});
it('matches /foo/:bar before /:baz/qux', () => {
const a = create_routes({
files: ['foo/[bar].html', '[baz]/qux.html']
});
const b = create_routes({
files: ['[baz]/qux.html', 'foo/[bar].html']
});
assert.deepEqual(
a.map(r => r.file),
['foo/[bar].html', '[baz]/qux.html']
);
assert.deepEqual(
b.map(r => r.file),
['foo/[bar].html', '[baz]/qux.html']
);
});
it('fails if routes are indistinguishable', () => {
assert.throws(() => {
create_routes({
files: ['[foo].html', '[bar]/index.html']
});
}, /The \[foo\].html and \[bar\]\/index.html routes clash/);
assert.throws(() => {
create_routes({
files: ['foo.html', 'foo.js']
});
}, /The foo.html and foo.js routes clash/);
});
it('matches nested routes', () => {
const route = create_routes({
files: ['settings/[submenu].html']
})[0];
assert.deepEqual(route.exec('/settings/foo'), {
submenu: 'foo'
});
assert.deepEqual(route.exec('/settings'), {
submenu: null
});
});
it('prefers index routes to nested routes', () => {
const routes = create_routes({
files: ['settings/[submenu].html', 'settings.html']
});
assert.deepEqual(
routes.map(r => r.file),
['settings.html', 'settings/[submenu].html']
);
});
it('matches deeply nested routes', () => {
const route = create_routes({
files: ['settings/[a]/[b]/index.html']
})[0];
assert.deepEqual(route.exec('/settings/foo/bar'), {
a: 'foo',
b: 'bar'
});
assert.deepEqual(route.exec('/settings/foo'), {
a: 'foo',
b: null
});
assert.deepEqual(route.exec('/settings'), {
a: null,
b: null
});
});
it('matches a dynamic part within a part', () => {
const route = create_routes({
files: ['things/[slug].json.js']
})[0];
assert.deepEqual(route.exec('/things/foo.json'), {
slug: 'foo'
});
});
it('matches multiple dynamic parts within a part', () => {
const route = create_routes({
files: ['things/[id]_[slug].json.js']
})[0];
assert.deepEqual(route.exec('/things/someid_someslug.json'), {
id: 'someid',
slug: 'someslug'
});
});
it('fails if dynamic params are not separated', () => {
assert.throws(() => {
create_routes({
files: ['[foo][bar].js']
});
}, /Invalid route \[foo\]\[bar\]\.js — parameters must be separated/);
});
});

18
tsconfig.json Normal file
View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"diagnostics": true,
"noImplicitThis": true,
"noImplicitAny": true,
"noEmitOnError": true,
"allowJs": true,
"lib": ["es5", "es6", "dom"],
"importHelpers": true
},
"target": "ES5",
"include": [
"src"
],
"exclude": [
"node_modules"
]
}

View File

@@ -1,43 +0,0 @@
const path = require('path');
const route_manager = require('../lib/route_manager.js');
const { src, dest, dev } = require('../lib/config.js');
module.exports = {
dev,
client: {
entry: () => {
return {
main: `${dest}/main.js`
};
},
output: () => {
return {
path: `${dest}/client`,
filename: '[name].[hash].js',
chunkFilename: '[name].[id].[hash].js',
publicPath: '/client/'
};
}
},
server: {
entry: () => {
const entries = {};
route_manager.routes.forEach(route => {
entries[route.id] = path.resolve(src, route.file);
});
return entries;
},
output: () => {
return {
path: `${dest}/server`,
filename: '[name].[hash].js',
chunkFilename: '[name].[id].[hash].js',
libraryTarget: 'commonjs2'
};
}
}
};