Compare commits

..

159 Commits

Author SHA1 Message Date
Richard Harris
f0588ed9ab update to node stable 2019-04-27 13:05:51 -04:00
Richard Harris
01dd08849a Merge branch 'master' of github.com:sveltejs/sapper 2019-04-27 13:05:28 -04:00
Richard Harris
534a96214e pin node version to 11, for now 2019-04-27 13:05:22 -04:00
Rich Harris
9725767fe2 Merge pull request #593 from thgh/patch-4
Provide session in layout preload
2019-04-27 12:19:58 -04:00
Rich Harris
90ec61f14d Merge pull request #603 from sveltejs/gh-602
fix handling of empty hrefs during export
2019-04-27 12:18:45 -04:00
Rich Harris
4f9919e95c Merge pull request #610 from cudr/navigate_fix
Navigate from /a/[id] to /b/[id] fix
2019-04-27 11:56:03 -04:00
Rich Harris
b089ca42ff Merge pull request #608 from cudr/preload_return_fix
Don't crash if preload return empty
2019-04-27 11:15:14 -04:00
Rich Harris
6cb4030b2b Merge branch 'master' into navigate_fix 2019-04-27 11:11:27 -04:00
Rich Harris
a89f7b01bb Merge pull request #616 from Seb35/fix-swindex-base-path
Fix base-path with service-worker-index.html - fixes #579
2019-04-27 11:07:36 -04:00
Rich Harris
96a068245b Merge pull request #620 from cudr/error_page_hooks
Error page lifecycle
2019-04-27 11:06:28 -04:00
Richard Harris
0862d0e2c8 add a test for server-route-as-middleware 2019-04-27 10:55:22 -04:00
Richard Harris
a26f8600c1 Merge branch 'html' of https://github.com/akihikodaki/sapper into akihikodaki-html 2019-04-27 10:47:49 -04:00
Rich Harris
f9d1dc5d3f Merge pull request #622 from artemjackson/patch-2
Fixed %sapper.styles% injection for webpack apps
2019-04-21 13:42:21 -04:00
Rich Harris
52c4106d2c in this house we use tabs 2019-04-21 13:41:52 -04:00
Akihiko Odaki
0fd332135e Allow to have middleware for the path same with a HTML page
The feature was introduced with 9e2d0a7fbc,
but regressed with commit 8dc52a04e4.
2019-04-13 19:42:19 +09:00
cudr
9bb8bfa884 receive preloaded root_data 2019-04-08 20:07:42 +03:00
Artyom Stepanishchev
01c0097acb Fixed %sapper.styles% injection for webpack builds 2019-04-08 18:51:35 +03:00
cudr
dcf726a89b add missed semicolon 2019-04-06 02:51:05 +03:00
cudr
9e60a71cf5 clean up 2019-04-06 02:50:00 +03:00
cudr
3a9d457389 execute error page hooks 2019-04-06 02:32:11 +03:00
Seb35
1e9cd84854 Fix base-path with service-worker-index.html - fixes #579 2019-03-25 20:33:43 +01:00
cudr
d2cda4b6c0 add test 2019-03-15 05:13:18 +03:00
cudr
0dd2d2eb4a complete fix 2019-03-15 04:57:10 +03:00
cudr
6bf3dd04dd fix empty preload 2019-03-13 15:18:23 +03:00
Conduitry
6d5aa9a35d fix handling of empty hrefs during export (#602) 2019-03-11 10:39:35 -04:00
Richard Harris
7be7e1eb9f bump alpha version 2019-03-08 09:25:54 -05:00
Richard Harris
ca7973465b Merge branch 'master' of github.com:sveltejs/sapper 2019-03-08 08:37:08 -05:00
Rich Harris
f7c88df3be Merge pull request #596 from sveltejs/recover-alpha-10
various lost changes and fixes from 0.26.0-alpha.10
2019-03-08 08:35:47 -05:00
Conduitry
74c66b784f Server-side preload check fixes (fixes #575, #594, #598) 2019-03-08 08:34:36 -05:00
Conduitry
9e9bd10333 various lost changes and fixes from 0.26.0-alpha.10 2019-03-07 13:18:40 -05:00
Thomas Ghysels
8858301fed Provide session in layout preload 2019-03-05 15:22:40 +01:00
Rich Harris
9540383796 bump alpha version 2019-03-04 19:44:22 -05:00
halfnelson
b5edf0edd5 Fix export race condition (#585)
* Await all items in the export queue before killing server
2019-03-04 05:21:30 -05:00
Richard Harris
6dad750942 Merge branch 'better-errors' 2019-02-24 20:56:20 -05:00
Richard Harris
eee9d21900 tidy up 2019-02-24 20:56:16 -05:00
Richard Harris
55505571f8 extract entry css correctly 2019-02-24 20:44:19 -05:00
Richard Harris
4fe3c96c2d print file/location/frame when encountering an initial error in dev 2019-02-22 09:28:08 -05:00
Rich Harris
411e2594af bump alpha version 2019-02-21 17:05:06 -05:00
Rich Harris
e0de230e13 Slot-based routing (#573) 2019-02-21 16:34:07 -05:00
Tamas Ridly
c637687922 Fixed README - Get started section 2019-02-18 08:01:09 -05:00
Richard Harris
57fe5bdfa2 bump alpha version 2019-02-17 10:10:02 -05:00
Rich Harris
b2b476abb1 Merge pull request #568 from nolanlawson/nolan/preload-in-export
fix: add link rel=preload for exported sites
2019-02-17 09:55:08 -04:00
Rich Harris
ad0ebb8a69 Merge branch 'master' into nolan/preload-in-export 2019-02-17 09:51:03 -04:00
Rich Harris
130eafbd0a Merge pull request #566 from nolanlawson/nolan/issue-565
fix: fix "sapper export" with a custom PORT
2019-02-17 09:48:02 -04:00
Rich Harris
9d2ce6d852 Merge pull request #563 from nolanlawson/patch-2
Remove unnecessary index.html file serve
2019-02-17 09:46:55 -04:00
Rich Harris
a476d21c9b Merge pull request #561 from thgh/patch-3
Use getElementById to avoid querySelector error
2019-02-17 09:45:57 -04:00
Rich Harris
30b4b6660b Merge pull request #569 from sveltejs/update-rollup
update Rollup, remove some superfluous deps
2019-02-17 09:45:02 -04:00
Richard Harris
cfd10c6f61 remove some more unused deps 2019-02-17 08:32:02 -05:00
Richard Harris
82a4973943 remove node 6 from ci matrix 2019-02-17 08:31:48 -05:00
Richard Harris
0609a92f3a update Rollup, remove some superfluous deps 2019-02-17 08:23:30 -05:00
Nolan Lawson
37780656fd fix incorrect test 2019-02-16 00:13:27 -08:00
Nolan Lawson
351ab13d29 fix: add link rel=preload for exported sites 2019-02-16 00:07:06 -08:00
Nolan Lawson
795da23418 fix: fix "sapper export" with a custom PORT
Fixes #565
2019-02-15 18:50:34 -08:00
Nolan Lawson
1f1211b7b4 Remove unnecessary index.html file serve
As far as I can tell, `/index.html` should no longer exist (outside of `sapper export`, which doesn't apply here). After #525 was merged, we're now using `/service-worker-index.html`, but that's handled by `get_page_handler.ts`, not here. So this code is doing nothing.
2019-02-14 19:14:14 -08:00
Thomas Ghysels
acafeac1cc Fix #561 2019-02-13 17:59:32 +01:00
Rich Harris
82e637ea7c Merge pull request #559 from sveltejs/svelte-extension
Use .svelte extension
2019-02-08 11:47:37 -05:00
Rich Harris
14ace57612 rename .html files to .svelte 2019-02-08 11:40:30 -05:00
Rich Harris
84a0ae562f support .svelte or .html extensions 2019-02-08 11:34:33 -05:00
Rich Harris
8870b58766 bump alpha version 2019-02-08 10:42:46 -05:00
Rich Harris
54506c1eb6 bump alpha version 2019-02-07 08:47:32 -05:00
Rich Harris
4f6b2dcb7c fix kleur thing 2019-02-07 08:47:02 -05:00
Richard Harris
0a87204593 Merge branch 'svelte-3' 2019-02-04 07:43:11 -05:00
Richard Harris
720cf8a859 treat foo.html and foo/index.html the same when generating manifest 2019-02-04 07:42:45 -05:00
Rich Harris
ca034d0857 Support Svelte 3
fixes #546, #551, #552, #554
2019-02-03 14:29:47 -05:00
Richard Harris
96b9d19715 remove typescript in favour of sucrase 2019-02-03 14:28:59 -05:00
Richard Harris
293da8bcd1 fix a cmd-f/cmd-r mistake 2019-02-03 14:12:17 -05:00
Richard Harris
7150c7e088 -> v0.26.0-alpha.3 2019-02-03 13:06:37 -05:00
Richard Harris
a85e1424e3 fix segments 2019-02-03 13:01:40 -05:00
Richard Harris
0628ea99ab minor fixes 2019-02-03 12:51:29 -05:00
Richard Harris
3d7cfbbf3d tidy up 2019-02-03 00:14:53 -05:00
Richard Harris
66be631572 remove experimentalCodeSplitting 2019-02-02 21:00:32 -05:00
Richard Harris
c964500118 implement session/preload on client 2019-02-02 16:13:07 -05:00
Richard Harris
91ea0335ae implement session/preload on server 2019-02-02 15:44:16 -05:00
Richard Harris
11d3da3aed add session tests 2019-02-02 15:42:35 -05:00
Richard Harris
7fefc59929 tidy up 2019-02-02 13:28:48 -05:00
Richard Harris
3521eff4f4 more tidying up 2019-02-02 13:20:53 -05:00
Richard Harris
b7fce99438 simplify 2019-02-02 13:01:55 -05:00
Richard Harris
85c86b5562 tidy up 2019-02-02 12:53:36 -05:00
Richard Harris
81bbfce448 tidy up 2019-02-02 00:15:01 -05:00
Richard Harris
6e8ba295d4 bump svelte version 2019-02-01 23:50:21 -05:00
Rich Harris
0168d8b70c tidy up 2019-02-01 22:37:39 -05:00
Rich Harris
548de702ac move all page info to app-level stores 2019-02-01 22:28:47 -05:00
Rich Harris
7ba1a0a9fa fix test 2019-02-01 11:09:15 -05:00
Rich Harris
3bab780f88 only import preload when appropriate 2019-02-01 11:09:08 -05:00
Rich Harris
263bb08334 various 2019-02-01 10:54:43 -05:00
Richard Harris
3445ec66ac figure out if component has preload early on 2019-02-01 09:28:24 -05:00
Richard Harris
4940c5d5be merge master -> svelte-3, fix various typescript and webpack oddities 2019-02-01 08:21:45 -05:00
Richard Harris
83c8d7f855 -> v0.25.0 2019-02-01 06:45:08 -05:00
Rich Harris
fdfe282130 Merge pull request #484 from mrkishi/goto-temporary-fix
Ensure deepest layout is always refreshed on goto
2019-02-01 06:40:43 -05:00
Rich Harris
b5fbc7e0e8 Merge pull request #539 from cudr/nested-routes-navigate-fix
Fix nested routes navigates
2019-02-01 06:39:25 -05:00
Rich Harris
64eb3f856a Merge pull request #369 from sveltejs/crawl-queue
Don't try to crawl hundreds of pages simultaneously
2019-02-01 06:15:30 -05:00
Richard Harris
18d8e61ecb remove unused declaration 2019-02-01 06:04:43 -05:00
Richard Harris
2a635f92a9 merge master -> crawl-queue 2019-02-01 06:04:00 -05:00
Richard Harris
44bcbeb7d6 remove redundant line 2019-02-01 05:57:17 -05:00
Rich Harris
4023831b18 Merge pull request #542 from sveltejs/gh-528
avoid mutating `opts.headers` in `this.fetch` (#528)
2019-02-01 05:47:54 -05:00
Richard Harris
969169ae20 -> v0.24.3 2019-02-01 05:40:58 -05:00
Rich Harris
79fa15da3d Merge pull request #525 from nolanlawson/nolan/sw-index-html
add service-worker-index.html
2019-02-01 05:36:08 -05:00
Rich Harris
ddc08d94cc Merge pull request #535 from nolanlawson/nolan/no-sourcemap-in-sw
Do not include sourcemap files in Service Worker shell
2019-02-01 05:34:20 -05:00
Rich Harris
f587161d7d first crack at context-driven store 2019-01-31 21:42:29 -05:00
Rich Harris
d486542a8b update store test 2019-01-31 11:32:51 -05:00
Rich Harris
c990c771d8 switch to mjs to align with Svelte 2019-01-31 11:32:05 -05:00
Rich Harris
90f3393ebf default to generating app in src/node_modules/@sapper - fixes #551 2019-01-31 10:35:14 -05:00
Rich Harris
76ce7f227f tidy up test 2019-01-30 16:27:13 -05:00
Rich Harris
1f9efd353c fix scrolling to deep links 2019-01-30 15:43:42 -05:00
Rich Harris
3499631e8e remove experimentalCodeSplitting 2019-01-30 14:08:55 -05:00
Rich Harris
2a825269e9 bump svelte version 2019-01-30 13:38:48 -05:00
Rich Harris
83f7102f6b use nextTick 2019-01-30 13:35:50 -05:00
Rich Harris
e4319bee0e error handling 2019-01-30 10:25:43 -05:00
Rich Harris
da540ef15f update some tests 2019-01-30 10:21:55 -05:00
Rich Harris
c00af6dad0 update deps 2019-01-30 10:21:43 -05:00
Conduitry
92206742d4 avoid mutating opts.headers in this.fetch (#528) 2018-12-31 17:08:29 -05:00
Conduitry
14fc6b3176 support Rollup 1.0 2018-12-31 16:51:05 -05:00
Richard Harris
e2193a6080 -> v0.24.2 2018-12-31 11:46:08 -05:00
Richard Harris
f66c7dcb0d -> v0.24.2 2018-12-31 11:45:20 -05:00
Rich Harris
06f1a0e6c0 Merge pull request #541 from sveltejs/rollup-1.0
support Rollup 1.0
2018-12-31 11:44:22 -05:00
Conduitry
7726325b4b support Rollup 1.0 2018-12-31 11:14:49 -05:00
cudr
b6bc90cea9 add test 2018-12-26 14:12:49 +03:00
cudr
cfba9b2168 fix nested routes navigate 2018-12-25 19:05:00 +03:00
Rich Harris
b25c642bf1 upgrade shimport, bump alpha version 2018-12-23 17:08:54 -05:00
Rich Harris
82a023c302 various 2018-12-20 23:16:03 -05:00
Nolan Lawson
f97400caaa Do not include sourcemap files in Service Worker shell
Fixes #534
2018-12-16 11:48:54 -08:00
Nolan Lawson
03af9b1a16 add service-worker-index.html
fixes #422
2018-12-12 18:30:16 +00:00
Rich Harris
02cef046aa -> v0.24.1 2018-12-09 10:30:34 -05:00
Rich Harris
c2aeac34b6 Merge pull request #530 from artemjackson/patch-1
Fixed rel="preload" `as` attribute for styles
2018-12-09 10:28:39 -05:00
Rich Harris
abd2f7fd39 Merge pull request #529 from artemjackson/master
Added css build info for webpack apps
2018-12-09 10:28:19 -05:00
Artyom Stepanishchev
e7cf9bf1b6 Merge branch 'master' of github.com:sveltejs/sapper 2018-12-09 18:01:31 +03:00
Artyom Stepanishchev
1fdd0e3ad2 Merge branch 'master' of github.com:sveltejs/sapper into patch-1 2018-12-09 18:00:55 +03:00
Rich Harris
af0a7e04f9 Merge pull request #531 from artemjackson/bugfix/tests
Fixed tests for node 6
2018-12-09 09:57:12 -05:00
Artyom Stepanishchev
ed19a19fed Fixed tests for node 6 2018-12-09 17:28:59 +03:00
Artyom Stepanishchev
8ebfcc9a54 Fixed rel="preload" as attribute for styles 2018-12-07 23:59:51 +03:00
Artyom Stepanishchev
af2a792508 Added css build info for webpack apps 2018-12-07 23:28:28 +03:00
Conduitry
14e809af6e update npm-run-all to remove malicious flatmap-stream@0.1.1 2018-12-06 07:02:04 -05:00
Rich Harris
03c5f5b446 -> v0.24.0 2018-10-27 12:40:27 -04:00
Rich Harris
6655b1b49d Merge pull request #503 from sveltejs/gh-490
redirect to external URLs
2018-10-27 12:36:10 -04:00
Rich Harris
eebf076f23 Merge pull request #501 from sveltejs/gh-497
consistent query parameter handling between client and server
2018-10-27 12:35:58 -04:00
Rich Harris
198be28f4b Merge pull request #504 from sveltejs/gh-480
change scroll[X|Y] to page[X|Y]Offset
2018-10-27 12:35:47 -04:00
Rich Harris
4f720446b2 change scroll[X|Y] to page[X|Y]Offset - closes #480 2018-10-27 12:26:53 -04:00
Rich Harris
e69cb3639a redirect to external URLs - closes #490 2018-10-27 12:14:28 -04:00
Rich Harris
1b1a86764f Merge pull request #502 from sveltejs/gh-495
strip leading slash from basepath
2018-10-27 11:50:23 -04:00
Rich Harris
f50f83c4a4 strip leading slash from basepath - fixes #495 2018-10-27 11:41:02 -04:00
Rich Harris
eadefd996b consistent query parameter handling between client and server - fixes #497 2018-10-27 11:36:18 -04:00
Rich Harris
ab52aabd1d Merge pull request #500 from sveltejs/dont-buffer-logs
don't buffer stdout/stderr
2018-10-27 11:18:50 -04:00
Rich Harris
c5a80543b3 reduce flakiness of unrelated test 2018-10-27 11:12:10 -04:00
Rich Harris
cfd95ac024 dont buffer stdout/stderr 2018-10-27 11:02:34 -04:00
Rich Harris
73ff95c677 Merge pull request #498 from mrkishi/build-dir
Add missing posixify to `build_dir`
2018-10-27 10:17:58 -04:00
mrkishi
382fe6b7b9 Add missing posixify 2018-10-26 11:03:29 -03:00
Rich Harris
3b714c0de3 -> v0.23.5 2018-10-24 21:40:04 -04:00
Rich Harris
28186227a9 add tests 2018-10-24 21:20:27 -04:00
Rich Harris
2ac0f2bf3d Merge branch 'search-params-decoding' of https://github.com/mrkishi/sapper into mrkishi-search-params-decoding 2018-10-24 21:10:35 -04:00
Rich Harris
4991f3b359 support non-native promise implementations 2018-10-24 21:05:25 -04:00
Rich Harris
65128118c7 Merge branch '487-async-route-errors' of https://github.com/nikku/sapper into nikku-487-async-route-errors 2018-10-24 20:58:52 -04:00
Rich Harris
3eced6fa4d Merge pull request #492 from sveltejs/lazy-css
fix lazy css bug, add tests
2018-10-24 20:58:08 -04:00
mrkishi
c4aee66c32 Fix search params decoding 2018-10-24 21:19:03 -03:00
Rich Harris
410c52df41 fix lazy css bug, add tests 2018-10-24 18:48:38 -04:00
Nico Rehwaldt
3fe7b55955 async -> Promise.reject 2018-10-20 22:46:42 +02:00
Nico Rehwaldt
464924ed67 handle async route errors
Related to #487
2018-10-20 22:40:21 +02:00
mrkishi
8108642845 Ensure deepest layout is always refreshed on goto 2018-10-18 18:14:23 -03:00
Rich Harris
18e6f29de7 merge master -> crawl-queue 2018-08-22 18:39:28 -04:00
Rich Harris
d0c6b9cdca limit parallel crawls 2018-08-19 12:49:58 -04:00
238 changed files with 4144 additions and 3828 deletions

3
.gitignore vendored
View File

@@ -12,4 +12,5 @@ sapper
runtime.js runtime.js
dist dist
!rollup.config.js !rollup.config.js
templates/*.js /runtime/app.mjs
/runtime/server.mjs

View File

@@ -3,7 +3,6 @@ sudo: false
language: node_js language: node_js
node_js: node_js:
- "6"
- "stable" - "stable"
env: env:

View File

@@ -1,5 +1,41 @@
# sapper changelog # sapper changelog
## 0.25.0
* Force refresh on `goto(current_url)` ([#484](https://github.com/sveltejs/sapper/pull/484))
* Fix preloading navigation bug ([#532](https://github.com/sveltejs/sapper/issues/532))
* Don't mutate opts.headers ([#528](https://github.com/sveltejs/sapper/issues/528))
* Don't crawl hundreds of pages simultaneously ([#369](https://github.com/sveltejs/sapper/pull/369))
## 0.24.3
* Add service-worker-index.html shell file for offline support ([#422](https://github.com/sveltejs/sapper/issues/422))
* Don't cache .map files ([#534](https://github.com/sveltejs/sapper/issues/534))
## 0.24.2
* Support Rollup 1.0 ([#541](https://github.com/sveltejs/sapper/pull/541))
## 0.24.1
* Include CSS chunks in webpack build info to avoid duplication ([#529](https://github.com/sveltejs/sapper/pull/529))
* Fix preload `as` for styles ([#530](https://github.com/sveltejs/sapper/pull/530))
## 0.24.0
* Handle external URLs in `this.redirect` ([#490](https://github.com/sveltejs/sapper/issues/490))
* Strip leading `/` from basepath ([#495](https://github.com/sveltejs/sapper/issues/495))
* Treat duplicate query string parameters as arrays ([#497](https://github.com/sveltejs/sapper/issues/497))
* Don't buffer `stdout` and `stderr` ([#305](https://github.com/sveltejs/sapper/issues/305))
* Posixify `build_dir` ([#498](https://github.com/sveltejs/sapper/pull/498))
* Use `page[XY]Offset` instead of `scroll[XY]` ([#480](https://github.com/sveltejs/sapper/issues/480))
## 0.23.5
* Include lazily-imported CSS in main CSS chunk ([#492](https://github.com/sveltejs/sapper/pull/492))
* Make search param decoding spec-compliant ([#493](https://github.com/sveltejs/sapper/pull/493))
* Handle async route errors ([#488](https://github.com/sveltejs/sapper/pull/488))
## 0.23.4 ## 0.23.4
* Ignore empty anchors when exporting ([#491](https://github.com/sveltejs/sapper/pull/491)) * Ignore empty anchors when exporting ([#491](https://github.com/sveltejs/sapper/pull/491))

View File

@@ -11,9 +11,11 @@ Sapper is a framework for building high-performance universal web apps. [Read th
## Get started ## Get started
Clone the [starter project template](https://github.com/sveltejs/sapper-template) with [degit](https://github.com/rich-harris/degit)... Clone the [starter project template](https://github.com/sveltejs/sapper-template) with [degit](https://github.com/rich-harris/degit)...
When cloning you have to choose between rollup or webpack:
```bash ```bash
npx degit sveltejs/sapper-template my-app npx degit "sveltejs/sapper-template#rollup" my-app
# or: npx degit "sveltejs/sapper-template#webpack" my-app
``` ```
...then install dependencies and start the dev server... ...then install dependencies and start the dev server...

View File

@@ -10,7 +10,7 @@ build: off
environment: environment:
matrix: matrix:
# node.js # node.js
- nodejs_version: 10.5 - nodejs_version: 11
install: install:
- ps: Install-Product node $env:nodejs_version - ps: Install-Product node $env:nodejs_version

View File

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

3408
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "sapper", "name": "sapper",
"version": "0.23.4", "version": "0.26.0-alpha.12",
"description": "Military-grade apps, engineered by Svelte", "description": "Military-grade apps, engineered by Svelte",
"bin": { "bin": {
"sapper": "./sapper" "sapper": "./sapper"
@@ -11,59 +11,57 @@
"config", "config",
"sapper", "sapper",
"dist/*.js", "dist/*.js",
"templates/*.js" "runtime/*.mjs",
"runtime/internal"
], ],
"directories": { "directories": {
"test": "test" "test": "test"
}, },
"dependencies": { "dependencies": {
"html-minifier": "^3.5.20", "html-minifier": "^3.5.21",
"shimport": "0.0.11", "http-link-header": "^1.0.2",
"source-map-support": "^0.5.9", "shimport": "0.0.14",
"sourcemap-codec": "^1.4.3", "sourcemap-codec": "^1.4.4",
"string-hash": "^1.1.3", "string-hash": "^1.1.3"
"tslib": "^1.9.3"
}, },
"devDependencies": { "devDependencies": {
"@types/mkdirp": "^0.5.2",
"@types/mocha": "^5.2.5", "@types/mocha": "^5.2.5",
"@types/node": "^10.12.0", "@types/node": "^10.12.21",
"@types/puppeteer": "^1.9.0", "@types/puppeteer": "^1.11.3",
"@types/rimraf": "^2.0.2",
"agadoo": "^1.0.1", "agadoo": "^1.0.1",
"cheap-watch": "^1.0.0", "cheap-watch": "^1.0.2",
"cookie": "^0.3.1", "cookie": "^0.3.1",
"devalue": "^1.0.4", "devalue": "^1.1.0",
"eslint": "^5.7.0", "eslint": "^5.12.1",
"eslint-plugin-import": "^2.14.0", "eslint-plugin-import": "^2.16.0",
"kleur": "^2.0.2", "kleur": "^3.0.1",
"mkdirp": "^0.5.1",
"mocha": "^5.2.0", "mocha": "^5.2.0",
"node-fetch": "^2.2.0", "node-fetch": "^2.3.0",
"npm-run-all": "^4.1.3", "npm-run-all": "^4.1.5",
"polka": "^0.5.1", "polka": "^0.5.1",
"port-authority": "^1.0.5", "port-authority": "^1.0.5",
"pretty-bytes": "^5.1.0", "pretty-bytes": "^5.1.0",
"puppeteer": "^1.9.0", "puppeteer": "^1.12.0",
"require-relative": "^0.8.7", "require-relative": "^0.8.7",
"rimraf": "^2.6.2", "rollup": "^1.1.2",
"rollup": "^0.66.6",
"rollup-plugin-commonjs": "^9.2.0", "rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-json": "^3.1.0", "rollup-plugin-json": "^3.1.0",
"rollup-plugin-node-resolve": "^3.4.0", "rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-replace": "^2.1.0", "rollup-plugin-replace": "^2.1.0",
"rollup-plugin-string": "^2.0.2", "rollup-plugin-string": "^2.0.2",
"rollup-plugin-svelte": "^4.3.2", "rollup-plugin-sucrase": "^2.1.0",
"rollup-plugin-typescript": "^1.0.0", "rollup-plugin-svelte": "^5.0.3",
"sade": "^1.4.1", "sade": "^1.4.2",
"sander": "^0.6.0",
"sirv": "^0.2.2", "sirv": "^0.2.2",
"svelte": "^2.13.5", "sucrase": "^3.9.5",
"svelte-loader": "^2.11.0", "svelte": "^3.0.0-beta.11",
"ts-node": "^7.0.1", "svelte-loader": "^2.13.3",
"typescript": "^3.1.3", "webpack": "^4.29.0",
"webpack": "^4.20.2", "webpack-format-messages": "^2.0.5",
"webpack-format-messages": "^2.0.3" "yootils": "0.0.14"
},
"peerDependencies": {
"svelte": "^3.0.0"
}, },
"scripts": { "scripts": {
"test": "mocha --opts mocha.opts", "test": "mocha --opts mocha.opts",
@@ -72,7 +70,7 @@
"prepare": "npm run build", "prepare": "npm run build",
"dev": "rollup -cw", "dev": "rollup -cw",
"prepublishOnly": "npm test", "prepublishOnly": "npm test",
"update_mime_types": "curl http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types | grep -e \"^[^#]\" > templates/src/server/middleware/mime-types.md" "update_mime_types": "curl http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types | grep -e \"^[^#]\" > runtime/src/server/middleware/mime-types.md"
}, },
"repository": "https://github.com/sveltejs/sapper", "repository": "https://github.com/sveltejs/sapper",
"keywords": [ "keywords": [

View File

@@ -1,4 +1,4 @@
import typescript from 'rollup-plugin-typescript'; import sucrase from 'rollup-plugin-sucrase';
import string from 'rollup-plugin-string'; import string from 'rollup-plugin-string';
import json from 'rollup-plugin-json'; import json from 'rollup-plugin-json';
import resolve from 'rollup-plugin-node-resolve'; import resolve from 'rollup-plugin-node-resolve';
@@ -9,34 +9,37 @@ import { builtinModules } from 'module';
const external = [].concat( const external = [].concat(
Object.keys(pkg.dependencies), Object.keys(pkg.dependencies),
Object.keys(process.binding('natives')), Object.keys(process.binding('natives')),
'sapper/core.js' 'sapper/core.js',
'svelte/compiler'
); );
function template(kind, external, target) { function template(kind, external) {
return { return {
input: `templates/src/${kind}/index.ts`, input: `runtime/src/${kind}/index.ts`,
output: { output: {
file: `templates/${kind}.js`, file: `runtime/${kind}.mjs`,
format: 'es' format: 'es',
paths: id => id.replace('@sapper', '.')
}, },
external, external,
plugins: [ plugins: [
resolve(), resolve({
extensions: ['.mjs', '.js', '.ts']
}),
commonjs(), commonjs(),
string({ string({
include: '**/*.md' include: '**/*.md'
}), }),
typescript({ sucrase({
typescript: require('typescript'), transforms: ['typescript']
target
}) })
] ]
}; };
} }
export default [ export default [
template('client', ['__ROOT__', '__ERROR__'], 'ES2017'), template('app', id => /^(svelte\/?|@sapper\/)/.test(id)),
template('server', builtinModules, 'ES2015'), template('server', id => /^(svelte\/?|@sapper\/)/.test(id) || builtinModules.includes(id)),
{ {
input: [ input: [
@@ -54,12 +57,13 @@ export default [
external, external,
plugins: [ plugins: [
json(), json(),
resolve(), resolve({
extensions: ['.mjs', '.js', '.ts']
}),
commonjs(), commonjs(),
typescript({ sucrase({
typescript: require('typescript') transforms: ['typescript']
}) })
], ]
experimentalCodeSplitting: true
} }
]; ];

View File

@@ -0,0 +1,12 @@
<script>
import { setContext } from 'svelte';
import { CONTEXT_KEY } from './shared';
export let Root;
export let props;
export let session;
setContext(CONTEXT_KEY, session);
</script>
<Root {...props}/>

View File

@@ -0,0 +1,7 @@
<h1>{status}</h1>
<p>{error.message}</p>
{#if process.env.NODE_ENV === 'development'}
<pre>{error.stack}</pre>
{/if}

View File

@@ -0,0 +1 @@
<slot></slot>

View File

@@ -0,0 +1,10 @@
import { writable } from 'svelte/store';
export const stores = {
preloading: writable(false),
page: writable(null)
};
export const CONTEXT_KEY = {};
export const preload = () => ({});

349
runtime/src/app/app.ts Normal file
View File

@@ -0,0 +1,349 @@
import { writable } from 'svelte/store.mjs';
import App from '@sapper/internal/App.svelte';
import { stores } from '@sapper/internal/shared';
import { Root, root_preload, ErrorComponent, ignore, components, routes } from '@sapper/internal/manifest-client';
import {
Target,
ScrollPosition,
Component,
Redirect,
ComponentLoader,
ComponentConstructor,
Route,
Query,
Page
} from './types';
import goto from './goto';
declare const __SAPPER__;
export const initial_data = typeof __SAPPER__ !== 'undefined' && __SAPPER__;
let ready = false;
let root_component: Component;
let current_token: {};
let root_preloaded: Promise<any>;
let current_branch = [];
const session = writable(initial_data && initial_data.session);
let $session;
let session_dirty: boolean;
session.subscribe(async value => {
$session = value;
if (!ready) return;
session_dirty = true;
const target = select_target(new URL(location.href));
const token = current_token = {};
const { redirect, props, branch } = await hydrate_target(target);
if (token !== current_token) return; // a secondary navigation happened while we were loading
await render(redirect, branch, props, target.page);
});
export let prefetching: {
href: string;
promise: Promise<{ redirect?: Redirect, data?: any }>;
} = null;
export function set_prefetching(href, promise) {
prefetching = { href, promise };
}
export let store;
export function set_store(fn) {
store = fn(initial_data.store);
}
export let target: Node;
export function set_target(element) {
target = element;
}
export let uid = 1;
export function set_uid(n) {
uid = n;
}
export let cid: number;
export function set_cid(n) {
cid = n;
}
const _history = typeof history !== 'undefined' ? history : {
pushState: (state: any, title: string, href: string) => {},
replaceState: (state: any, title: string, href: string) => {},
scrollRestoration: ''
};
export { _history as history };
export const scroll_history: Record<string, ScrollPosition> = {};
export function extract_query(search: string) {
const query = Object.create(null);
if (search.length > 0) {
search.slice(1).split('&').forEach(searchParam => {
let [, key, value] = /([^=]*)(?:=(.*))?/.exec(decodeURIComponent(searchParam));
value = (value || '').replace(/\+/g, ' ');
if (typeof query[key] === 'string') query[key] = [<string>query[key]];
if (typeof query[key] === 'object') (query[key] as string[]).push(value);
else query[key] = value;
});
}
return query;
}
export function select_target(url: URL): Target {
if (url.origin !== location.origin) return null;
if (!url.pathname.startsWith(initial_data.baseUrl)) return null;
const path = url.pathname.slice(initial_data.baseUrl.length);
// avoid accidental clashes between server routes and page routes
if (ignore.some(pattern => pattern.test(path))) return;
for (let i = 0; i < routes.length; i += 1) {
const route = routes[i];
const match = route.pattern.exec(path);
if (match) {
const query: Query = extract_query(url.search);
const part = route.parts[route.parts.length - 1];
const params = part.params ? part.params(match) : {};
const page = { path, query, params };
return { href: url.href, route, match, page };
}
}
}
export function handle_error(url: URL) {
const { pathname, search } = location;
const { session, preloaded, status, error } = initial_data;
if (!root_preloaded) {
root_preloaded = preloaded && preloaded[0]
}
const props = {
error,
status,
session,
level0: {
props: root_preloaded
},
level1: {
props: {
status,
error
},
component: ErrorComponent
},
segments: preloaded
}
const query = extract_query(search);
render(null, [], props, { path: pathname, query, params: {} });
}
export function scroll_state() {
return {
x: pageXOffset,
y: pageYOffset
};
}
export async function navigate(target: Target, id: number, noscroll?: boolean, hash?: string): Promise<any> {
if (id) {
// popstate or initial navigation
cid = id;
} else {
const current_scroll = scroll_state();
// clicked on a link. preserve scroll state
scroll_history[cid] = current_scroll;
id = cid = ++uid;
scroll_history[cid] = noscroll ? current_scroll : { x: 0, y: 0 };
}
cid = id;
if (root_component) stores.preloading.set(true);
const loaded = prefetching && prefetching.href === target.href ?
prefetching.promise :
hydrate_target(target);
prefetching = null;
const token = current_token = {};
const { redirect, props, branch } = await loaded;
if (token !== current_token) return; // a secondary navigation happened while we were loading
await render(redirect, branch, props, target.page);
if (document.activeElement) document.activeElement.blur();
if (!noscroll) {
let scroll = scroll_history[id];
if (hash) {
// scroll is an element id (from a hash), we need to compute y.
const deep_linked = document.getElementById(hash.slice(1));
if (deep_linked) {
scroll = {
x: 0,
y: deep_linked.getBoundingClientRect().top
};
}
}
scroll_history[cid] = scroll;
if (scroll) scrollTo(scroll.x, scroll.y);
}
}
async function render(redirect: Redirect, branch: any[], props: any, page: Page) {
if (redirect) return goto(redirect.location, { replaceState: true });
stores.page.set(page);
stores.preloading.set(false);
if (root_component) {
root_component.$set(props);
} else {
props.session = session;
props.level0 = {
props: await root_preloaded
};
// 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);
}
root_component = new App({
target,
props,
hydrate: true
});
}
current_branch = branch;
ready = true;
session_dirty = false;
}
export async function hydrate_target(target: Target): Promise<{
redirect?: Redirect;
props?: any;
branch?: Array<{ Component: ComponentConstructor, preload: (page) => Promise<any>, segment: string }>;
}> {
const { route, page } = target;
const segments = page.path.split('/').filter(Boolean);
let redirect: Redirect = null;
const props = { error: null, status: 200, segments: [segments[0]] };
const preload_context = {
fetch: (url: string, opts?: any) => fetch(url, opts),
redirect: (statusCode: number, location: string) => {
if (redirect && (redirect.statusCode !== statusCode || redirect.location !== location)) {
throw new Error(`Conflicting redirects`);
}
redirect = { statusCode, location };
},
error: (status: number, error: Error | string) => {
props.error = typeof error === 'string' ? new Error(error) : error;
props.status = status;
}
};
if (!root_preloaded) {
root_preloaded = initial_data.preloaded[0] || root_preload.call(preload_context, {
path: page.path,
query: page.query,
params: {}
}, $session);
}
let branch;
let l = 1;
try {
branch = await Promise.all(route.parts.map(async (part, i) => {
props.segments[l] = segments[i + 1]; // TODO make this less confusing
if (!part) return null;
const j = l++;
const segment = segments[i];
if (!session_dirty && current_branch[i] && current_branch[i].segment === segment && current_branch[i].part === part.i) return current_branch[i];
const { default: component, preload } = await load_component(components[part.i]);
let preloaded;
if (ready || !initial_data.preloaded[i + 1]) {
preloaded = preload
? await preload.call(preload_context, {
path: page.path,
query: page.query,
params: part.params ? part.params(target.match) : {}
}, $session)
: {};
} else {
preloaded = initial_data.preloaded[i + 1];
}
return (props[`level${j}`] = { component, props: preloaded, segment, part: part.i });
}));
} catch (error) {
props.error = error;
props.status = 500;
branch = [];
}
return { redirect, props, branch };
}
function load_css(chunk: string) {
const href = `client/${chunk}`;
if (document.querySelector(`link[href="${href}"]`)) return;
return new Promise((fulfil, reject) => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
link.onload = () => fulfil();
link.onerror = reject;
document.head.appendChild(link);
});
}
export function load_component(component: ComponentLoader): Promise<{
default: ComponentConstructor,
preload?: (input: any) => any
}> {
// TODO this is temporary — once placeholders are
// always rewritten, scratch the ternary
const promises: Array<Promise<any>> = (typeof component.css === 'string' ? [] : component.css.map(load_css));
promises.unshift(component.js());
return Promise.all(promises).then(values => values[0]);
}
function detach(node: Node) {
node.parentNode.removeChild(node);
}

View File

@@ -1,7 +1,7 @@
import { history, select_route, navigate, cid } from '../app'; import { history, select_target, navigate, cid } from '../app';
export default function goto(href: string, opts = { replaceState: false }) { export default function goto(href: string, opts = { replaceState: false }) {
const target = select_route(new URL(href, document.baseURI)); const target = select_target(new URL(href, document.baseURI));
if (target) { if (target) {
history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href); history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);

12
runtime/src/app/index.ts Normal file
View File

@@ -0,0 +1,12 @@
import { getContext } from 'svelte';
import { CONTEXT_KEY, stores } from '@sapper/internal/shared';
export const preloading = { subscribe: stores.preloading.subscribe };
export const page = { subscribe: stores.page.subscribe };
export const getSession = () => getContext(CONTEXT_KEY);
export { default as start } from './start/index';
export { default as goto } from './goto/index';
export { default as prefetch } from './prefetch/index';
export { default as prefetchRoutes } from './prefetchRoutes/index';

View File

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

View File

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

View File

@@ -5,26 +5,23 @@ import {
navigate, navigate,
scroll_history, scroll_history,
scroll_state, scroll_state,
select_route, select_target,
set_store, handle_error,
set_target, set_target,
uid, uid,
set_uid, set_uid,
set_cid set_cid
} from '../app'; } from '../app';
import prefetch from '../prefetch/index'; import prefetch from '../prefetch/index';
import { Store, ScrollPosition } from '../types';
export default function start(opts: { export default function start(opts: {
target: Node, target: Node
store?: (data: any) => Store
}) { }) {
if ('scrollRestoration' in history) { if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual'; history.scrollRestoration = 'manual';
} }
set_target(opts.target); set_target(opts.target);
if (opts.store) set_store(opts.store);
addEventListener('click', handle_click); addEventListener('click', handle_click);
addEventListener('popstate', handle_popstate); addEventListener('popstate', handle_popstate);
@@ -38,10 +35,12 @@ export default function start(opts: {
history.replaceState({ id: uid }, '', href); history.replaceState({ id: uid }, '', href);
if (!initial_data.error) { const url = new URL(location.href);
const target = select_route(new URL(location.href));
if (target) return navigate(target, uid, false, hash); if (initial_data.error) return handle_error(url);
}
const target = select_target(url);
if (target) return navigate(target, uid, false, hash);
}); });
} }
@@ -96,7 +95,7 @@ function handle_click(event: MouseEvent) {
// Don't handle hash changes // Don't handle hash changes
if (url.pathname === location.pathname && url.search === location.search) return; if (url.pathname === location.pathname && url.search === location.search) return;
const target = select_route(url); const target = select_target(url);
if (target) { if (target) {
const noscroll = a.hasAttribute('sapper-noscroll'); const noscroll = a.hasAttribute('sapper-noscroll');
navigate(target, null, noscroll, url.hash); navigate(target, null, noscroll, url.hash);
@@ -119,7 +118,7 @@ function handle_popstate(event: PopStateEvent) {
if (event.state) { if (event.state) {
const url = new URL(location.href); const url = new URL(location.href);
const target = select_route(url); const target = select_target(url);
if (target) { if (target) {
navigate(target, event.state.id); navigate(target, event.state.id);
} else { } else {
@@ -131,4 +130,4 @@ function handle_popstate(event: PopStateEvent) {
set_cid(uid); set_cid(uid);
history.replaceState({ id: cid }, '', location.href); history.replaceState({ id: cid }, '', location.href);
} }
} }

View File

@@ -8,21 +8,14 @@ type Child = {
component?: Component; component?: Component;
}; };
export type RootProps = {
path: string;
params: Record<string, string>;
query: Record<string, string>;
child: Child;
};
export interface ComponentConstructor { export interface ComponentConstructor {
new (options: { target: Node, data: any, store: Store, hydrate: boolean }): Component; new (options: { target: Node, props: any, hydrate: boolean }): Component;
preload: (props: { params: Params, query: Query }) => Promise<any>; preload: (props: { params: Params, query: Query }) => Promise<any>;
}; };
export interface Component { export interface Component {
set: (data: any) => void; $set: (data: any) => void;
destroy: () => void; $destroy: () => void;
} }
export type ComponentLoader = { export type ComponentLoader = {
@@ -30,7 +23,7 @@ export type ComponentLoader = {
css: string[] css: string[]
}; };
export type Page = { export type Route = {
pattern: RegExp; pattern: RegExp;
parts: Array<{ parts: Array<{
i: number; i: number;
@@ -42,7 +35,7 @@ export type Manifest = {
ignore: RegExp[]; ignore: RegExp[];
root: ComponentConstructor; root: ComponentConstructor;
error: () => Promise<{ default: ComponentConstructor }>; error: () => Promise<{ default: ComponentConstructor }>;
pages: Page[] pages: Route[]
}; };
export type ScrollPosition = { export type ScrollPosition = {
@@ -51,11 +44,10 @@ export type ScrollPosition = {
}; };
export type Target = { export type Target = {
url: URL; href: string;
path: string; route: Route;
page: Page;
match: RegExpExecArray; match: RegExpExecArray;
query: Record<string, string | true>; page: Page;
}; };
export type Redirect = { export type Redirect = {
@@ -63,6 +55,8 @@ export type Redirect = {
location: string; location: string;
}; };
export type Store = { export type Page = {
get: () => any; path: string;
} params: Record<string, string>;
query: Record<string, string | string[]>;
};

View File

@@ -0,0 +1 @@
export const IGNORE = '__SAPPER__IGNORE__';

View File

@@ -1,15 +1,19 @@
import * as fs from 'fs'; import { writable } from 'svelte/store.mjs';
import * as path from 'path'; import fs from 'fs';
import path from 'path';
import cookie from 'cookie'; import cookie from 'cookie';
import devalue from 'devalue'; import devalue from 'devalue';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import { URL } from 'url'; import URL from 'url';
import { build_dir, dev, src_dir, IGNORE } from '../placeholders'; import { IGNORE } from '../constants';
import { Manifest, Page, Props, Req, Res, Store } from './types'; import { Manifest, Page, Props, Req, Res } from './types';
import { build_dir, dev, src_dir } from '@sapper/internal/manifest-server';
import { stores } from '@sapper/internal/shared';
import App from '@sapper/internal/App.svelte';
export function get_page_handler( export function get_page_handler(
manifest: Manifest, manifest: Manifest,
store_getter: (req: Req, res: Res) => Store session_getter: (req: Req, res: Res) => any
) { ) {
const get_build_info = dev const get_build_info = dev
? () => JSON.parse(fs.readFileSync(path.join(build_dir, 'build.json'), 'utf-8')) ? () => JSON.parse(fs.readFileSync(path.join(build_dir, 'build.json'), 'utf-8'))
@@ -34,6 +38,7 @@ export function get_page_handler(
} }
async function handle_page(page: Page, req: Req, res: Res, status = 200, error: Error | string = null) { async function handle_page(page: Page, req: Req, res: Res, status = 200, error: Error | string = null) {
const is_service_worker_index = req.path === '/service-worker-index.html';
const build_info: { const build_info: {
bundler: 'rollup' | 'webpack', bundler: 'rollup' | 'webpack',
shimport: string | null, shimport: string | null,
@@ -47,7 +52,7 @@ export function get_page_handler(
// preload main.js and current route // preload main.js and current route
// TODO detect other stuff we can preload? images, CSS, fonts? // TODO detect other stuff we can preload? images, CSS, fonts?
let preloaded_chunks = Array.isArray(build_info.assets.main) ? build_info.assets.main : [build_info.assets.main]; let preloaded_chunks = Array.isArray(build_info.assets.main) ? build_info.assets.main : [build_info.assets.main];
if (!error) { if (!error && !is_service_worker_index) {
page.parts.forEach(part => { page.parts.forEach(part => {
if (!part) return; if (!part) return;
@@ -67,13 +72,16 @@ export function get_page_handler(
} else { } else {
const link = preloaded_chunks const link = preloaded_chunks
.filter(file => file && !file.match(/\.map$/)) .filter(file => file && !file.match(/\.map$/))
.map(file => `<${req.baseUrl}/client/${file}>;rel="preload";as="script"`) .map((file) => {
const as = /\.css$/.test(file) ? 'style' : 'script';
return `<${req.baseUrl}/client/${file}>;rel="preload";as="${as}"`;
})
.join(', '); .join(', ');
res.setHeader('Link', link); res.setHeader('Link', link);
} }
const store = store_getter ? store_getter(req, res) : null; const session = session_getter(req, res);
let redirect: { statusCode: number, location: string }; let redirect: { statusCode: number, location: string };
let preload_error: { statusCode: number, message: Error | string }; let preload_error: { statusCode: number, message: Error | string };
@@ -90,7 +98,7 @@ export function get_page_handler(
preload_error = { statusCode, message }; preload_error = { statusCode, message };
}, },
fetch: (url: string, opts?: any) => { fetch: (url: string, opts?: any) => {
const parsed = new URL(url, `http://127.0.0.1:${process.env.PORT}${req.baseUrl ? req.baseUrl + '/' :''}`); const parsed = new URL.URL(url, `http://127.0.0.1:${process.env.PORT}${req.baseUrl ? req.baseUrl + '/' :''}`);
if (opts) { if (opts) {
opts = Object.assign({}, opts); opts = Object.assign({}, opts);
@@ -101,7 +109,7 @@ export function get_page_handler(
); );
if (include_cookies) { if (include_cookies) {
if (!opts.headers) opts.headers = {}; opts.headers = Object.assign({}, opts.headers);
const cookies = Object.assign( const cookies = Object.assign(
{}, {},
@@ -124,35 +132,44 @@ export function get_page_handler(
} }
return fetch(parsed.href, opts); return fetch(parsed.href, opts);
}, }
store
}; };
let preloaded; let preloaded;
let match; let match;
let params;
try { try {
const root_preloaded = manifest.root.preload const root_preloaded = manifest.root_preload
? manifest.root.preload.call(preload_context, { ? manifest.root_preload.call(preload_context, {
path: req.path, path: req.path,
query: req.query, query: req.query,
params: {} params: {}
}) }, session)
: {}; : {};
match = error ? null : page.pattern.exec(req.path); match = error ? null : page.pattern.exec(req.path);
preloaded = await Promise.all([root_preloaded].concat(page.parts.map(part => {
if (!part) return null;
return part.component.preload let toPreload = [root_preloaded];
? part.component.preload.call(preload_context, { if (!is_service_worker_index) {
path: req.path, toPreload = toPreload.concat(page.parts.map(part => {
query: req.query, if (!part) return null;
params: part.params ? part.params(match) : {}
}) // the deepest level is used below, to initialise the store
: {}; params = part.params ? part.params(match) : {};
})));
return part.preload
? part.preload.call(preload_context, {
path: req.path,
query: req.query,
params
}, session)
: {};
}))
}
preloaded = await Promise.all(toPreload);
} catch (err) { } catch (err) {
preload_error = { statusCode: 500, message: err }; preload_error = { statusCode: 500, message: err };
preloaded = []; // appease TypeScript preloaded = []; // appease TypeScript
@@ -160,7 +177,7 @@ export function get_page_handler(
try { try {
if (redirect) { if (redirect) {
const location = `${req.baseUrl}/${redirect.location}`; const location = URL.resolve(req.baseUrl || '/', redirect.location);
res.statusCode = redirect.statusCode; res.statusCode = redirect.statusCode;
res.setHeader('Location', location); res.setHeader('Location', location);
@@ -174,61 +191,67 @@ export function get_page_handler(
return; return;
} }
const serialized = {
preloaded: `[${preloaded.map(data => try_serialize(data)).join(',')}]`,
store: store && try_serialize(store.get())
};
const segments = req.path.split('/').filter(Boolean); const segments = req.path.split('/').filter(Boolean);
const props: Props = { // TODO make this less confusing
path: req.path, const layout_segments = [segments[0]];
query: req.query, let l = 1;
params: {},
child: null page.parts.forEach((part, i) => {
layout_segments[l] = segments[i + 1];
if (!part) return null;
l++;
});
const props = {
segments: layout_segments,
status: error ? status : 200,
error: error ? error instanceof Error ? error : { message: error } : null,
session: writable(session),
level0: {
props: preloaded[0]
},
level1: {
segment: segments[0],
props: {}
}
}; };
if (error) { if (!is_service_worker_index) {
props.error = error instanceof Error ? error : { message: error }; let l = 1;
props.status = status; for (let i = 0; i < page.parts.length; i += 1) {
} const part = page.parts[i];
if (!part) continue;
const data = Object.assign({}, props, preloaded[0], { props[`level${l++}`] = {
params: {}, component: part.component,
child: { props: preloaded[i + 1] || {},
segment: segments[0] segment: segments[i]
};
} }
});
let level = data.child;
for (let i = 0; i < page.parts.length; i += 1) {
const part = page.parts[i];
if (!part) continue;
const get_params = part.params || (() => ({}));
Object.assign(level, {
component: part.component,
props: Object.assign({}, props, {
params: get_params(match)
}, preloaded[i + 1])
});
level.props.child = <Props["child"]>{
segment: segments[i + 1]
};
level = level.props.child;
} }
const { html, head, css } = manifest.root.render(data, { stores.page.set({
store path: req.path,
query: req.query,
params: params
}); });
const { html, head, css } = App.render(props);
const serialized = {
preloaded: `[${preloaded.map(data => try_serialize(data)).join(',')}]`,
session: session && try_serialize(session, err => {
throw new Error(`Failed to serialize session data: ${err.message}`);
}),
error: error && try_serialize(props.error)
};
let script = `__SAPPER__={${[ let script = `__SAPPER__={${[
error && `error:1`, error && `error:${serialized.error},status:${status}`,
`baseUrl:"${req.baseUrl}"`, `baseUrl:"${req.baseUrl}"`,
serialized.preloaded && `preloaded:${serialized.preloaded}`, serialized.preloaded && `preloaded:${serialized.preloaded}`,
serialized.store && `store:${serialized.store}` serialized.session && `session:${serialized.session}`
].filter(Boolean).join(',')}};`; ].filter(Boolean).join(',')}};`;
if (has_service_worker) { if (has_service_worker) {
@@ -287,6 +310,7 @@ export function get_page_handler(
res.statusCode = status; res.statusCode = status;
res.end(body); res.end(body);
} catch(err) { } catch(err) {
console.log(err);
if (error) { if (error) {
// we encountered an error while rendering the error page — oops // we encountered an error while rendering the error page — oops
res.statusCode = 500; res.statusCode = 500;
@@ -300,12 +324,16 @@ export function get_page_handler(
return function find_route(req: Req, res: Res, next: () => void) { return function find_route(req: Req, res: Res, next: () => void) {
if (req[IGNORE]) return next(); if (req[IGNORE]) return next();
if (!server_routes.some(route => route.pattern.test(req.path))) { if (req.path === '/service-worker-index.html') {
for (const page of pages) { const homePage = pages.find(page => page.pattern.test('/'));
if (page.pattern.test(req.path)) { handle_page(homePage, req, res);
handle_page(page, req, res); return;
return; }
}
for (const page of pages) {
if (page.pattern.test(req.path)) {
handle_page(page, req, res);
return;
} }
} }
@@ -317,10 +345,11 @@ function read_template(dir = build_dir) {
return fs.readFileSync(`${dir}/template.html`, 'utf-8'); return fs.readFileSync(`${dir}/template.html`, 'utf-8');
} }
function try_serialize(data: any) { function try_serialize(data: any, fail?: (err) => void) {
try { try {
return devalue(data); return devalue(data);
} catch (err) { } catch (err) {
if (fail) fail(err);
return null; return null;
} }
} }
@@ -335,4 +364,4 @@ function escape_html(html: string) {
}; };
return html.replace(/["'&<>]/g, c => `&${chars[c]};`); return html.replace(/["'&<>]/g, c => `&${chars[c]};`);
} }

View File

@@ -1,8 +1,8 @@
import { IGNORE } from '../placeholders'; import { IGNORE } from '../constants';
import { Req, Res, ServerRoute } from './types'; import { Req, Res, ServerRoute } from './types';
export function get_server_route_handler(routes: ServerRoute[]) { export function get_server_route_handler(routes: ServerRoute[]) {
function handle_route(route: ServerRoute, req: Req, res: Res, next: () => void) { async function handle_route(route: ServerRoute, req: Req, res: Res, next: () => void) {
req.params = route.params(route.pattern.exec(req.path)); req.params = route.params(route.pattern.exec(req.path));
const method = req.method.toLowerCase(); const method = req.method.toLowerCase();
@@ -53,7 +53,7 @@ export function get_server_route_handler(routes: ServerRoute[]) {
}; };
try { try {
handle_method(req, res, handle_next); await handle_method(req, res, handle_next);
} catch (err) { } catch (err) {
handle_next(err); handle_next(err);
} }

View File

@@ -1,16 +1,17 @@
import * as fs from 'fs'; import fs from 'fs';
import * as path from 'path'; import path from 'path';
import { build_dir, dev, manifest, IGNORE } from '../placeholders'; import { build_dir, dev, manifest } from '@sapper/internal/manifest-server';
import { Handler, Req, Res, Store } from './types'; import { Handler, Req, Res } from './types';
import { get_server_route_handler } from './get_server_route_handler'; import { get_server_route_handler } from './get_server_route_handler';
import { get_page_handler } from './get_page_handler'; import { get_page_handler } from './get_page_handler';
import { lookup } from './mime'; import { lookup } from './mime';
import { IGNORE } from '../constants';
export default function middleware(opts: { export default function middleware(opts: {
store?: (req: Req, res: Res) => Store, session?: (req: Req, res: Res) => any,
ignore?: any ignore?: any
} = {}) { } = {}) {
const { store, ignore } = opts; const { session, ignore } = opts;
let emitted_basepath = false; let emitted_basepath = false;
@@ -51,11 +52,6 @@ export default function middleware(opts: {
next(); next();
}, },
fs.existsSync(path.join(build_dir, 'index.html')) && serve({
pathname: '/index.html',
cache_control: dev ? 'no-cache' : 'max-age=600'
}),
fs.existsSync(path.join(build_dir, 'service-worker.js')) && serve({ fs.existsSync(path.join(build_dir, 'service-worker.js')) && serve({
pathname: '/service-worker.js', pathname: '/service-worker.js',
cache_control: 'no-cache, no-store, must-revalidate' cache_control: 'no-cache, no-store, must-revalidate'
@@ -73,7 +69,7 @@ export default function middleware(opts: {
get_server_route_handler(manifest.server_routes), get_server_route_handler(manifest.server_routes),
get_page_handler(manifest, store) get_page_handler(manifest, session || noop)
].filter(Boolean)); ].filter(Boolean));
} }
@@ -140,4 +136,6 @@ export function serve({ prefix, pathname, cache_control }: {
next(); next();
} }
}; };
} }
function noop(){}

View File

@@ -12,6 +12,7 @@ export type Page = {
name: string; name: string;
component: Component; component: Component;
params?: (match: RegExpMatchArray) => Record<string, string>; params?: (match: RegExpMatchArray) => Record<string, string>;
preload?: (data: any) => any | Promise<any>;
}> }>
}; };
@@ -19,19 +20,13 @@ export type Manifest = {
server_routes: ServerRoute[]; server_routes: ServerRoute[];
pages: Page[]; pages: Page[];
root: Component; root: Component;
root_preload?: (data: any) => any | Promise<any>;
error: Component; error: Component;
} }
export type Handler = (req: Req, res: Res, next: () => void) => void; export type Handler = (req: Req, res: Res, next: () => void) => void;
export type Store = {
get: () => any
};
export type Props = { export type Props = {
path: string;
query: Record<string, string>;
params: Record<string, string>;
error?: { message: string }; error?: { message: string };
status?: number; status?: number;
child: { child: {
@@ -60,10 +55,9 @@ export interface Res extends ServerResponse {
export { ServerResponse }; export { ServerResponse };
interface Component { interface Component {
render: (data: any, opts: { store: Store }) => { render: (data: any) => {
head: string; head: string;
css: { code: string, map: any }; css: { code: string, map: any };
html: string html: string
}, }
preload: (data: any) => any | Promise<any>
} }

View File

@@ -1,14 +1,14 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import minify_html from './utils/minify_html'; import minify_html from './utils/minify_html';
import { create_compilers, create_main_manifests, create_manifest_data, create_serviceworker_manifest } from '../core'; import { create_compilers, create_app, create_manifest_data, create_serviceworker_manifest } from '../core';
import { copy_shimport } from './utils/copy_shimport'; import { copy_shimport } from './utils/copy_shimport';
import read_template from '../core/read_template'; import read_template from '../core/read_template';
import { CompileResult } from '../core/create_compilers/interfaces'; import { CompileResult } from '../core/create_compilers/interfaces';
import { noop } from './utils/noop'; import { noop } from './utils/noop';
import validate_bundler from './utils/validate_bundler'; import validate_bundler from './utils/validate_bundler';
import { copy_runtime } from './utils/copy_runtime';
import { rimraf, mkdirp } from './utils/fs_utils';
type Opts = { type Opts = {
cwd?: string; cwd?: string;
@@ -26,7 +26,7 @@ export async function build({
cwd, cwd,
src = 'src', src = 'src',
routes = 'src/routes', routes = 'src/routes',
output = '__sapper__', output = 'src/node_modules/@sapper',
static: static_files = 'static', static: static_files = 'static',
dest = '__sapper__/build', dest = '__sapper__/build',
@@ -42,14 +42,17 @@ export async function build({
routes = path.resolve(cwd, routes); routes = path.resolve(cwd, routes);
output = path.resolve(cwd, output); output = path.resolve(cwd, output);
static_files = path.resolve(cwd, static_files); static_files = path.resolve(cwd, static_files);
dest = path.resolve(cwd, dest);
if (legacy && bundler === 'webpack') { if (legacy && bundler === 'webpack') {
throw new Error(`Legacy builds are not supported for projects using webpack`); throw new Error(`Legacy builds are not supported for projects using webpack`);
} }
rimraf.sync(path.join(dest, '**/*')); rimraf(output);
mkdirp.sync(`${dest}/client`); mkdirp(output);
copy_runtime(output);
rimraf(dest);
mkdirp(`${dest}/client`);
copy_shimport(dest); copy_shimport(dest);
// minify src/template.html // minify src/template.html
@@ -67,8 +70,8 @@ export async function build({
const manifest_data = create_manifest_data(routes); const manifest_data = create_manifest_data(routes);
// create src/manifest/client.js and src/manifest/server.js // create src/node_modules/@sapper/app.mjs and server.mjs
create_main_manifests({ create_app({
bundler, bundler,
manifest_data, manifest_data,
cwd, cwd,
@@ -116,10 +119,15 @@ export async function build({
let serviceworker_stats; let serviceworker_stats;
if (serviceworker) { if (serviceworker) {
const client_files = client_result.chunks
.filter(chunk => !chunk.file.endsWith('.map')) // SW does not need to cache sourcemap files
.map(chunk => `client/${chunk.file}`);
create_serviceworker_manifest({ create_serviceworker_manifest({
manifest_data, manifest_data,
output, output,
client_files: client_result.chunks.map(chunk => `client/${chunk.file}`), client_files,
static_files static_files
}); });

View File

@@ -3,10 +3,8 @@ import * as fs from 'fs';
import * as http from 'http'; import * as http from 'http';
import * as child_process from 'child_process'; import * as child_process from 'child_process';
import * as ports from 'port-authority'; import * as ports from 'port-authority';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { create_manifest_data, create_main_manifests, create_compilers, create_serviceworker_manifest } from '../core'; import { create_manifest_data, create_app, create_compilers, create_serviceworker_manifest } from '../core';
import { Compiler, Compilers } from '../core/create_compilers'; import { Compiler, Compilers } from '../core/create_compilers';
import { CompileResult } from '../core/create_compilers/interfaces'; import { CompileResult } from '../core/create_compilers/interfaces';
import Deferred from './utils/Deferred'; import Deferred from './utils/Deferred';
@@ -15,6 +13,8 @@ import { copy_shimport } from './utils/copy_shimport';
import { ManifestData, FatalEvent, ErrorEvent, ReadyEvent, InvalidEvent } from '../interfaces'; import { ManifestData, FatalEvent, ErrorEvent, ReadyEvent, InvalidEvent } from '../interfaces';
import read_template from '../core/read_template'; import read_template from '../core/read_template';
import { noop } from './utils/noop'; import { noop } from './utils/noop';
import { copy_runtime } from './utils/copy_runtime';
import { rimraf, mkdirp } from './utils/fs_utils';
type Opts = { type Opts = {
cwd?: string, cwd?: string,
@@ -72,7 +72,7 @@ class Watcher extends EventEmitter {
cwd = '.', cwd = '.',
src = 'src', src = 'src',
routes = 'src/routes', routes = 'src/routes',
output = '__sapper__', output = 'src/node_modules/@sapper',
static: static_files = 'static', static: static_files = 'static',
dest = '__sapper__/dev', dest = '__sapper__/dev',
'dev-port': dev_port, 'dev-port': dev_port,
@@ -144,8 +144,13 @@ class Watcher extends EventEmitter {
} }
const { cwd, src, dest, routes, output, static: static_files } = this.dirs; const { cwd, src, dest, routes, output, static: static_files } = this.dirs;
rimraf.sync(dest);
mkdirp.sync(`${dest}/client`); rimraf(output);
mkdirp(output);
copy_runtime(output);
rimraf(dest);
mkdirp(`${dest}/client`);
if (this.bundler === 'rollup') copy_shimport(dest); if (this.bundler === 'rollup') copy_shimport(dest);
if (!this.dev_port) this.dev_port = await ports.find(10000); if (!this.dev_port) this.dev_port = await ports.find(10000);
@@ -157,7 +162,7 @@ class Watcher extends EventEmitter {
try { try {
manifest_data = create_manifest_data(routes); manifest_data = create_manifest_data(routes);
create_main_manifests({ create_app({
bundler: this.bundler, bundler: this.bundler,
manifest_data, manifest_data,
dev: true, dev: true,
@@ -185,7 +190,7 @@ class Watcher extends EventEmitter {
() => { () => {
try { try {
const new_manifest_data = create_manifest_data(routes); const new_manifest_data = create_manifest_data(routes);
create_main_manifests({ create_app({
bundler: this.bundler, bundler: this.bundler,
manifest_data, // TODO is this right? not new_manifest_data? manifest_data, // TODO is this right? not new_manifest_data?
dev: true, dev: true,
@@ -194,9 +199,10 @@ class Watcher extends EventEmitter {
}); });
manifest_data = new_manifest_data; manifest_data = new_manifest_data;
} catch (err) { } catch (error) {
this.emit('error', <ErrorEvent>{ this.emit('error', <ErrorEvent>{
message: err.message type: 'manifest',
error
}); });
} }
} }
@@ -214,12 +220,9 @@ class Watcher extends EventEmitter {
// TODO watch the configs themselves? // TODO watch the configs themselves?
const compilers: Compilers = await create_compilers(this.bundler, cwd, src, dest, false); const compilers: Compilers = await create_compilers(this.bundler, cwd, src, dest, false);
let log = '';
const emitFatal = () => { const emitFatal = () => {
this.emit('fatal', <FatalEvent>{ this.emit('fatal', <FatalEvent>{
message: `Server crashed`, message: `Server crashed`
log
}); });
this.crashed = true; this.crashed = true;
@@ -236,7 +239,6 @@ class Watcher extends EventEmitter {
handle_result: (result: CompileResult) => { handle_result: (result: CompileResult) => {
deferred.promise.then(() => { deferred.promise.then(() => {
const restart = () => { const restart = () => {
log = '';
this.crashed = false; this.crashed = false;
ports.wait(this.port) ports.wait(this.port)
@@ -260,8 +262,7 @@ class Watcher extends EventEmitter {
if (this.crashed) return; if (this.crashed) return;
this.emit('fatal', <FatalEvent>{ this.emit('fatal', <FatalEvent>{
message: `Server is not listening on port ${this.port}`, message: `Server is not listening on port ${this.port}`
log
}); });
}); });
}; };
@@ -292,12 +293,10 @@ class Watcher extends EventEmitter {
}); });
this.proc.stdout.on('data', chunk => { this.proc.stdout.on('data', chunk => {
log += chunk;
this.emit('stdout', chunk); this.emit('stdout', chunk);
}); });
this.proc.stderr.on('data', chunk => { this.proc.stderr.on('data', chunk => {
log += chunk;
this.emit('stderr', chunk); this.emit('stderr', chunk);
}); });
@@ -410,11 +409,11 @@ class Watcher extends EventEmitter {
}) { }) {
compiler.oninvalid(invalid); compiler.oninvalid(invalid);
compiler.watch((err?: Error, result?: CompileResult) => { compiler.watch((error?: Error, result?: CompileResult) => {
if (err) { if (error) {
this.emit('error', <ErrorEvent>{ this.emit('error', <ErrorEvent>{
type: name, type: name,
message: err.message error
}); });
} else { } else {
this.emit('build', { this.emit('build', {
@@ -491,7 +490,7 @@ function watch_dir(
let watch: any; let watch: any;
let closed = false; let closed = false;
import('cheap-watch').then(CheapWatch => { import('cheap-watch').then(({ default: CheapWatch }) => {
if (closed) return; if (closed) return;
watch = new CheapWatch({ dir, filter, debounce: 50 }); watch = new CheapWatch({ dir, filter, debounce: 50 });

View File

@@ -1,13 +1,16 @@
import * as child_process from 'child_process'; import * as child_process from 'child_process';
import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as sander from 'sander';
import * as url from 'url'; import * as url from 'url';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import * as yootils from 'yootils';
import * as ports from 'port-authority'; import * as ports from 'port-authority';
import clean_html from './utils/clean_html'; import clean_html from './utils/clean_html';
import minify_html from './utils/minify_html'; import minify_html from './utils/minify_html';
import Deferred from './utils/Deferred'; import Deferred from './utils/Deferred';
import { noop } from './utils/noop'; import { noop } from './utils/noop';
import { parse as parseLinkHeader } from 'http-link-header';
import { rimraf, copy, mkdirp } from './utils/fs_utils';
type Opts = { type Opts = {
build_dir?: string, build_dir?: string,
@@ -20,6 +23,12 @@ type Opts = {
onfile?: ({ file, size, status }: { file: string, size: number, status: number }) => void; onfile?: ({ file, size, status }: { file: string, size: number, status: number }) => void;
}; };
type Ref = {
uri: string,
rel: string,
as: string
};
function resolve(from: string, to: string) { function resolve(from: string, to: string) {
return url.parse(url.resolve(from, to)); return url.parse(url.resolve(from, to));
} }
@@ -38,26 +47,23 @@ async function _export({
oninfo = noop, oninfo = noop,
onfile = noop onfile = noop
}: Opts = {}) { }: Opts = {}) {
basepath = basepath.replace(/^\//, '')
cwd = path.resolve(cwd); cwd = path.resolve(cwd);
static_files = path.resolve(cwd, static_files); static_files = path.resolve(cwd, static_files);
build_dir = path.resolve(cwd, build_dir); build_dir = path.resolve(cwd, build_dir);
export_dir = path.resolve(cwd, export_dir, basepath); export_dir = path.resolve(cwd, export_dir, basepath);
// Prep output directory // Prep output directory
sander.rimrafSync(export_dir); rimraf(export_dir);
sander.copydirSync(static_files).to(export_dir); copy(static_files, export_dir);
sander.copydirSync(build_dir, 'client').to(export_dir, 'client'); copy(path.join(build_dir, 'client'), path.join(export_dir, 'client'));
copy(path.join(build_dir, 'service-worker.js'), path.join(export_dir, 'service-worker.js'));
copy(path.join(build_dir, 'service-worker.js.map'), path.join(export_dir, 'service-worker.js.map'));
if (sander.existsSync(build_dir, 'service-worker.js')) { const defaultPort = process.env.PORT ? parseInt(process.env.PORT) : 3000;
sander.copyFileSync(build_dir, 'service-worker.js').to(export_dir, 'service-worker.js'); const port = await ports.find(defaultPort);
}
if (sander.existsSync(build_dir, 'service-worker.js.map')) {
sander.copyFileSync(build_dir, 'service-worker.js.map').to(export_dir, 'service-worker.js.map');
}
const port = await ports.find(3000);
const protocol = 'http:'; const protocol = 'http:';
const host = `localhost:${port}`; const host = `localhost:${port}`;
@@ -82,8 +88,8 @@ async function _export({
const seen = new Set(); const seen = new Set();
const saved = new Set(); const saved = new Set();
function save(path: string, status: number, type: string, body: string) { function save(url: string, status: number, type: string, body: string) {
const { pathname } = resolve(origin, path); const { pathname } = resolve(origin, url);
let file = decodeURIComponent(pathname.slice(1)); let file = decodeURIComponent(pathname.slice(1));
if (saved.has(file)) return; if (saved.has(file)) return;
@@ -92,7 +98,9 @@ async function _export({
const is_html = type === 'text/html'; const is_html = type === 'text/html';
if (is_html) { if (is_html) {
file = file === '' ? 'index.html' : `${file}/index.html`; if (pathname !== '/service-worker-index.html') {
file = file === '' ? 'index.html' : `${file}/index.html`;
}
body = minify_html(body); body = minify_html(body);
} }
@@ -102,7 +110,9 @@ async function _export({
status status
}); });
sander.writeFileSync(export_dir, file, body); const export_file = path.join(export_dir, file);
mkdirp(path.dirname(export_file));
fs.writeFileSync(export_file, body);
} }
proc.on('message', message => { proc.on('message', message => {
@@ -111,7 +121,10 @@ async function _export({
}); });
async function handle(url: URL) { async function handle(url: URL) {
const pathname = (url.pathname.replace(root.pathname, '') || '/'); let pathname = url.pathname;
if (pathname !== '/service-worker-index.html') {
pathname = pathname.replace(root.pathname, '') || '/'
}
if (seen.has(pathname)) return; if (seen.has(pathname)) return;
seen.add(pathname); seen.add(pathname);
@@ -131,37 +144,48 @@ async function _export({
clearTimeout(the_timeout); // prevent it hanging at the end clearTimeout(the_timeout); // prevent it hanging at the end
let type = r.headers.get('Content-Type'); let type = r.headers.get('Content-Type');
let body = await r.text(); let body = await r.text();
const range = ~~(r.status / 100); const range = ~~(r.status / 100);
if (range === 2) { if (range === 2) {
if (type === 'text/html') { if (type === 'text/html') {
const urls: URL[] = []; // parse link rel=preload headers and embed them in the HTML
let link = parseLinkHeader(r.headers.get('Link') || '');
link.refs.forEach((ref: Ref) => {
if (ref.rel === 'preload') {
body = body.replace('</head>',
`<link rel="preload" as=${JSON.stringify(ref.as)} href=${JSON.stringify(ref.uri)}></head>`)
}
});
if (pathname !== '/service-worker-index.html') {
const cleaned = clean_html(body);
const cleaned = clean_html(body); const q = yootils.queue(8);
const base_match = /<base ([\s\S]+?)>/m.exec(cleaned); const base_match = /<base ([\s\S]+?)>/m.exec(cleaned);
const base_href = base_match && get_href(base_match[1]); const base_href = base_match && get_href(base_match[1]);
const base = resolve(url.href, base_href); const base = resolve(url.href, base_href);
let match; let match;
let pattern = /<a ([\s\S]+?)>/gm; let pattern = /<a ([\s\S]+?)>/gm;
while (match = pattern.exec(cleaned)) { while (match = pattern.exec(cleaned)) {
const attrs = match[1]; const attrs = match[1];
const href = get_href(attrs); const href = get_href(attrs);
if (href) { if (href) {
const url = resolve(base.href, href); const url = resolve(base.href, href);
if (url.protocol === protocol && url.host === host) { if (url.protocol === protocol && url.host === host) {
urls.push(url); q.add(() => handle(url));
}
} }
} }
}
await Promise.all(urls.map(handle)); await q.close();
}
} }
} }
@@ -179,6 +203,7 @@ async function _export({
return ports.wait(port) return ports.wait(port)
.then(() => handle(root)) .then(() => handle(root))
.then(() => handle(resolve(root.href, 'service-worker-index.html')))
.then(() => proc.kill()) .then(() => proc.kill())
.catch(err => { .catch(err => {
proc.kill(); proc.kill();
@@ -187,6 +212,6 @@ async function _export({
} }
function get_href(attrs: string) { function get_href(attrs: string) {
const match = /href\s*=\s*(?:"(.*?)"|'(.+?)'|([^\s>]+))/.exec(attrs); const match = /href\s*=\s*(?:"(.*?)"|'(.*?)'|([^\s>]*))/.exec(attrs);
return match[1] || match[2] || match[3]; return match && (match[1] || match[2] || match[3]);
} }

View File

@@ -0,0 +1,21 @@
import * as fs from 'fs';
import * as path from 'path';
import { mkdirp } from './fs_utils';
const runtime = [
'app.mjs',
'server.mjs',
'internal/shared.mjs',
'internal/layout.svelte',
'internal/error.svelte'
].map(file => ({
file,
source: fs.readFileSync(path.join(__dirname, `../runtime/${file}`), 'utf-8')
}));
export function copy_runtime(output: string) {
runtime.forEach(({ file, source }) => {
mkdirp(path.dirname(`${output}/${file}`));
fs.writeFileSync(`${output}/${file}`, source);
});
}

46
src/api/utils/fs_utils.ts Normal file
View File

@@ -0,0 +1,46 @@
import * as fs from 'fs';
import * as path from 'path';
export function mkdirp(dir: string) {
const parent = path.dirname(dir);
if (parent === dir) return;
mkdirp(parent);
try {
fs.mkdirSync(dir);
} catch (err) {
// ignore
}
}
export function rimraf(thing: string) {
if (!fs.existsSync(thing)) return;
const stats = fs.statSync(thing);
if (stats.isDirectory()) {
fs.readdirSync(thing).forEach(file => {
rimraf(path.join(thing, file));
});
fs.rmdirSync(thing);
} else {
fs.unlinkSync(thing);
}
}
export function copy(from: string, to: string) {
if (!fs.existsSync(from)) return;
const stats = fs.statSync(from);
if (stats.isDirectory()) {
fs.readdirSync(from).forEach(file => {
copy(path.join(from, file), path.join(to, file));
});
} else {
mkdirp(path.dirname(to));
fs.writeFileSync(to, fs.readFileSync(from));
}
}

View File

@@ -10,7 +10,7 @@ const prog = sade('sapper').version(pkg.version);
if (process.argv[2] === 'start') { if (process.argv[2] === 'start') {
// remove this in a future version // remove this in a future version
console.error(colors.bold.red(`'sapper start' has been removed`)); console.error(colors.bold().red(`'sapper start' has been removed`));
console.error(`Use 'node [build_dir]' instead`); console.error(`Use 'node [build_dir]' instead`);
process.exit(1); process.exit(1);
} }
@@ -29,7 +29,7 @@ prog.command('dev')
.option('--src', 'Source directory', 'src') .option('--src', 'Source directory', 'src')
.option('--routes', 'Routes directory', 'src/routes') .option('--routes', 'Routes directory', 'src/routes')
.option('--static', 'Static files directory', 'static') .option('--static', 'Static files directory', 'static')
.option('--output', 'Sapper output directory', '__sapper__') .option('--output', 'Sapper output directory', 'src/node_modules/@sapper')
.option('--build-dir', 'Development build directory', '__sapper__/dev') .option('--build-dir', 'Development build directory', '__sapper__/dev')
.action(async (opts: { .action(async (opts: {
port: number, port: number,
@@ -64,45 +64,51 @@ prog.command('dev')
let first = true; let first = true;
watcher.on('stdout', data => {
process.stdout.write(data);
});
watcher.on('stderr', data => {
process.stderr.write(data);
});
watcher.on('ready', async (event: ReadyEvent) => { watcher.on('ready', async (event: ReadyEvent) => {
if (first) { if (first) {
console.log(colors.bold.cyan(`> Listening on http://localhost:${event.port}`)); console.log(colors.bold().cyan(`> Listening on http://localhost:${event.port}`));
if (opts.open) { if (opts.open) {
const { exec } = await import('child_process'); const { exec } = await import('child_process');
exec(`open http://localhost:${event.port}`); exec(`open http://localhost:${event.port}`);
} }
first = false; first = false;
} }
// TODO clear screen?
event.process.stdout.on('data', data => {
process.stdout.write(data);
});
event.process.stderr.on('data', data => {
process.stderr.write(data);
});
}); });
watcher.on('invalid', (event: InvalidEvent) => { watcher.on('invalid', (event: InvalidEvent) => {
const changed = event.changed.map(filename => path.relative(process.cwd(), filename)).join(', '); const changed = event.changed.map(filename => path.relative(process.cwd(), filename)).join(', ');
console.log(`\n${colors.bold.cyan(changed)} changed. rebuilding...`); console.log(`\n${colors.bold().cyan(changed)} changed. rebuilding...`);
}); });
watcher.on('error', (event: ErrorEvent) => { watcher.on('error', (event: ErrorEvent) => {
console.log(colors.red(`${event.type}`)); const { type, error } = event;
console.log(colors.red(event.message));
console.log(colors.bold().red(`${type}`));
if (error.loc && error.loc.file) {
console.log(colors.bold(`${path.relative(process.cwd(), error.loc.file)} (${error.loc.line}:${error.loc.column})`));
}
console.log(colors.red(event.error.message));
if (error.frame) console.log(error.frame);
}); });
watcher.on('fatal', (event: FatalEvent) => { watcher.on('fatal', (event: FatalEvent) => {
console.log(colors.bold.red(`> ${event.message}`)); console.log(colors.bold().red(`> ${event.message}`));
if (event.log) console.log(event.log); if (event.log) console.log(event.log);
}); });
watcher.on('build', (event: BuildEvent) => { watcher.on('build', (event: BuildEvent) => {
if (event.errors.length) { if (event.errors.length) {
console.log(colors.bold.red(`${event.type}`)); console.log(colors.bold().red(`${event.type}`));
event.errors.filter(e => !e.duplicate).forEach(error => { event.errors.filter(e => !e.duplicate).forEach(error => {
if (error.file) console.log(colors.bold(error.file)); if (error.file) console.log(colors.bold(error.file));
@@ -114,7 +120,7 @@ prog.command('dev')
console.log(`${hidden} duplicate ${hidden === 1 ? 'error' : 'errors'} hidden\n`); console.log(`${hidden} duplicate ${hidden === 1 ? 'error' : 'errors'} hidden\n`);
} }
} else if (event.warnings.length) { } else if (event.warnings.length) {
console.log(colors.bold.yellow(`${event.type}`)); console.log(colors.bold().yellow(`${event.type}`));
event.warnings.filter(e => !e.duplicate).forEach(warning => { event.warnings.filter(e => !e.duplicate).forEach(warning => {
if (warning.file) console.log(colors.bold(warning.file)); if (warning.file) console.log(colors.bold(warning.file));
@@ -126,11 +132,12 @@ prog.command('dev')
console.log(`${hidden} duplicate ${hidden === 1 ? 'warning' : 'warnings'} hidden\n`); console.log(`${hidden} duplicate ${hidden === 1 ? 'warning' : 'warnings'} hidden\n`);
} }
} else { } else {
console.log(`${colors.bold.green(`${event.type}`)} ${colors.gray(`(${format_milliseconds(event.duration)})`)}`); console.log(`${colors.bold().green(`${event.type}`)} ${colors.gray(`(${format_milliseconds(event.duration)})`)}`);
} }
}); });
} catch (err) { } catch (err) {
console.log(colors.bold.red(`> ${err.message}`)); console.log(colors.bold().red(`> ${err.message}`));
console.log(colors.gray(err.stack));
process.exit(1); process.exit(1);
} }
}); });
@@ -143,7 +150,7 @@ prog.command('build [dest]')
.option('--cwd', 'Current working directory', '.') .option('--cwd', 'Current working directory', '.')
.option('--src', 'Source directory', 'src') .option('--src', 'Source directory', 'src')
.option('--routes', 'Routes directory', 'src/routes') .option('--routes', 'Routes directory', 'src/routes')
.option('--output', 'Sapper output directory', '__sapper__') .option('--output', 'Sapper output directory', 'src/node_modules/@sapper')
.example(`build custom-dir -p 4567`) .example(`build custom-dir -p 4567`)
.action(async (dest = '__sapper__/build', opts: { .action(async (dest = '__sapper__/build', opts: {
port: string, port: string,
@@ -170,9 +177,10 @@ prog.command('build [dest]')
require('./server/server.js'); require('./server/server.js');
`.replace(/^\t+/gm, '').trim()); `.replace(/^\t+/gm, '').trim());
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold.cyan(`node ${dest}`)} to run the app.`); console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold().cyan(`node ${dest}`)} to run the app.`);
} catch (err) { } catch (err) {
console.log(`${colors.bold.red(`> ${err.message}`)}`); console.log(`${colors.bold().red(`> ${err.message}`)}`);
console.log(colors.gray(err.stack));
process.exit(1); process.exit(1);
} }
}); });
@@ -188,7 +196,7 @@ prog.command('export [dest]')
.option('--src', 'Source directory', 'src') .option('--src', 'Source directory', 'src')
.option('--routes', 'Routes directory', 'src/routes') .option('--routes', 'Routes directory', 'src/routes')
.option('--static', 'Static files directory', 'static') .option('--static', 'Static files directory', 'static')
.option('--output', 'Sapper output directory', '__sapper__') .option('--output', 'Sapper output directory', 'src/node_modules/@sapper')
.option('--build-dir', 'Intermediate build directory', '__sapper__/build') .option('--build-dir', 'Intermediate build directory', '__sapper__/build')
.action(async (dest = '__sapper__/export', opts: { .action(async (dest = '__sapper__/export', opts: {
build: boolean, build: boolean,
@@ -222,24 +230,24 @@ prog.command('export [dest]')
timeout: opts.timeout, timeout: opts.timeout,
oninfo: event => { oninfo: event => {
console.log(colors.bold.cyan(`> ${event.message}`)); console.log(colors.bold().cyan(`> ${event.message}`));
}, },
onfile: event => { onfile: event => {
const size_color = event.size > 150000 ? colors.bold.red : event.size > 50000 ? colors.bold.yellow : colors.bold.gray; const size_color = event.size > 150000 ? colors.bold().red : event.size > 50000 ? colors.bold().yellow : colors.bold().gray;
const size_label = size_color(left_pad(pb(event.size), 10)); const size_label = size_color(left_pad(pb(event.size), 10));
const file_label = event.status === 200 const file_label = event.status === 200
? event.file ? event.file
: colors.bold[event.status >= 400 ? 'red' : 'yellow'](`(${event.status}) ${event.file}`); : colors.bold()[event.status >= 400 ? 'red' : 'yellow'](`(${event.status}) ${event.file}`);
console.log(`${size_label} ${file_label}`); console.log(`${size_label} ${file_label}`);
} }
}); });
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold.cyan(`npx serve ${dest}`)} to run the app.`); console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold().cyan(`npx serve ${dest}`)} to run the app.`);
} catch (err) { } catch (err) {
console.error(colors.bold.red(`> ${err.message}`)); console.error(colors.bold().red(`> ${err.message}`));
process.exit(1); process.exit(1);
} }
}); });
@@ -268,12 +276,12 @@ async function _build(
oncompile: event => { oncompile: event => {
let banner = `built ${event.type}`; let banner = `built ${event.type}`;
let c = colors.cyan; let c = (txt: string) => colors.cyan(txt);
const { warnings } = event.result; const { warnings } = event.result;
if (warnings.length > 0) { if (warnings.length > 0) {
banner += ` with ${warnings.length} ${warnings.length === 1 ? 'warning' : 'warnings'}`; banner += ` with ${warnings.length} ${warnings.length === 1 ? 'warning' : 'warnings'}`;
c = colors.yellow; c = (txt: string) => colors.cyan(txt);
} }
console.log(); console.log();
@@ -284,4 +292,4 @@ async function _build(
console.log(event.result.print()); console.log(event.result.print());
} }
}); });
} }

View File

@@ -1,3 +1,3 @@
export * from './core/create_manifests'; export * from './core/create_app';
export { default as create_compilers } from './core/create_compilers/index'; export { default as create_compilers } from './core/create_compilers/index';
export { default as create_manifest_data } from './core/create_manifest_data'; export { default as create_manifest_data } from './core/create_manifest_data';

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

@@ -0,0 +1,291 @@
import * as fs from 'fs';
import * as path from 'path';
import { posixify, stringify, walk, write_if_changed } from '../utils';
import { Page, PageComponent, ManifestData } from '../interfaces';
export function create_app({
bundler,
manifest_data,
dev_port,
dev,
cwd,
src,
dest,
routes,
output
}: {
bundler: string,
manifest_data: ManifestData;
dev_port?: number;
dev: boolean;
cwd: string;
src: string;
dest: string;
routes: string;
output: string
}) {
if (!fs.existsSync(output)) fs.mkdirSync(output);
const path_to_routes = path.relative(`${output}/internal`, routes);
const client_manifest = generate_client_manifest(manifest_data, path_to_routes, bundler, dev, dev_port);
const server_manifest = generate_server_manifest(manifest_data, path_to_routes, cwd, src, dest, dev);
const app = generate_app(manifest_data, path_to_routes);
write_if_changed(`${output}/internal/manifest-client.mjs`, client_manifest);
write_if_changed(`${output}/internal/manifest-server.mjs`, server_manifest);
write_if_changed(`${output}/internal/App.svelte`, app);
}
export function create_serviceworker_manifest({ manifest_data, output, client_files, static_files }: {
manifest_data: ManifestData;
output: string;
client_files: string[];
static_files: string;
}) {
let files: string[] = ['service-worker-index.html'];
if (fs.existsSync(static_files)) {
files = files.concat(walk(static_files));
} else {
// TODO remove in a future version
if (fs.existsSync('assets')) {
throw new Error(`As of Sapper 0.21, the assets/ directory should become static/`);
}
}
let code = `
// This file is generated by Sapper — do not edit it!
export const timestamp = ${Date.now()};
export const files = [\n\t${files.map((x: string) => stringify(x)).join(',\n\t')}\n];
export { files as assets }; // legacy
export const shell = [\n\t${client_files.map((x: string) => stringify(x)).join(',\n\t')}\n];
export const routes = [\n\t${manifest_data.pages.map((r: Page) => `{ pattern: ${r.pattern} }`).join(',\n\t')}\n];
`.replace(/^\t\t/gm, '').trim();
write_if_changed(`${output}/service-worker.js`, code);
}
function generate_client_manifest(
manifest_data: ManifestData,
path_to_routes: string,
bundler: string,
dev: boolean,
dev_port?: number
) {
const page_ids = new Set(manifest_data.pages.map(page =>
page.pattern.toString()));
const server_routes_to_ignore = manifest_data.server_routes.filter(route =>
!page_ids.has(route.pattern.toString()));
const component_indexes: Record<string, number> = {};
const components = `[
${manifest_data.components.map((component, i) => {
const annotation = bundler === 'webpack'
? `/* webpackChunkName: "${component.name}" */ `
: '';
const source = get_file(path_to_routes, component);
component_indexes[component.name] = i;
return `{
js: () => import(${annotation}${stringify(source)}),
css: "__SAPPER_CSS_PLACEHOLDER:${stringify(component.file, false)}__"
}`;
}).join(',\n\t\t\t\t')}
]`.replace(/^\t/gm, '');
let needs_decode = false;
let routes = `[
${manifest_data.pages.map(page => `{
// ${page.parts[page.parts.length - 1].component.file}
pattern: ${page.pattern},
parts: [
${page.parts.map(part => {
if (part === null) return 'null';
if (part.params.length > 0) {
needs_decode = true;
const props = part.params.map((param, i) => `${param}: d(match[${i + 1}])`);
return `{ i: ${component_indexes[part.component.name]}, params: match => ({ ${props.join(', ')} }) }`;
}
return `{ i: ${component_indexes[part.component.name]} }`;
}).join(',\n\t\t\t\t\t\t')}
]
}`).join(',\n\n\t\t\t\t')}
]`.replace(/^\t/gm, '');
if (needs_decode) {
routes = `(d => ${routes})(decodeURIComponent)`
}
return `
// This file is generated by Sapper — do not edit it!
export { default as Root } from '${stringify(get_file(path_to_routes, manifest_data.root), false)}';
export { preload as root_preload } from '${manifest_data.root.has_preload ? stringify(get_file(path_to_routes, manifest_data.root), false) : './shared'}';
export { default as ErrorComponent } from '${stringify(get_file(path_to_routes, manifest_data.error), false)}';
export const ignore = [${server_routes_to_ignore.map(route => route.pattern).join(', ')}];
export const components = ${components};
export const routes = ${routes};
${dev ? `if (typeof window !== 'undefined') {
import(${stringify(posixify(path.resolve(__dirname, '../sapper-dev-client.js')))}).then(client => {
client.connect(${dev_port});
});
}` : ''}
`.replace(/^\t{2}/gm, '').trim();
}
function generate_server_manifest(
manifest_data: ManifestData,
path_to_routes: string,
cwd: string,
src: string,
dest: string,
dev: boolean
) {
const imports = [].concat(
manifest_data.server_routes.map((route, i) =>
`import * as route_${i} from ${stringify(posixify(`${path_to_routes}/${route.file}`))};`),
manifest_data.components.map((component, i) =>
`import component_${i}${component.has_preload ? `, { preload as preload_${i} }` : ''} from ${stringify(get_file(path_to_routes, component))};`),
`import root${manifest_data.root.has_preload ? `, { preload as root_preload }` : ''} from ${stringify(get_file(path_to_routes, manifest_data.root))};`,
`import error from ${stringify(get_file(path_to_routes, manifest_data.error))};`
);
const component_lookup: Record<string, number> = {};
manifest_data.components.forEach((component, i) => {
component_lookup[component.name] = i;
});
let code = `
`.replace(/^\t\t/gm, '').trim();
const build_dir = posixify(path.relative(cwd, dest));
const src_dir = posixify(path.relative(cwd, src));
return `
// This file is generated by Sapper — do not edit it!
${imports.join('\n')}
const d = decodeURIComponent;
export const manifest = {
server_routes: [
${manifest_data.server_routes.map((route, i) => `{
// ${route.file}
pattern: ${route.pattern},
handlers: route_${i},
params: ${route.params.length > 0
? `match => ({ ${route.params.map((param, i) => `${param}: d(match[${i + 1}])`).join(', ')} })`
: `() => ({})`}
}`).join(',\n\n\t\t\t\t')}
],
pages: [
${manifest_data.pages.map(page => `{
// ${page.parts[page.parts.length - 1].component.file}
pattern: ${page.pattern},
parts: [
${page.parts.map(part => {
if (part === null) return 'null';
const props = [
`name: "${part.component.name}"`,
`file: ${stringify(part.component.file)}`,
`component: component_${component_lookup[part.component.name]}`,
part.component.has_preload && `preload: preload_${component_lookup[part.component.name]}`
].filter(Boolean);
if (part.params.length > 0) {
const params = part.params.map((param, i) => `${param}: d(match[${i + 1}])`);
props.push(`params: match => ({ ${params.join(', ')} })`);
}
return `{ ${props.join(', ')} }`;
}).join(',\n\t\t\t\t\t\t')}
]
}`).join(',\n\n\t\t\t\t')}
],
root,
root_preload${manifest_data.root.has_preload ? '' : `: () => {}`},
error
};
export const build_dir = ${JSON.stringify(build_dir)};
export const src_dir = ${JSON.stringify(src_dir)};
export const dev = ${dev ? 'true' : 'false'};
`.replace(/^\t{2}/gm, '').trim();
}
function generate_app(manifest_data: ManifestData, path_to_routes: string) {
// TODO remove default layout altogether
const max_depth = Math.max(...manifest_data.pages.map(page => page.parts.filter(Boolean).length));
const levels = [];
for (let i = 0; i < max_depth; i += 1) {
levels.push(i + 1);
}
let l = max_depth;
let pyramid = `<svelte:component this={level${l}.component} {...level${l}.props}/>`;
while (l-- > 1) {
pyramid = `
<svelte:component this={level${l}.component} segment={segments[${l}]} {...level${l}.props}>
{#if level${l + 1}}
${pyramid.replace(/\n/g, '\n\t\t\t\t\t')}
{/if}
</svelte:component>
`.replace(/^\t\t\t/gm, '').trim();
}
return `
<!-- This file is generated by Sapper — do not edit it! -->
<script>
import { setContext } from 'svelte';
import { CONTEXT_KEY } from './shared';
import Layout from '${get_file(path_to_routes, manifest_data.root)}';
import Error from '${get_file(path_to_routes, manifest_data.error)}';
export let session;
export let error;
export let status;
export let segments;
export let level0;
${levels.map(l => `export let level${l} = null;`).join('\n\t\t\t')}
setContext(CONTEXT_KEY, session);
</script>
<Layout segment={segments[0]} {...level0.props}>
{#if error}
<Error {error} {status}/>
{:else}
${pyramid.replace(/\n/g, '\n\t\t\t\t')}
{/if}
</Layout>
`.replace(/^\t\t/gm, '').trim();
}
function get_file(path_to_routes: string, component: PageComponent) {
if (component.default) return `./${component.type}.svelte`;
return posixify(`${path_to_routes}/${component.file}`);
}

View File

@@ -142,12 +142,14 @@ export default class RollupCompiler {
const bundle = await rollup.rollup({ const bundle = await rollup.rollup({
input, input,
inlineDynamicImports: true,
external: (id: string) => { external: (id: string) => {
return (id[0] !== '.' && !path.isAbsolute(id)) || id.slice(-5, id.length) === '.json'; return (id[0] !== '.' && !path.isAbsolute(id)) || id.slice(-5, id.length) === '.json';
} }
}); });
const { code } = await bundle.generate({ format: 'cjs' }); const resp = await bundle.generate({ format: 'cjs' });
const { code } = resp.output ? resp.output[0] : resp;
// temporarily override require // temporarily override require
const defaultLoader = require.extensions['.js']; const defaultLoader = require.extensions['.js'];

View File

@@ -47,12 +47,13 @@ export default class RollupResult implements CompileResult {
} else { } else {
for (const name in compiler.input) { for (const name in compiler.input) {
const file = compiler.input[name]; const file = compiler.input[name];
this.assets[name] = compiler.chunks.find(chunk => file in chunk.modules).fileName; const chunk = compiler.chunks.find(chunk => file in chunk.modules);
if (chunk) this.assets[name] = chunk.fileName;
} }
} }
this.summary = compiler.chunks.map(chunk => { this.summary = compiler.chunks.map(chunk => {
const size_color = chunk.code.length > 150000 ? colors.bold.red : chunk.code.length > 50000 ? colors.bold.yellow : colors.bold.white; const size_color = chunk.code.length > 150000 ? colors.bold().red : chunk.code.length > 50000 ? colors.bold().yellow : colors.bold().white;
const size_label = left_pad(pb(chunk.code.length), 10); const size_label = left_pad(pb(chunk.code.length), 10);
const lines = [size_color(`${size_label} ${chunk.fileName}`)]; const lines = [size_color(`${size_label} ${chunk.fileName}`)];

View File

@@ -1,6 +1,6 @@
import format_messages from 'webpack-format-messages'; import format_messages from 'webpack-format-messages';
import { CompileResult, BuildInfo, CompileError, Chunk, CssFile } from './interfaces'; import { CompileResult, BuildInfo, CompileError, Chunk, CssFile } from './interfaces';
import { ManifestData, Dirs } from '../../interfaces'; import { ManifestData, Dirs, PageComponent } from '../../interfaces';
const locPattern = /\((\d+):(\d+)\)$/; const locPattern = /\((\d+):(\d+)\)$/;
@@ -55,14 +55,28 @@ export default class WebpackResult implements CompileResult {
} }
to_json(manifest_data: ManifestData, dirs: Dirs): BuildInfo { to_json(manifest_data: ManifestData, dirs: Dirs): BuildInfo {
const extract_css = (assets: string[] | string) => {
assets = Array.isArray(assets) ? assets : [assets];
return assets.find(asset => /\.css$/.test(asset));
};
return { return {
bundler: 'webpack', bundler: 'webpack',
shimport: null, // webpack has its own loader shimport: null, // webpack has its own loader
assets: this.assets, assets: this.assets,
css: { css: {
// TODO main: extract_css(this.assets.main),
main: null, chunks: manifest_data.components
chunks: {} .reduce((chunks: Record<string, string[]>, component: PageComponent) => {
const css_dependencies = [];
const css = extract_css(this.assets[component.name]);
if (css) css_dependencies.push(css);
chunks[component.file] = css_dependencies;
return chunks;
}, {})
} }
}; };
} }
@@ -70,4 +84,4 @@ export default class WebpackResult implements CompileResult {
print() { print() {
return this.stats.toString({ colors: true }); return this.stats.toString({ colors: true });
} }
} }

View File

@@ -161,6 +161,8 @@ export default function extract_css(client_result: CompileResult, components: Pa
// recursively find the chunks this component depends on // recursively find the chunks this component depends on
entry_chunk_dependencies.forEach(chunk => { entry_chunk_dependencies.forEach(chunk => {
if (!chunk) return; // TODO why does this happen?
chunk.imports.forEach(file => { chunk.imports.forEach(file => {
entry_chunk_dependencies.add(lookup.get(file)); entry_chunk_dependencies.add(lookup.get(file));
}); });
@@ -182,7 +184,8 @@ export default function extract_css(client_result: CompileResult, components: Pa
if (!chunk) { if (!chunk) {
// this should never happen! // this should never happen!
throw new Error(`Could not find chunk that owns ${component.file}`); return;
// throw new Error(`Could not find chunk that owns ${component.file}`);
} }
const chunk_dependencies: Set<Chunk> = new Set([chunk]); const chunk_dependencies: Set<Chunk> = new Set([chunk]);
@@ -190,6 +193,8 @@ export default function extract_css(client_result: CompileResult, components: Pa
// recursively find the chunks this component depends on // recursively find the chunks this component depends on
chunk_dependencies.forEach(chunk => { chunk_dependencies.forEach(chunk => {
if (!chunk) return; // TODO why does this happen?
chunk.imports.forEach(file => { chunk.imports.forEach(file => {
chunk_dependencies.add(lookup.get(file)); chunk_dependencies.add(lookup.get(file));
}); });
@@ -219,7 +224,7 @@ export default function extract_css(client_result: CompileResult, components: Pa
}); });
unclaimed.forEach(file => { unclaimed.forEach(file => {
entry_css_modules.push(css_map.get(file)); entry_css_modules.push(file);
}); });
const leftover = get_css_from_modules(entry_css_modules, css_map, dirs); const leftover = get_css_from_modules(entry_css_modules, css_map, dirs);

View File

@@ -1,22 +1,48 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import svelte from 'svelte/compiler';
import { Page, PageComponent, ServerRoute, ManifestData } from '../interfaces'; import { Page, PageComponent, ServerRoute, ManifestData } from '../interfaces';
import { posixify, reserved_words } from '../utils'; import { posixify, reserved_words } from '../utils';
const component_extensions = ['.svelte', '.html']; // TODO make this configurable (to include e.g. .svelte.md?)
export default function create_manifest_data(cwd: string): ManifestData { export default function create_manifest_data(cwd: string): ManifestData {
// TODO remove in a future version // TODO remove in a future version
if (!fs.existsSync(cwd)) { if (!fs.existsSync(cwd)) {
throw new Error(`As of Sapper 0.21, the routes/ directory should become src/routes/`); throw new Error(`As of Sapper 0.21, the routes/ directory should become src/routes/`);
} }
function has_preload(file: string) {
const source = fs.readFileSync(path.join(cwd, file), 'utf-8');
if (/preload/.test(source)) {
try {
const { vars } = svelte.compile(source.replace(/<style\b[^>]*>[^]*?<\/style>/g, ''), { generate: false });
return vars.some((variable: any) => variable.module && variable.export_name === 'preload');
} catch (err) {}
}
return false;
}
const components: PageComponent[] = []; const components: PageComponent[] = [];
const pages: Page[] = []; const pages: Page[] = [];
const server_routes: ServerRoute[] = []; const server_routes: ServerRoute[] = [];
const default_layout: PageComponent = { const default_layout: PageComponent = {
default: true, default: true,
type: 'layout',
name: '_default_layout', name: '_default_layout',
file: null file: null,
has_preload: false
};
const default_error: PageComponent = {
default: true,
type: 'error',
name: '_default_error',
file: null,
has_preload: false
}; };
function walk( function walk(
@@ -43,7 +69,7 @@ export default function create_manifest_data(cwd: string): ManifestData {
const parts = get_parts(segment); const parts = get_parts(segment);
const is_index = is_dir ? false : basename.startsWith('index.'); const is_index = is_dir ? false : basename.startsWith('index.');
const is_page = ext === '.html'; const is_page = component_extensions.indexOf(ext) !== -1;
parts.forEach(part => { parts.forEach(part => {
if (/\]\[/.test(part.content)) { if (/\]\[/.test(part.content)) {
@@ -57,6 +83,7 @@ export default function create_manifest_data(cwd: string): ManifestData {
return { return {
basename, basename,
ext,
parts, parts,
file: posixify(file), file: posixify(file),
is_dir, is_dir,
@@ -103,11 +130,15 @@ export default function create_manifest_data(cwd: string): ManifestData {
params.push(...item.parts.filter(p => p.dynamic).map(p => p.content)); params.push(...item.parts.filter(p => p.dynamic).map(p => p.content));
if (item.is_dir) { if (item.is_dir) {
const index = path.join(dir, item.basename, '_layout.html'); const ext = component_extensions.find((ext: string) => {
const index = path.join(dir, item.basename, `_layout${ext}`);
return fs.existsSync(index);
});
const component = fs.existsSync(index) && { const component = ext && {
name: `${get_slug(item.file)}__layout`, name: `${get_slug(item.file)}__layout`,
file: `${item.file}/_layout.html` file: `${item.file}/_layout${ext}`,
has_preload: has_preload(`${item.file}/_layout${ext}`)
}; };
if (component) components.push(component); if (component) components.push(component);
@@ -123,28 +154,26 @@ export default function create_manifest_data(cwd: string): ManifestData {
} }
else if (item.is_page) { else if (item.is_page) {
const is_index = item.basename === `index${item.ext}`;
const component = { const component = {
name: get_slug(item.file), name: get_slug(item.file),
file: item.file file: item.file,
has_preload: has_preload(item.file)
}; };
const parts = stack.concat({
component,
params
});
components.push(component); components.push(component);
if (item.basename === 'index.html') {
pages.push({ const parts = (is_index && stack[stack.length - 1] === null)
pattern: get_pattern(parent_segments, true), ? stack.slice(0, -1).concat({ component, params })
parts : stack.concat({ component, params })
});
} else { const page = {
pages.push({ pattern: get_pattern(is_index ? parent_segments : segments, true),
pattern: get_pattern(segments, true), parts
parts };
});
} pages.push(page);
} }
else { else {
@@ -158,14 +187,24 @@ export default function create_manifest_data(cwd: string): ManifestData {
}); });
} }
const root_file = path.join(cwd, '_layout.html'); const root_ext = component_extensions.find(ext => fs.existsSync(path.join(cwd, `_layout${ext}`)));
const root = fs.existsSync(root_file) const root = root_ext
? { ? {
name: 'main', name: 'main',
file: '_layout.html' file: `_layout${root_ext}`,
has_preload: has_preload(`_layout${root_ext}`)
} }
: default_layout; : default_layout;
const error_ext = component_extensions.find(ext => fs.existsSync(path.join(cwd, `_error${ext}`)));
const error = error_ext
? {
name: 'error',
file: `_error${error_ext}`,
has_preload: has_preload(`_error${error_ext}`)
}
: default_error;
walk(cwd, [], [], []); walk(cwd, [], [], []);
// check for clashes // check for clashes
@@ -196,6 +235,7 @@ export default function create_manifest_data(cwd: string): ManifestData {
return { return {
root, root,
error,
components, components,
pages, pages,
server_routes server_routes
@@ -303,4 +343,4 @@ function get_pattern(segments: Part[][], add_trailing_slash: boolean) {
}).join('') + }).join('') +
(add_trailing_slash ? '\\\/?$' : '$') (add_trailing_slash ? '\\\/?$' : '$')
); );
} }

View File

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

View File

@@ -19,14 +19,18 @@ export type Template = {
stream: (req, res, data: Record<string, string | Promise<string>>) => void; stream: (req, res, data: Record<string, string | Promise<string>>) => void;
}; };
export type Store = { export type WritableStore<T> = {
get: () => any; set: (value: T) => void;
update: (fn: (value: T) => T) => void;
subscribe: (fn: (T: any) => void) => () => void;
}; };
export type PageComponent = { export type PageComponent = {
default?: boolean; default?: boolean;
type?: string;
name: string; name: string;
file: string; file: string;
has_preload: boolean;
}; };
export type Page = { export type Page = {
@@ -52,6 +56,7 @@ export type Dirs = {
export type ManifestData = { export type ManifestData = {
root: PageComponent; root: PageComponent;
error: PageComponent;
components: PageComponent[]; components: PageComponent[];
pages: Page[]; pages: Page[];
server_routes: ServerRoute[]; server_routes: ServerRoute[];
@@ -64,12 +69,11 @@ export type ReadyEvent = {
export type ErrorEvent = { export type ErrorEvent = {
type: string; type: string;
message: string; error: Error;
}; };
export type FatalEvent = { export type FatalEvent = {
message: string; message: string;
log?: string;
}; };
export type InvalidEvent = { export type InvalidEvent = {

1
src/types.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module 'svelte/compiler';

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import * as path from 'path'; import * as path from 'path';
import * as puppeteer from 'puppeteer'; import puppeteer from 'puppeteer';
import * as ports from 'port-authority'; import * as ports from 'port-authority';
import { fork, ChildProcess } from 'child_process'; import { fork, ChildProcess } from 'child_process';
@@ -8,6 +8,10 @@ declare const prefetchRoutes: () => Promise<void>;
declare const prefetch: (href: string) => Promise<void>; declare const prefetch: (href: string) => Promise<void>;
declare const goto: (href: string) => Promise<void>; declare const goto: (href: string) => Promise<void>;
type StartOpts = {
requestInterceptor?: (interceptedRequst: puppeteer.Request) => any
};
export class AppRunner { export class AppRunner {
cwd: string; cwd: string;
entry: string; entry: string;
@@ -24,7 +28,7 @@ export class AppRunner {
this.messages = []; this.messages = [];
} }
async start() { async start({ requestInterceptor }: StartOpts = {}) {
this.port = await ports.find(3000); this.port = await ports.find(3000);
this.proc = fork(this.entry, [], { this.proc = fork(this.entry, [], {
@@ -50,16 +54,21 @@ export class AppRunner {
} }
}); });
if (requestInterceptor) {
await this.page.setRequestInterception(true);
this.page.on('request', requestInterceptor);
}
return { return {
page: this.page, page: this.page,
base: `http://localhost:${this.port}`, base: `http://localhost:${this.port}`,
// helpers // helpers
start: () => this.page.evaluate(() => start()), start: () => this.page.evaluate(() => start()).then(() => void 0),
prefetchRoutes: () => this.page.evaluate(() => prefetchRoutes()), prefetchRoutes: () => this.page.evaluate(() => prefetchRoutes()).then(() => void 0),
prefetch: (href: string) => this.page.evaluate((href: string) => prefetch(href), href), prefetch: (href: string) => this.page.evaluate((href: string) => prefetch(href), href).then(() => void 0),
goto: (href: string) => this.page.evaluate((href: string) => goto(href), href), goto: (href: string) => this.page.evaluate((href: string) => goto(href), href).then(() => void 0),
title: () => this.page.$eval('h1', node => node.textContent) title: () => this.page.$eval('h1', node => node.textContent).then(serializable => String(serializable))
}; };
} }

View File

@@ -22,10 +22,7 @@ export default {
emitCss: true emitCss: true
}), }),
resolve() resolve()
], ]
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
}, },
server: { server: {
@@ -44,10 +41,7 @@ export default {
preferBuiltins: true preferBuiltins: true
}) })
], ],
external: ['sirv', 'polka'], external: ['sirv', 'polka']
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
}, },
serviceworker: { serviceworker: {

View File

@@ -1,4 +1,4 @@
import * as sapper from '../__sapper__/client.js'; import * as sapper from '@sapper/app';
window.start = () => sapper.start({ window.start = () => sapper.start({
target: document.querySelector('#sapper') target: document.querySelector('#sapper')

View File

@@ -0,0 +1,5 @@
<script>
import { page } from '@sapper/app';
</script>
<h1>{$page.params.slug.toUpperCase()}</h1>

View File

@@ -1,11 +0,0 @@
<h1>{letter}</h1>
<script>
export default {
preload() {
return this.fetch('b.json').then(r => r.json()).then(letter => {
return { letter };
});
}
};
</script>

View File

@@ -0,0 +1,13 @@
<script context="module">
export function preload() {
return this.fetch('b.json').then(r => r.json()).then(letter => {
return { letter };
});
}
</script>
<script>
export let letter;
</script>
<h1>{letter}</h1>

View File

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

View File

@@ -0,0 +1,17 @@
<script>
import { onMount } from 'svelte';
onMount(() => {
window.deleted = null;
});
function del() {
fetch(`delete-test/42.json`, { method: 'DELETE' })
.then(r => r.json())
.then(data => {
window.deleted = data;
});
}
</script>
<button class="del" on:click={del}>delete</button>

View File

@@ -0,0 +1,3 @@
<h1>A page</h1>
<a href="dirs/foo/xyz">same segment</a>

View File

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

View File

@@ -0,0 +1 @@
<h1>B page</h1>

View File

@@ -0,0 +1,2 @@
<h1>foo</h1>
<a href="dirs/bar">bar</a>

View File

@@ -1,9 +0,0 @@
<h1>message: "{message}"</h1>
<script>
export default {
preload({ query }) {
return query;
}
};
</script>

View File

@@ -0,0 +1,5 @@
<script>
import { page } from '@sapper/app';
</script>
<h1>{JSON.stringify($page.query)}</h1>

View File

@@ -3,5 +3,6 @@
<a href="a">a</a> <a href="a">a</a>
<a href="ambiguous/ok.json">ok</a> <a href="ambiguous/ok.json">ok</a>
<a href="echo-query?message">ok</a> <a href="echo-query?message">ok</a>
<a href="echo-query?p=one&p=two">ok</a>
<div class='hydrate-test'></div> <div class='hydrate-test'></div>

View File

@@ -0,0 +1,8 @@
export function get(req, res, next) {
if (req.headers.accept === 'application/json') {
res.end('{"json":true}');
return;
}
next();
}

View File

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

View File

@@ -1,9 +0,0 @@
$&
<script>
export default {
preload() {
return '$&';
}
};
</script>

View File

@@ -0,0 +1,7 @@
<script context="module">
export function preload() {
return '$&';
}
</script>
$&

View File

@@ -1,5 +1,5 @@
import polka from 'polka'; import polka from 'polka';
import * as sapper from '../__sapper__/server.js'; import * as sapper from '@sapper/server';
const { PORT } = process.env; const { PORT } = process.env;

View File

@@ -1,10 +1,10 @@
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js'; import * as sapper from '@sapper/service-worker';
const ASSETS = `cache${timestamp}`; const ASSETS = `cache${sapper.timestamp}`;
// `shell` is an array of all the files generated by webpack, // `app.shell` is an array of all the files generated by webpack,
// `files` is an array of everything in the `static` directory // `app.files` is an array of everything in the `static` directory
const to_cache = shell.concat(ASSETS); const to_cache = sapper.shell.concat(sapper.files);
const cached = new Set(to_cache); const cached = new Set(to_cache);
self.addEventListener('install', event => { self.addEventListener('install', event => {
@@ -52,7 +52,6 @@ self.addEventListener('fetch', event => {
// which Sapper has generated for you. It's not right for every // which Sapper has generated for you. It's not right for every
// app, but if it's right for yours then uncomment this section // 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')); event.respondWith(caches.match('/index.html'));
return; return;
} }
@@ -65,7 +64,7 @@ self.addEventListener('fetch', event => {
// might prefer a cache-first approach to a network-first one.) // might prefer a cache-first approach to a network-first one.)
event.respondWith( event.respondWith(
caches caches
.open(`offline${timestamp}`) .open(`offline${sapper.timestamp}`)
.then(async cache => { .then(async cache => {
try { try {
const response = await fetch(event.request); const response = await fetch(event.request);

View File

@@ -8,6 +8,25 @@ import { wait } from '../../utils';
declare let deleted: { id: number }; declare let deleted: { id: number };
declare let el: any; declare let el: any;
function get(url: string, opts?: any): Promise<{ headers: Record<string, string>, body: string }> {
return new Promise((fulfil, reject) => {
const req = http.get(url, opts || {}, res => {
res.on('error', reject);
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
fulfil({
headers: res.headers as Record<string, string>,
body
});
});
});
req.on('error', reject);
});
}
describe('basics', function() { describe('basics', function() {
this.timeout(10000); this.timeout(10000);
@@ -20,19 +39,18 @@ describe('basics', function() {
let prefetchRoutes: () => Promise<void>; let prefetchRoutes: () => Promise<void>;
let prefetch: (href: string) => Promise<void>; let prefetch: (href: string) => Promise<void>;
let goto: (href: string) => Promise<void>; let goto: (href: string) => Promise<void>;
let title: () => Promise<string>;
// hooks // hooks
before(async () => { before(async () => {
await build({ cwd: __dirname }); await build({ cwd: __dirname });
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js'); runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes, prefetch, goto } = await runner.start()); ({ base, page, start, prefetchRoutes, prefetch, goto, title } = await runner.start());
}); });
after(() => runner.end()); after(() => runner.end());
const title = () => page.$eval('h1', node => node.textContent);
it('serves /', async () => { it('serves /', async () => {
await page.goto(base); await page.goto(base);
@@ -117,38 +135,25 @@ describe('basics', function() {
assert.equal(requests[1], `${base}/b.json`); assert.equal(requests[1], `${base}/b.json`);
}); });
// TODO equivalent test for a webpack app // TODO equivalent test for a webpack app
it('sets Content-Type, Link...modulepreload, and Cache-Control headers', () => { it('sets Content-Type, Link...modulepreload, and Cache-Control headers', async () => {
return new Promise((fulfil, reject) => { const { headers } = await get(base);
const req = http.get(base, res => {
try {
const { headers } = res;
assert.equal( assert.equal(
headers['content-type'], headers['content-type'],
'text/html' 'text/html'
); );
assert.equal( assert.equal(
headers['cache-control'], headers['cache-control'],
'max-age=600' 'max-age=600'
); );
// TODO preload more than just the entry point // TODO preload more than just the entry point
const regex = /<\/client\/client\.\w+\.js>;rel="modulepreload"/; const regex = /<\/client\/client\.\w+\.js>;rel="modulepreload"/;
const link = <string>headers['link']; const link = <string>headers['link'];
assert.ok(regex.test(link), link); assert.ok(regex.test(link), link);
fulfil();
} catch (err) {
reject(err);
}
});
req.on('error', reject);
});
}); });
it('calls a delete handler', async () => { it('calls a delete handler', async () => {
@@ -204,7 +209,7 @@ describe('basics', function() {
assert.equal( assert.equal(
await title(), await title(),
'message: ""' '{"message":""}'
); );
}); });
@@ -217,7 +222,29 @@ describe('basics', function() {
assert.equal( assert.equal(
await title(), await title(),
'message: ""' '{"message":""}'
);
});
it('accepts duplicated query string parameter on server', async () => {
await page.goto(`${base}/echo-query?p=one&p=two`);
assert.equal(
await title(),
'{"p":["one","two"]}'
);
});
it('accepts duplicated query string parameter on client', async () => {
await page.goto(base);
await start();
await prefetchRoutes();
await page.click('a[href="echo-query?p=one&p=two"]')
assert.equal(
await title(),
'{"p":["one","two"]}'
); );
}); });
@@ -240,7 +267,43 @@ describe('basics', function() {
await page.goto(`${base}/unsafe-replacement`); await page.goto(`${base}/unsafe-replacement`);
await start(); await start();
const html = await page.evaluate(() => document.body.innerHTML); const html = String(await page.evaluate(() => document.body.innerHTML));
assert.equal(html.indexOf('%sapper'), -1); assert.equal(html.indexOf('%sapper'), -1);
}); });
it('navigates between routes with empty parts', async () => {
await page.goto(`${base}/dirs/foo`);
await start();
assert.equal(await title(), 'foo');
await page.click('[href="dirs/bar"]');
await wait(50);
assert.equal(await title(), 'bar');
});
it('navigates between dynamic routes with same segments', async () => {
await page.goto(`${base}/dirs/bar/xyz`);
await start();
assert.equal(await title(), 'A page');
await page.click('[href="dirs/foo/xyz"]');
await wait(50);
assert.equal(await title(), 'B page');
});
it('runs server route handlers before page handlers, if they match', async () => {
const json = await get(`${base}/middleware`, {
headers: {
'Accept': 'application/json'
}
});
assert.equal(json.body, '{"json":true}');
const html = await get(`${base}/middleware`);
assert.ok(html.body.indexOf('<h1>HTML</h1>') !== -1);
});
}); });

View File

@@ -22,10 +22,7 @@ export default {
emitCss: true emitCss: true
}), }),
resolve() resolve()
], ]
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
}, },
server: { server: {
@@ -44,10 +41,7 @@ export default {
preferBuiltins: true preferBuiltins: true
}) })
], ],
external: ['sirv', 'polka', 'cookie'], external: ['sirv', 'polka', 'cookie']
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
}, },
serviceworker: { serviceworker: {

View File

@@ -1,4 +1,4 @@
import * as sapper from '../__sapper__/client.js'; import * as sapper from '@sapper/app';
window.start = () => sapper.start({ window.start = () => sapper.start({
target: document.querySelector('#sapper') target: document.querySelector('#sapper')

View File

@@ -1,11 +1,13 @@
<h1>{message}</h1> <script context="module">
export function preload({ query }) {
return this.fetch(`credentials/test.json`, {
credentials: query.creds
}).then(r => r.json());
}
</script>
<script> <script>
export default { export let message;
preload({ query }) { </script>
return this.fetch(`credentials/test.json`, {
credentials: query.creds <h1>{message}</h1>
}).then(r => r.json());
}
};
</script>

View File

@@ -1,5 +1,5 @@
import polka from 'polka'; import polka from 'polka';
import * as sapper from '../__sapper__/server.js'; import * as sapper from '@sapper/server';
const { PORT } = process.env; const { PORT } = process.env;

View File

@@ -1,10 +1,10 @@
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js'; import * as sapper from '@sapper/service-worker';
const ASSETS = `cache${timestamp}`; const ASSETS = `cache${sapper.timestamp}`;
// `shell` is an array of all the files generated by webpack, // `shell` is an array of all the files generated by webpack,
// `files` is an array of everything in the `static` directory // `files` is an array of everything in the `static` directory
const to_cache = shell.concat(ASSETS); const to_cache = sapper.shell.concat(sapper.files);
const cached = new Set(to_cache); const cached = new Set(to_cache);
self.addEventListener('install', event => { self.addEventListener('install', event => {
@@ -65,7 +65,7 @@ self.addEventListener('fetch', event => {
// might prefer a cache-first approach to a network-first one.) // might prefer a cache-first approach to a network-first one.)
event.respondWith( event.respondWith(
caches caches
.open(`offline${timestamp}`) .open(`offline${sapper.timestamp}`)
.then(async cache => { .then(async cache => {
try { try {
const response = await fetch(event.request); const response = await fetch(event.request);

View File

@@ -22,10 +22,7 @@ export default {
emitCss: true emitCss: true
}), }),
resolve() resolve()
], ]
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
}, },
server: { server: {
@@ -44,10 +41,7 @@ export default {
preferBuiltins: true preferBuiltins: true
}) })
], ],
external: ['sirv', 'polka'], external: ['sirv', 'polka']
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
}, },
serviceworker: { serviceworker: {

View File

@@ -1,9 +1,7 @@
import { Store } from 'svelte/store.js'; import * as sapper from '@sapper/app';
import * as sapper from '../__sapper__/client.js';
window.start = () => sapper.start({ window.start = () => sapper.start({
target: document.querySelector('#sapper'), target: document.querySelector('#sapper')
store: data => new Store(data)
}); });
window.prefetchRoutes = () => sapper.prefetchRoutes(); window.prefetchRoutes = () => sapper.prefetchRoutes();

View File

@@ -0,0 +1,7 @@
<h1>Title</h1>
<style>
h1 {
color: green;
}
</style>

View File

@@ -0,0 +1,13 @@
<script>
import { onMount } from 'svelte';
export let Title;
onMount(() => {
import('./_components/Title.svelte').then(mod => {
Title = mod.default;
});
});
</script>
<svelte:component this={Title}/>

View File

@@ -0,0 +1,7 @@
<h1>Foo</h1>
<style>
h1 {
color: blue;
}
</style>

View File

@@ -0,0 +1,10 @@
<h1>Great success!</h1>
<a href="foo">foo</a>
<a href="bar">bar</a>
<style>
h1 {
color: red;
}
</style>

View File

@@ -0,0 +1,8 @@
import polka from 'polka';
import * as sapper from '@sapper/server';
const { PORT } = process.env;
polka()
.use(sapper.middleware())
.listen(PORT);

View File

@@ -0,0 +1,82 @@
import * as sapper from '@sapper/service-worker';
const ASSETS = `cache${sapper.timestamp}`;
// `shell` is an array of all the files generated by webpack,
// `files` is an array of everything in the `static` directory
const to_cache = sapper.shell.concat(sapper.files);
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);
}
self.clients.claim();
})
);
});
self.addEventListener('fetch', event => {
if (event.request.method !== 'GET') return;
const url = new URL(event.request.url);
// don't try to handle e.g. data: URIs
if (!url.protocol.startsWith('http')) return;
// ignore dev server requests
if (url.hostname === self.location.hostname && url.port !== self.location.port) return;
// always serve assets and webpack-generated files from cache
if (url.host === self.location.host && 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;
}
*/
if (event.request.cache === 'only-if-cached') 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${sapper.timestamp}`)
.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;
}
})
);
});

78
test/apps/css/test.ts Normal file
View File

@@ -0,0 +1,78 @@
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
import { AppRunner } from '../AppRunner';
import { wait } from '../../utils';
describe('css', function() {
this.timeout(10000);
let runner: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
let prefetch: (href: string) => Promise<void>;
let goto: (href: string) => Promise<void>;
let title: () => Promise<string>;
// hooks
before(async () => {
await build({ cwd: __dirname });
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes, prefetch, goto, title } = await runner.start());
});
after(() => runner.end());
it('includes critical CSS with server render', async () => {
await page.goto(base);
assert.equal(
await page.evaluate(() => {
const h1 = document.querySelector('h1');
return getComputedStyle(h1).color;
}),
'rgb(255, 0, 0)'
);
});
it('loads CSS when navigating client-side', async () => {
await page.goto(base);
await start();
await prefetchRoutes();
await page.click(`[href="foo"]`);
await wait(50);
assert.equal(
await page.evaluate(() => {
const h1 = document.querySelector('h1');
return getComputedStyle(h1).color;
}),
'rgb(0, 0, 255)'
);
});
it('loads CSS for a lazily-rendered component', async () => {
await page.goto(base);
await start();
await prefetchRoutes();
await page.click(`[href="bar"]`);
await wait(50);
assert.equal(
await page.evaluate(() => {
const h1 = document.querySelector('h1');
return getComputedStyle(h1).color;
}),
'rgb(0, 128, 0)'
);
});
});

View File

@@ -22,10 +22,7 @@ export default {
emitCss: true emitCss: true
}), }),
resolve() resolve()
], ]
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
}, },
server: { server: {
@@ -44,10 +41,7 @@ export default {
preferBuiltins: true preferBuiltins: true
}) })
], ],
external: ['sirv', 'polka'], external: ['sirv', 'polka']
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
}, },
serviceworker: { serviceworker: {

View File

@@ -1,4 +1,4 @@
import * as sapper from '../__sapper__/client.js'; import * as sapper from '@sapper/app';
window.start = () => sapper.start({ window.start = () => sapper.start({
target: document.querySelector('#sapper') target: document.querySelector('#sapper')

View File

@@ -1,12 +1,14 @@
<h1>{slug} ({message})</h1> <script context="module">
export function preload({ params }) {
return {
slug: params.slug
};
}
</script>
<script> <script>
export default { import { page } from '@sapper/app';
preload({ params, query }) { export let slug;
return { </script>
slug: params.slug,
message: query.message <h1>{slug} {JSON.stringify($page.query)}</h1>
};
}
};
</script>

View File

@@ -1,11 +1,13 @@
<h1>{phrase}</h1> <script context="module">
export function preload() {
return this.fetch('fünke.json').then(r => r.json()).then(phrase => {
return { phrase };
});
}
</script>
<script> <script>
export default { export let phrase;
preload() { </script>
return this.fetch('fünke.json').then(r => r.json()).then(phrase => {
return { phrase }; <h1>{phrase}</h1>
});
}
};
</script>

View File

@@ -1,3 +1,3 @@
<h1>Great success!</h1> <h1>Great success!</h1>
<a href="echo/page/encöded?message=hëllö+wörld">link</a> <a href="echo/page/encöded?message=hëllö+wörld&föo=bar&=baz">link</a>

View File

@@ -1,5 +1,5 @@
import polka from 'polka'; import polka from 'polka';
import * as sapper from '../__sapper__/server.js'; import * as sapper from '@sapper/server';
const { PORT } = process.env; const { PORT } = process.env;

View File

@@ -1,10 +1,10 @@
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js'; import * as sapper from '@sapper/service-worker';
const ASSETS = `cache${timestamp}`; const ASSETS = `cache${sapper.timestamp}`;
// `shell` is an array of all the files generated by webpack, // `shell` is an array of all the files generated by webpack,
// `files` is an array of everything in the `static` directory // `files` is an array of everything in the `static` directory
const to_cache = shell.concat(ASSETS); const to_cache = sapper.shell.concat(sapper.files);
const cached = new Set(to_cache); const cached = new Set(to_cache);
self.addEventListener('install', event => { self.addEventListener('install', event => {
@@ -65,7 +65,7 @@ self.addEventListener('fetch', event => {
// might prefer a cache-first approach to a network-first one.) // might prefer a cache-first approach to a network-first one.)
event.respondWith( event.respondWith(
caches caches
.open(`offline${timestamp}`) .open(`offline${sapper.timestamp}`)
.then(async cache => { .then(async cache => {
try { try {
const response = await fetch(event.request); const response = await fetch(event.request);

View File

@@ -35,11 +35,11 @@ describe('encoding', function() {
}); });
it('encodes req.params and req.query for server-rendered pages', async () => { it('encodes req.params and req.query for server-rendered pages', async () => {
await page.goto(`${base}/echo/page/encöded?message=hëllö+wörld`); await page.goto(`${base}/echo/page/encöded?message=hëllö+wörld&föo=bar&=baz`);
assert.equal( assert.equal(
await page.$eval('h1', node => node.textContent), await page.$eval('h1', node => node.textContent),
'encöded (hëllö wörld)' 'encöded {"message":"hëllö wörld","föo":"bar","":"baz"}'
); );
}); });
@@ -48,12 +48,12 @@ describe('encoding', function() {
await start(); await start();
await prefetchRoutes(); await prefetchRoutes();
await page.click('a[href="echo/page/encöded?message=hëllö+wörld"]'); await page.click('a[href="echo/page/encöded?message=hëllö+wörld&föo=bar&=baz"]');
await wait(50); await wait(50);
assert.equal( assert.equal(
await page.$eval('h1', node => node.textContent), await page.$eval('h1', node => node.textContent),
'encöded (hëllö wörld)' 'encöded {"message":"hëllö wörld","föo":"bar","":"baz"}'
); );
}); });

View File

@@ -22,10 +22,7 @@ export default {
emitCss: true emitCss: true
}), }),
resolve() resolve()
], ]
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
}, },
server: { server: {
@@ -44,10 +41,7 @@ export default {
preferBuiltins: true preferBuiltins: true
}) })
], ],
external: ['sirv', 'polka'], external: ['sirv', 'polka']
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
}, },
serviceworker: { serviceworker: {

View File

@@ -1,4 +1,4 @@
import * as sapper from '../__sapper__/client.js'; import * as sapper from '@sapper/app';
window.start = () => sapper.start({ window.start = () => sapper.start({
target: document.querySelector('#sapper') target: document.querySelector('#sapper')

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