Compare commits

..

130 Commits

Author SHA1 Message Date
Rich Harris
4843e9a40a -> v0.11.0 2018-05-03 23:51:04 -04:00
Rich Harris
ca4a1ca9b0 Merge pull request #251 from sveltejs/client-info
only save the bits of client_info we need
2018-05-03 23:49:30 -04:00
Rich Harris
ad7c872ee3 Merge pull request #250 from sveltejs/gh-240
implement --launcher
2018-05-03 23:49:06 -04:00
Rich Harris
4f98324a8a oops, missed one 2018-05-03 23:46:41 -04:00
Rich Harris
1fcf3f79ee only save the bits of client_info we need 2018-05-03 23:42:19 -04:00
Rich Harris
0b5741194a Merge pull request #205 from sveltejs/gh-140
prefetch on mouse stop
2018-05-03 23:31:08 -04:00
Rich Harris
9653d4c6ce Merge pull request #249 from sveltejs/gh-241
allow process.env.NODE_ENV to be overridden when building
2018-05-03 23:30:55 -04:00
Rich Harris
4fa5ed5e2c simplify 2018-05-03 23:30:09 -04:00
Rich Harris
f4eac2515f fix tests 2018-05-03 23:23:02 -04:00
Rich Harris
1a5364ae9d on second thoughts, default to build/index.js 2018-05-03 23:16:56 -04:00
Rich Harris
d7a9074c69 implement --launcher 2018-05-03 23:04:05 -04:00
Rich Harris
00adb53802 allow process.env.NODE_ENV to be overridden when building (#241) 2018-05-03 22:21:00 -04:00
Rich Harris
b10edddc96 cheat 2018-05-03 22:01:14 -04:00
Rich Harris
93b2d12438 Merge branch 'master' into gh-140 2018-05-03 21:54:52 -04:00
Rich Harris
7303e811be update tests, move test app to v2 2018-05-03 21:54:23 -04:00
Rich Harris
3531cc587d -> v0.10.7 2018-05-03 21:42:50 -04:00
Rich Harris
562a91fa57 Merge pull request #245 from johnmuhl/patch-1
Include process.env in exporter server options
2018-05-03 21:40:50 -04:00
Rich Harris
93128a0156 Merge pull request #243 from akihikodaki/dot
Accept directory entries which starts with dot as routes
2018-05-03 21:39:46 -04:00
Rich Harris
d7a2132966 Merge pull request #234 from akihikodaki/master
Do not encode characters allowed in path when generating routes
2018-05-03 21:37:59 -04:00
John Muhl
56ac1aea9d match sapper start 2018-04-22 22:50:05 -05:00
John Muhl
37a9fb62e2 Include process.env in exporter server options 2018-04-22 20:29:47 -05:00
Rich Harris
a70e88b1f4 -> v0.10.6 2018-04-19 13:03:12 -04:00
Akihiko Odaki
6f9ce9ce85 Accept directory entries which starts with dot as routes
It allows to implement .well-known URIs.
2018-04-19 22:04:03 +09:00
Akihiko Odaki
b13cc6f39a Do not encode characters allowed in path when generating routes
In RFC 3986, some characters not allowed in query, which encodeURIComponent
is designed for, is allowed in path.
A notable example is "@", which is commonly included in paths of social
profile pages. Such characters should not be encoded.

The new encoding function is conforming to the RFC.
2018-04-14 01:08:27 +09:00
Rich-Harris
2758382c68 -> v0.10.5 2018-04-06 18:32:44 -07:00
Rich Harris
dd7f1ff99c Merge pull request #231 from sveltejs/fix-missing-service-worker
fix missing service worker
2018-04-06 21:31:53 -04:00
Rich-Harris
45142cd037 fix missing service worker 2018-04-06 14:44:50 -07:00
Rich-Harris
ceb1caf1de -> v0.10.4 2018-04-03 21:43:30 -04:00
Rich Harris
7e263a3076 Merge pull request #227 from naturalethic/upgrade-chokidar-disable-globbing-issue-212
Upgrade chokidar disable globbing issue 212
2018-04-03 21:42:34 -04:00
Rich Harris
ec88d4a430 Remove unnecessary globbing pattern 2018-04-03 21:38:28 -04:00
Joshua Kifer
909ea72108 Update dev.ts 2018-04-03 14:04:08 -07:00
Joshua Kifer
cd09d75d99 Merge branch 'master' into upgrade-chokidar-disable-globbing-issue-212 2018-04-03 13:00:07 -07:00
Joshua Kifer
0e3abe489a Re-upgrade chokidar, disable globbing 2018-04-03 12:58:06 -07:00
Joshua Kifer
a5d141d2f1 Update (#1)
* Downgrade chokidar to 1.7.0

* -> v0.10.3
2018-04-03 12:49:55 -07:00
Rich-Harris
87eae6164b -> v0.10.3 2018-04-02 15:39:34 -04:00
Rich Harris
97e00f5a9c Merge pull request #226 from naturalethic/downgrade-chokidar
Downgrade chokidar to 1.7.0
2018-04-02 15:38:40 -04:00
Joshua Kifer
bd55558b5e Downgrade chokidar to 1.7.0 2018-04-02 12:24:57 -07:00
Rich-Harris
25dc4b3a4c -> v0.10.2 2018-03-25 15:20:48 -04:00
Rich Harris
72c27b78a3 Merge pull request #215 from sveltejs/stable-sort
Stable sort
2018-03-25 15:19:43 -04:00
Rich-Harris
25809ec409 enforce stable sort 2018-03-25 15:12:35 -04:00
Rich-Harris
3220c522d7 attach store to error pages 2018-03-20 16:08:23 -04:00
Rich Harris
d5d25f1d30 -> v0.10.1 2018-03-18 22:47:11 -04:00
Rich Harris
7ccd6ba329 Merge pull request #207 from sveltejs/fix-fetch-paths
fix server-side fetch paths
2018-03-18 22:41:01 -04:00
Rich Harris
35c30ae2c5 fix server-side fetch paths 2018-03-18 22:36:55 -04:00
Rich Harris
2c61f6d396 -> v0.10.0 2018-03-18 22:17:53 -04:00
Rich Harris
86233a8eab Merge pull request #206 from sveltejs/sapper-base-error
app/template.html must have %sapper.base%
2018-03-18 22:13:24 -04:00
Rich Harris
c140b128ee expect %sapper.base% 2018-03-18 22:06:11 -04:00
Rich Harris
a6b1527fd3 try using mousemove in tests 2018-03-18 21:53:13 -04:00
Rich Harris
c2f3a2aac0 prefetch on mouse stop (#140) 2018-03-18 21:41:47 -04:00
Rich Harris
66ac9773c0 Merge pull request #204 from nolanlawson/ignore-source-maps
Add support for sourcemap *.map files
2018-03-18 21:23:26 -04:00
Rich Harris
e60714bb98 Merge pull request #203 from sveltejs/gh-178-fetch
implement this.fetch
2018-03-18 21:21:00 -04:00
Nolan Lawson
52dfd6e939 Don't preload .map files 2018-03-18 16:31:53 -07:00
Nolan Lawson
fc2312eba6 Add full *.map file support 2018-03-18 16:17:41 -07:00
Nolan Lawson
cf90476255 Ignore source map files in %sapper.scripts% 2018-03-18 11:13:24 -07:00
Rich Harris
1e8d7d10ab implement this.fetch (#178) 2018-03-17 19:21:25 -04:00
Rich Harris
cf6621b83c Merge pull request #202 from sveltejs/gh-178-store
add server- and client-side store management (#178)
2018-03-17 16:10:48 -04:00
Rich Harris
9812cbd71c add server- and client-side store management (#178) 2018-03-17 13:45:59 -04:00
Rich Harris
67a81a3cac Merge pull request #201 from sveltejs/simplify-tests
simplify tests
2018-03-17 13:24:42 -04:00
Rich Harris
67463683cc simplify tests 2018-03-17 13:18:18 -04:00
Rich Harris
b94481b716 Support being mounted on a path — fixes #180 2018-03-17 11:55:02 -04:00
Rich Harris
a95ddee48d add protocol to startup message 2018-03-16 10:51:19 -04:00
Rich Harris
953694f77f update deps 2018-03-16 10:51:04 -04:00
Rich Harris
2f24cb0429 -> v0.9.6 2018-03-11 22:01:06 -04:00
Rich Harris
687071902d -> v0.9.5 2018-03-11 21:30:17 -04:00
Rich Harris
cd3fcfdf3c move deps to devDeps 2018-03-11 21:29:34 -04:00
Rich Harris
dad48e4abd Merge pull request #197 from sveltejs/fix-clorox
workaround clorox bug
2018-03-11 21:29:12 -04:00
Rich Harris
37d3d57694 workaround clorox bug 2018-03-11 20:51:14 -04:00
Rich Harris
9a5d273590 -> v0.9.4 2018-03-11 19:00:39 -04:00
Rich Harris
3816fe71ad Merge pull request #196 from sveltejs/gh-186
implement --open
2018-03-11 16:52:41 -04:00
Rich Harris
69f5b9cac7 implement --open - fixes #186 2018-03-11 16:01:13 -04:00
Rich Harris
ad14320dc3 Merge pull request #195 from sveltejs/export-logs
show which files are being exported
2018-03-11 15:50:13 -04:00
Rich Harris
43563bd8e5 show which files are being exported 2018-03-11 15:44:53 -04:00
Rich Harris
02d558b97c Merge pull request #194 from sveltejs/gh-172
minify HTML on export
2018-03-11 15:19:07 -04:00
Rich Harris
866286c95e minify HTML on export 2018-03-11 14:57:10 -04:00
Rich Harris
e1b5e336dc Merge pull request #193 from sveltejs/gh-15
minify HTML templates
2018-03-11 14:14:39 -04:00
Rich Harris
1d71b86c0f remove all hard-coded locations (#181) 2018-03-11 13:43:11 -04:00
Rich Harris
bdc248f09a minify HTML at build time (also fixes #181) 2018-03-11 13:35:29 -04:00
Rich Harris
be63ea7c96 add SAPPER_BASE and SAPPER_APP environment variables 2018-03-11 13:29:39 -04:00
Rich Harris
819ec0b776 minify HTML templates - fixes #15 2018-03-11 13:10:43 -04:00
Rich Harris
d22d37fb18 Merge pull request #192 from sveltejs/misc-tidy-up
a few small tweaks
2018-03-11 13:10:15 -04:00
Rich Harris
8ec433581a a few small tweaks 2018-03-11 12:54:35 -04:00
Rich Harris
0d0e4d664e -> v0.9.3 2018-03-10 23:32:06 -05:00
Rich Harris
4348fad16d -> v0.9.2 2018-03-10 23:29:41 -05:00
Rich Harris
4314897a78 -> v0.9.1 2018-03-10 23:28:14 -05:00
Rich Harris
b1c57466c0 -> v0.9.0 2018-03-10 23:24:03 -05:00
Rich Harris
ef55fc5ddd move built files to dist 2018-03-10 23:20:11 -05:00
Rich Harris
e011fce935 Merge pull request #191 from sveltejs/gh-173
use code-splitting etc
2018-03-10 23:14:53 -05:00
Rich Harris
ba3d9c85c5 clorox seems to have a bug with inverse (TODO investigate) 2018-03-10 23:09:02 -05:00
Rich Harris
cddd7adaad lazy-load stuff 2018-03-10 23:00:36 -05:00
Rich Harris
d8412f33ba ignore some files 2018-03-10 22:33:33 -05:00
Rich Harris
254e41b11e use code-splitting etc 2018-03-10 22:26:53 -05:00
Rich Harris
491c5e3b92 Merge pull request #190 from sveltejs/gh-57
allow server routes to be .ts files (or anything else)
2018-03-10 20:46:25 -05:00
Rich Harris
4441ceb91d Merge pull request #189 from sveltejs/introduce-spelling-error
use wrong spelling of "grey"
2018-03-10 20:45:56 -05:00
Rich Harris
77e418cd21 Merge pull request #188 from sveltejs/gh-112
use devalue instead of serialize-javascript
2018-03-10 20:45:39 -05:00
Rich Harris
4171786953 ensure directories are not mistaken for routes 2018-03-10 20:38:48 -05:00
Rich Harris
5f7cbadd8d remove extensions from entry points 2018-03-10 20:16:46 -05:00
Rich Harris
9bac32eea4 allow server routes to be .ts files (or anything else) - fixes #57 2018-03-10 20:08:23 -05:00
Rich Harris
3a19657ad9 use wrong spelling of "grey" 2018-03-10 20:04:44 -05:00
Rich Harris
d1b6d029e9 merge master -> gh-112 2018-03-10 19:36:08 -05:00
Rich Harris
45b1147228 use devalue instead of serialize-javascript - fixes #112 2018-03-10 19:27:04 -05:00
Rich Harris
c827fda703 Merge pull request #187 from lukeed/fix/chalk
Replace Chalk with Clorox
2018-03-10 16:11:35 -05:00
Luke Edwards
dd39909371 replace chalk with clorox 2018-03-10 12:30:36 -08:00
Rich Harris
fb24c862f3 Merge pull request #185 from sveltejs/gh-165
use window.location.hostname in dev client
2018-03-10 10:44:48 -05:00
Rich Harris
542115f82e Merge pull request #184 from sveltejs/gh-169-b
Fix hard-coded port
2018-03-10 10:28:21 -05:00
Rich Harris
61000a4795 use window.location.hostname in dev client - fixes #165 2018-03-10 10:26:00 -05:00
Rich Harris
7f98d50e15 treat PORT=xxxx the same as --port xxxx 2018-03-10 10:09:18 -05:00
Rich Harris
c580259c07 dont hardcode port 2018-03-10 10:01:42 -05:00
Rich Harris
e8f3aff0da Merge pull request #183 from sveltejs/use-polka
Use polka, remove unused dependencies
2018-03-10 09:54:22 -05:00
Rich Harris
c82031a8e5 remove some unused deps 2018-03-10 09:48:52 -05:00
Rich Harris
1eed1023aa switch to polka, remove unused deps 2018-03-10 09:39:07 -05:00
Rich Harris
c1a2d93da6 Merge pull request #182 from sveltejs/gh-177
kill child process if webpack crashes
2018-03-10 08:51:05 -05:00
Rich Harris
504654b58e kill child process if webpack crashes - fixes #177 2018-03-09 21:02:05 -05:00
Rich Harris
b1067103a4 -> v0.8.4 2018-03-07 15:57:46 -05:00
Rich Harris
06af8e87da Merge pull request #175 from sveltejs/fix-route-sorting
fix route sorting
2018-03-07 14:32:56 -05:00
Rich Harris
8bb0999878 fix route sorting 2018-03-07 14:26:30 -05:00
Rich Harris
b5a8d29c37 -> v0.8.3 2018-03-05 15:27:38 -05:00
Rich Harris
5925636b16 Merge pull request #170 from sveltejs/gh-169
better CLI
2018-03-05 15:25:33 -05:00
Rich Harris
bc232007c3 simplify sade initialisation 2018-03-05 15:17:34 -05:00
Rich Harris
ffaacb4c99 use fs.existsSync 2018-03-05 15:11:44 -05:00
Rich Harris
47b50f2c0e admin 2018-03-05 15:09:59 -05:00
Rich Harris
a66ac00d42 tidy up tests 2018-03-05 15:09:51 -05:00
Rich Harris
0f8c04b03d use port-authority 2018-03-05 15:09:33 -05:00
Rich Harris
d9d93f41c4 add get-port back as dev dependency, for testing 2018-03-05 13:57:30 -05:00
Rich Harris
5289fc11d8 better CLI, more port control. fixes #169 2018-03-05 13:51:11 -05:00
Rich Harris
dd6c51567a -> v0.8.2 2018-03-04 22:55:00 -05:00
Rich Harris
01ff84f241 Merge pull request #167 from sveltejs/gh-166
rename preloadRoutes to prefetchRoutes
2018-03-04 22:54:11 -05:00
Rich Harris
329c113723 rename preloadRoutes to prefetchRoutes 2018-03-04 22:38:55 -05:00
Rich Harris
2ad10b380f -> v0.8.1 2018-03-04 21:37:55 -05:00
Rich Harris
e6314cde96 Merge pull request #164 from sveltejs/gh-163
add sapper start task
2018-03-04 21:36:51 -05:00
Rich Harris
b64e25a177 add sapper start task 2018-03-04 21:28:31 -05:00
59 changed files with 1291 additions and 6868 deletions

15
.gitignore vendored
View File

@@ -1,18 +1,13 @@
.DS_Store
yarn.lock
yarn-error.log
node_modules
cypress/screenshots
test/app/.sapper
test/app/app/manifest
test/app/export
test/app/build
sapper
runtime.js
runtime.js.map
cli.js
cli.js.map
middleware.js
middleware.js.map
core.js
core.js.map
webpack/config.js
webpack/config.js.map
yarn-error.log
dist
!rollup.config.js

View File

@@ -1,5 +1,106 @@
# sapper changelog
## 0.11.0
* Create launcher file ([#240](https://github.com/sveltejs/sapper/issues/240))
* Only keep necessary parts of webpack stats ([#251](https://github.com/sveltejs/sapper/pull/251))
* Allow `NODE_ENV` to be overridden when building ([#241](https://github.com/sveltejs/sapper/issues/241))
## 0.10.7
* Allow routes to have a leading `.` ([#243](https://github.com/sveltejs/sapper/pull/243))
* Only encode necessary characters in routes ([#234](https://github.com/sveltejs/sapper/pull/234))
* Preserve existing `process.env` when exporting ([#245](https://github.com/sveltejs/sapper/pull/245))
## 0.10.6
* Fix error reporting in `sapper start`
## 0.10.5
* Fix missing service worker ([#231](https://github.com/sveltejs/sapper/pull/231))
## 0.10.4
* Upgrade chokidar, this time with a fix ([#227](https://github.com/sveltejs/sapper/pull/227))
## 0.10.3
* Downgrade chokidar ([#212](https://github.com/sveltejs/sapper/issues/212))
## 0.10.2
* Attach `store` to error pages
* Fix sorting edge case ([#215](https://github.com/sveltejs/sapper/pull/215))
## 0.10.1
* Fix server-side `fetch` paths ([#207](https://github.com/sveltejs/sapper/pull/207))
## 0.10.0
* Support mounting on a path (this requires `app/template.html` to include `%sapper.base%`) ([#180](https://github.com/sveltejs/sapper/issues/180))
* Support per-request server-side `Store` with client-side hydration ([#178](https://github.com/sveltejs/sapper/issues/178))
* Add `this.fetch` to `preload`, with credentials support ([#178](https://github.com/sveltejs/sapper/issues/178))
* Exclude sourcemaps from preload links and `<script>` block ([#204](https://github.com/sveltejs/sapper/pull/204))
* Register service worker in `<script>` block
## 0.9.6
* Whoops — `tslib` is a runtime dependency
## 0.9.5
* Stringify clorox output ([#197](https://github.com/sveltejs/sapper/pull/197))
## 0.9.4
* Add `SAPPER_BASE` and `SAPPER_APP` environment variables ([#181](https://github.com/sveltejs/sapper/issues/181))
* Minify template in `sapper build` ([#15](https://github.com/sveltejs/sapper/issues/15))
* Minify all HTML files in `sapper export` ([#172](https://github.com/sveltejs/sapper/issues/172))
* Log exported files ([#195](https://github.com/sveltejs/sapper/pull/195))
* Add `--open`/`-o` flag to `sapper dev` and `sapper start` ([#186](https://github.com/sveltejs/sapper/issues/186))
## 0.9.3
* Fix path to `sapper-dev-client`
## 0.9.2
* Include `dist` files in package
## 0.9.1
* Include `sapper` bin
## 0.9.0
* Use `devalue` instead of `serialize-javascript`, allowing `preload` to return non-POJOs and cyclical/repeated references, but *not* functions ([#112](https://github.com/sveltejs/sapper/issues/112))
* Kill child process if webpack crashes ([#177](https://github.com/sveltejs/sapper/issues/177))
* Support HMR on remote devices ([#165](https://github.com/sveltejs/sapper/issues/165))
* Remove hard-coded port (([#169](https://github.com/sveltejs/sapper/issues/169)))
* Allow non-JS files, e.g. TypeScript to be used as entry points and server routes ([#57](https://github.com/sveltejs/sapper/issues/57))
* Faster startup ([#173](https://github.com/sveltejs/sapper/issues/173))
## 0.8.4
* Fix route sorting ([#175](https://github.com/sveltejs/sapper/pull/175))
## 0.8.3
* Automatically select available port, or use `--port` flag for `dev` and `start` ([#169](https://github.com/sveltejs/sapper/issues/169))
* Show stats after build/export ([#168](https://github.com/sveltejs/sapper/issues/168))
* Various CLI improvements ([#170](https://github.com/sveltejs/sapper/pull/170))
## 0.8.2
* Rename `preloadRoutes` to `prefetchRoutes` ([#166](https://github.com/sveltejs/sapper/issues/166))
## 0.8.1
* Add `sapper start` command, for running an app built with `sapper build` ([#163](https://github.com/sveltejs/sapper/issues/163))
## 0.8.0
* Update to webpack 4

View File

@@ -1,71 +1,69 @@
{
"name": "sapper",
"version": "0.8.0",
"version": "0.11.0",
"description": "Military-grade apps, engineered by Svelte",
"main": "middleware.js",
"main": "dist/middleware.ts.js",
"bin": {
"sapper": "cli.js"
"sapper": "./sapper"
},
"files": [
"cli.js",
"core.js",
"middleware.js",
"*.js",
"*.ts.js",
"runtime",
"runtime.js",
"sapper-dev-client.js",
"webpack"
"webpack",
"sapper",
"dist"
],
"directories": {
"test": "test"
},
"dependencies": {
"chalk": "^2.3.0",
"cheerio": "^1.0.0-rc.2",
"chokidar": "^1.7.0",
"code-frame": "^5.0.0",
"escape-html": "^1.0.3",
"express": "^4.16.2",
"get-port": "^3.2.0",
"chokidar": "^2.0.3",
"clorox": "^1.0.3",
"cookie": "^0.3.1",
"devalue": "^1.0.1",
"glob": "^7.1.2",
"locate-character": "^2.0.5",
"html-minifier": "^3.5.11",
"mkdirp": "^0.5.1",
"mri": "^1.1.0",
"node-fetch": "^1.7.3",
"node-fetch": "^2.1.1",
"port-authority": "^1.0.2",
"pretty-bytes": "^4.0.2",
"pretty-ms": "^3.1.0",
"relative": "^3.0.2",
"require-relative": "^0.8.7",
"rimraf": "^2.6.2",
"sade": "^1.4.0",
"sander": "^0.6.0",
"serialize-javascript": "^1.4.0",
"source-map-support": "^0.5.3",
"tslib": "^1.8.1",
"source-map-support": "^0.5.5",
"tslib": "^1.9.0",
"url-parse": "^1.2.0",
"walk-sync": "^0.3.2",
"webpack-format-messages": "^1.0.1"
"webpack-format-messages": "^1.0.2"
},
"devDependencies": {
"@std/esm": "^0.19.7",
"@std/esm": "^0.26.0",
"@types/glob": "^5.0.34",
"@types/mkdirp": "^0.5.2",
"@types/rimraf": "^2.0.2",
"compression": "^1.7.1",
"css-loader": "^0.28.7",
"electron": "^1.8.2",
"eslint": "^4.13.1",
"eslint-plugin-import": "^2.8.0",
"mocha": "^4.0.1",
"nightmare": "^2.10.0",
"express": "^4.16.3",
"get-port": "^3.2.0",
"mocha": "^5.0.4",
"nightmare": "^3.0.0",
"npm-run-all": "^4.1.2",
"rollup": "^0.53.0",
"rollup-plugin-commonjs": "^8.3.0",
"polka": "^0.3.4",
"rollup": "^0.58.2",
"rollup-plugin-commonjs": "^9.1.3",
"rollup-plugin-json": "^2.3.0",
"rollup-plugin-string": "^2.0.2",
"rollup-plugin-typescript": "^0.8.1",
"style-loader": "^0.19.1",
"svelte": "^1.49.1",
"svelte-loader": "^2.3.2",
"ts-node": "^4.1.0",
"typescript": "^2.6.2",
"serve-static": "^1.13.2",
"svelte": "^2.4.4",
"svelte-loader": "^2.9.0",
"ts-node": "^6.0.2",
"typescript": "^2.8.3",
"walk-sync": "^0.3.2",
"webpack": "^4.1.0"
},
"scripts": {

View File

@@ -10,36 +10,39 @@ const external = [].concat(
'sapper/core.js'
);
const paths = {
'sapper/core.js': './core.js'
};
const plugins = [
string({
include: '**/*.md'
}),
json(),
commonjs(),
typescript({
typescript: require('typescript')
})
];
export default [
{ name: 'cli', banner: true },
{ name: 'core' },
{ name: 'middleware' },
{ name: 'runtime', format: 'es' },
{ name: 'webpack', file: 'webpack/config' }
].map(obj => ({
input: `src/${obj.name}/index.ts`,
output: {
file: `${obj.file || obj.name}.js`,
format: obj.format || 'cjs',
banner: obj.banner && '#!/usr/bin/env node',
paths,
sourcemap: true
{
input: `src/runtime/index.ts`,
output: {
file: `runtime.js`,
format: 'es'
},
plugins: [
typescript({
typescript: require('typescript')
})
]
},
external,
plugins
}));
{
input: [`src/cli.ts`, `src/core.ts`, `src/middleware.ts`, `src/webpack.ts`],
output: {
dir: 'dist',
format: 'cjs',
sourcemap: true
},
external,
plugins: [
string({
include: '**/*.md'
}),
json(),
commonjs(),
typescript({
typescript: require('typescript')
})
],
experimentalCodeSplitting: true,
experimentalDynamicImport: true
}
];

2
sapper Executable file
View File

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

View File

@@ -11,7 +11,7 @@ function check() {
export function connect(port) {
if (source || !window.EventSource) return;
source = new EventSource(`http://localhost:${port}/__sapper__`);
source = new EventSource(`http://${window.location.hostname}:${port}/__sapper__`);
window.source = source;

95
src/cli.ts Executable file
View File

@@ -0,0 +1,95 @@
import * as fs from 'fs';
import * as path from 'path';
import * as child_process from 'child_process';
import sade from 'sade';
import * as clorox from 'clorox';
import prettyMs from 'pretty-ms';
// import upgrade from './cli/upgrade';
import * as ports from 'port-authority';
import * as pkg from '../package.json';
const prog = sade('sapper').version(pkg.version);
prog.command('dev')
.describe('Start a development server')
.option('-p, --port', 'Specify a port')
.option('-o, --open', 'Open a browser window')
.action(async (opts: { port: number, open: boolean }) => {
const { dev } = await import('./cli/dev');
dev(opts);
});
prog.command('build [dest]')
.describe('Create a production-ready version of your app')
.option('-p, --port', 'Default of process.env.PORT', '3000')
.example(`build custom-dir -p 4567`)
.action(async (dest = 'build', opts: { port: string }) => {
console.log(`> Building...`);
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
process.env.SAPPER_DEST = dest;
const start = Date.now();
try {
const { build } = await import('./cli/build');
await build();
const launcher = path.resolve(dest, 'index.js');
fs.writeFileSync(launcher, `
// generated by sapper build at ${new Date().toISOString()}
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
process.env.SAPPER_DEST = __dirname;
process.env.PORT = process.env.PORT || ${opts.port || 3000};
console.log('Starting server on port ' + process.env.PORT);
require('./server.js');
`.replace(/^\t+/gm, '').trim());
console.error(`\n> Finished in ${elapsed(start)}. Type ${clorox.bold.cyan(`node ${dest}`)} to run the app.`);
} catch (err) {
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
}
});
prog.command('start [dir]')
.describe('Start your app')
.option('-p, --port', 'Specify a port')
.option('-o, --open', 'Open a browser window')
.action(async (dir = 'build', opts: { port: number, open: boolean }) => {
const { start } = await import('./cli/start');
start(dir, opts);
});
prog.command('export [dest]')
.describe('Export your app as static files (if possible)')
.option('--basepath', 'Specify a base path')
.action(async (dest = 'export', opts: { basepath?: string }) => {
console.log(`> Building...`);
process.env.NODE_ENV = 'production';
process.env.SAPPER_DEST = '.sapper/.export';
const start = Date.now();
try {
const { build } = await import('./cli/build');
await build();
console.error(`\n> Built in ${elapsed(start)}. Crawling site...`);
const { exporter } = await import('./cli/export');
await exporter(dest, opts);
console.error(`\n> Finished in ${elapsed(start)}. Type ${clorox.bold.cyan(`npx serve ${dest}`)} to run the app.`);
} catch (err) {
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
}
});
// TODO upgrade
prog.parse(process.argv);
function elapsed(start: number) {
return prettyMs(Date.now() - start);
}

View File

@@ -1,36 +1,59 @@
import * as fs from 'fs';
import * as path from 'path';
import * as clorox from 'clorox';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import { create_compilers, create_app, create_routes, create_serviceworker } from 'sapper/core.js'
import { src, dest, dev } from '../config';
import { minify_html } from './utils/minify_html';
import { create_compilers, create_main_manifests, create_routes, create_serviceworker_manifest } from '../core'
import { locations } from '../config';
export default async function build() {
const output = dest();
export async function build() {
const output = locations.dest();
mkdirp.sync(output);
rimraf.sync(path.join(output, '**/*'));
// minify app/template.html
// TODO compile this to a function? could be quicker than str.replace(...).replace(...).replace(...)
const template = fs.readFileSync(`${locations.app()}/template.html`, 'utf-8');
// remove this in a future version
if (template.indexOf('%sapper.base%') === -1) {
console.log(`${clorox.bold.red(`> As of Sapper v0.10, your template.html file must include %sapper.base% in the <head>`)}`);
process.exit(1);
}
fs.writeFileSync(`${output}/template.html`, minify_html(template));
const routes = create_routes();
// create app/manifest/client.js and app/manifest/server.js
create_app({ routes, src, dev });
create_main_manifests({ routes });
const { client, server, serviceworker } = create_compilers();
const client_stats = await compile(client);
fs.writeFileSync(path.join(output, 'client_info.json'), JSON.stringify(client_stats.toJson()));
console.log(`${clorox.inverse(`\nbuilt client`)}`);
console.log(client_stats.toString({ colors: true }));
fs.writeFileSync(path.join(output, 'client_info.json'), JSON.stringify({
assets: client_stats.toJson().assetsByChunkName
}));
await compile(server);
const server_stats = await compile(server);
console.log(`${clorox.inverse(`\nbuilt server`)}`);
console.log(server_stats.toString({ colors: true }));
let serviceworker_stats;
if (serviceworker) {
create_serviceworker({
create_serviceworker_manifest({
routes,
client_files: client_stats.toJson().assets.map((chunk: { name: string }) => `/client/${chunk.name}`),
src
client_files: client_stats.toJson().assets.map((chunk: { name: string }) => `client/${chunk.name}`)
});
await compile(serviceworker);
serviceworker_stats = await compile(serviceworker);
console.log(`${clorox.inverse(`\nbuilt service worker`)}`);
console.log(serviceworker_stats.toString({ colors: true }));
}
}
@@ -52,4 +75,4 @@ function compile(compiler: any) {
}
});
});
}
}

View File

@@ -1,16 +1,16 @@
import * as fs from 'fs';
import * as path from 'path';
import * as net from 'net';
import * as chalk from 'chalk';
import * as clorox from 'clorox';
import * as child_process from 'child_process';
import * as http from 'http';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import format_messages from 'webpack-format-messages';
import prettyMs from 'pretty-ms';
import { wait_for_port } from './utils';
import { dest } from '../config';
import { create_compilers, create_app, create_routes, create_serviceworker } from 'sapper/core.js';
import * as ports from 'port-authority';
import { locations } from '../config';
import { create_compilers, create_main_manifests, create_routes, create_serviceworker_manifest } from '../core';
type Deferred = {
promise?: Promise<any>;
@@ -70,27 +70,44 @@ function create_hot_update_server(port: number, interval = 10000) {
return { send };
}
export default async function dev() {
export async function dev(opts: { port: number, open: boolean }) {
// remove this in a future version
const template = fs.readFileSync(path.join(locations.app(), 'template.html'), 'utf-8');
if (template.indexOf('%sapper.base%') === -1) {
console.log(`${clorox.bold.red(`> As of Sapper v0.10, your template.html file must include %sapper.base% in the <head>`)}`);
process.exit(1);
}
process.env.NODE_ENV = 'development';
const dir = dest();
let port = opts.port || +process.env.PORT;
if (port) {
if (!await ports.check(port)) {
console.log(`${clorox.bold.red(`> Port ${port} is unavailable`)}`);
return;
}
} else {
port = await ports.find(3000);
}
const dir = locations.dest();
rimraf.sync(dir);
mkdirp.sync(dir);
// initial build
const dev_port = await require('get-port')(10000);
const dev_port = await ports.find(10000);
const routes = create_routes();
create_app({ routes, dev_port });
create_main_manifests({ routes, dev_port });
const hot_update_server = create_hot_update_server(dev_port);
watch_files('routes/**/*', ['add', 'unlink'], () => {
watch_files(locations.routes(), ['add', 'unlink'], () => {
const routes = create_routes();
create_app({ routes, dev_port });
create_main_manifests({ routes, dev_port });
});
watch_files('app/template.html', ['change'], () => {
watch_files(`${locations.app()}/template.html`, ['change'], () => {
hot_update_server.send({
action: 'reload'
});
@@ -98,6 +115,11 @@ export default async function dev() {
let proc: child_process.ChildProcess;
process.on('exit', () => {
// sometimes webpack crashes, so we need to kill our children
if (proc) proc.kill();
});
const deferreds = {
server: deferred(),
client: deferred()
@@ -109,7 +131,7 @@ export default async function dev() {
unique_errors: new Set()
};
function restart_build(filename) {
function restart_build(filename: string) {
if (restarting) return;
restarting = true;
@@ -122,7 +144,7 @@ export default async function dev() {
restarting = false;
});
console.log(`\n${chalk.bold.cyan(path.relative(process.cwd(), filename))} changed. rebuilding...`);
console.log(`\n${clorox.bold.cyan(path.relative(process.cwd(), filename))} changed. rebuilding...`);
}
// TODO watch the configs themselves?
@@ -140,15 +162,15 @@ export default async function dev() {
compiler.watch({}, (err: Error, stats: any) => {
if (err) {
console.error(chalk.red(`${name}`));
console.error(chalk.red(err.message));
console.log(`${clorox.red(`${name}`)}`);
console.log(`${clorox.red(err.message)}`);
error(err);
} else {
const messages = format_messages(stats);
const info = stats.toJson();
if (messages.errors.length > 0) {
console.log(chalk.bold.red(`${name}`));
console.log(`${clorox.bold.red(`${name}`)}`);
const filtered = messages.errors.filter((message: string) => {
return !build.unique_errors.has(message);
@@ -165,7 +187,7 @@ export default async function dev() {
}
} else {
if (messages.warnings.length > 0) {
console.log(chalk.bold.yellow(`${name}`));
console.log(`${clorox.bold.yellow(`${name}`)}`);
const filtered = messages.warnings.filter((message: string) => {
return !build.unique_warnings.has(message);
@@ -181,7 +203,7 @@ export default async function dev() {
console.log(`${hidden} duplicate ${hidden === 1 ? 'warning' : 'warnings'} hidden\n`);
}
} else {
console.log(`${chalk.bold.green(`${name}`)} ${chalk.grey(`(${prettyMs(info.time)})`)}`);
console.log(`${clorox.bold.green(`${name}`)} ${clorox.gray(`(${prettyMs(info.time)})`)}`);
}
result(info);
@@ -206,7 +228,7 @@ export default async function dev() {
deferreds.client.promise.then(() => {
function restart() {
wait_for_port(3000).then(deferreds.server.fulfil); // TODO control port
ports.wait(port).then(deferreds.server.fulfil);
}
if (proc) {
@@ -218,12 +240,16 @@ export default async function dev() {
proc = child_process.fork(`${dir}/server.js`, [], {
cwd: process.cwd(),
env: Object.assign({}, process.env)
env: Object.assign({
PORT: port
}, process.env)
});
});
}
});
let first = true;
watch(compilers.client, {
name: 'client',
@@ -237,18 +263,26 @@ export default async function dev() {
},
result: info => {
fs.writeFileSync(path.join(dir, 'client_info.json'), JSON.stringify(info, null, ' '));
fs.writeFileSync(path.join(dir, 'client_info.json'), JSON.stringify({
assets: info.assetsByChunkName
}, null, ' '));
deferreds.client.fulfil();
const client_files = info.assets.map((chunk: { name: string }) => `/client/${chunk.name}`);
const client_files = info.assets.map((chunk: { name: string }) => `client/${chunk.name}`);
deferreds.server.promise.then(() => {
hot_update_server.send({
status: 'completed'
});
if (first) {
first = false;
console.log(`${clorox.bold.cyan(`> Listening on http://localhost:${port}`)}`);
if (opts.open) child_process.exec(`open http://localhost:${port}`);
}
});
create_serviceworker({
create_serviceworker_manifest({
routes: create_routes(),
client_files
});
@@ -279,10 +313,11 @@ function watch_files(pattern: string, events: string[], callback: () => void) {
const watcher = chokidar.watch(pattern, {
persistent: true,
ignoreInitial: true
ignoreInitial: true,
disableGlobbing: true
});
events.forEach(event => {
watcher.on(event, callback);
});
}
}

View File

@@ -1,21 +1,19 @@
import * as child_process from 'child_process';
import * as path from 'path';
import * as sander from 'sander';
import express from 'express';
import * as clorox from 'clorox';
import cheerio from 'cheerio';
import URL from 'url-parse';
import fetch from 'node-fetch';
import { wait_for_port } from './utils';
import { dest } from '../config';
import * as ports from 'port-authority';
import prettyBytes from 'pretty-bytes';
import { minify_html } from './utils/minify_html';
import { locations } from '../config';
const app = express();
export async function exporter(export_dir: string, { basepath = '' }) {
const build_dir = locations.dest();
function read_json(file: string) {
return JSON.parse(sander.readFileSync(file, { encoding: 'utf-8' }));
}
export default async function exporter(export_dir: string) {
const build_dir = dest();
export_dir = path.join(export_dir, basepath);
// Prep output directory
sander.rimrafSync(export_dir);
@@ -27,17 +25,22 @@ export default async function exporter(export_dir: string) {
sander.copyFileSync(build_dir, 'service-worker.js').to(export_dir, 'service-worker.js');
}
const port = await require('get-port')(3000);
if (sander.existsSync(build_dir, 'service-worker.js.map')) {
sander.copyFileSync(build_dir, 'service-worker.js.map').to(export_dir, 'service-worker.js.map');
}
const port = await ports.find(3000);
const origin = `http://localhost:${port}`;
const proc = child_process.fork(path.resolve(`${build_dir}/server.js`), [], {
cwd: process.cwd(),
env: {
env: Object.assign({
PORT: port,
NODE_ENV: 'production',
SAPPER_DEST: build_dir,
SAPPER_EXPORT: 'true'
}
}, process.env)
});
const seen = new Set();
@@ -46,49 +49,58 @@ export default async function exporter(export_dir: string) {
proc.on('message', message => {
if (!message.__sapper__) return;
const url = new URL(message.url, origin);
let file = new URL(message.url, origin).pathname.slice(1);
let { body } = message;
if (saved.has(url.pathname)) return;
saved.add(url.pathname);
if (saved.has(file)) return;
saved.add(file);
if (message.type === 'text/html') {
const file = `${export_dir}/${url.pathname}/index.html`;
sander.writeFileSync(file, message.body);
} else {
const file = `${export_dir}/${url.pathname}`;
sander.writeFileSync(file, message.body);
const is_html = message.type === 'text/html';
if (is_html) {
file = file === '' ? 'index.html' : `${file}/index.html`;
body = minify_html(body);
}
console.log(`${clorox.bold.cyan(file)} ${clorox.gray(`(${prettyBytes(body.length)})`)}`);
sander.writeFileSync(export_dir, file, body);
});
function handle(url: URL) {
if (url.origin !== origin) return;
async function handle(url: URL) {
const r = await fetch(url.href);
const range = ~~(r.status / 100);
if (seen.has(url.pathname)) return;
seen.add(url.pathname);
if (range >= 4) {
console.log(`${clorox.red(`> Received ${r.status} response when fetching ${url.pathname}`)}`);
return;
}
return fetch(url.href)
.then(r => {
if (r.headers.get('Content-Type') === 'text/html') {
return r.text().then((body: string) => {
const $ = cheerio.load(body);
const hrefs: string[] = [];
if (range === 2) {
if (r.headers.get('Content-Type') === 'text/html') {
const body = await r.text();
const $ = cheerio.load(body);
const urls: URL[] = [];
$('a[href]').each((i: number, $a) => {
hrefs.push($a.attribs.href);
});
const base = new URL($('base').attr('href') || '/', url.href);
return hrefs.reduce((promise, href) => {
return promise.then(() => handle(new URL(href, url.href)));
}, Promise.resolve());
});
$('a[href]').each((i: number, $a) => {
const url = new URL($a.attribs.href, base.href);
if (url.origin === origin && !seen.has(url.pathname)) {
seen.add(url.pathname);
urls.push(url);
}
});
for (const url of urls) {
await handle(url);
}
})
.catch((err: Error) => {
console.error(`Error rendering ${url.pathname}: ${err.message}`);
});
}
}
}
wait_for_port(port)
.then(() => handle(new URL(origin))) // TODO all static routes
return ports.wait(port)
.then(() => handle(new URL(`/${basepath}`, origin))) // TODO all static routes
.then(() => proc.kill());
}
}

View File

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

View File

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

39
src/cli/start.ts Normal file
View File

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

View File

@@ -1,5 +1,5 @@
import * as fs from 'fs';
import chalk from 'chalk';
import * as clorox from 'clorox';
export default async function upgrade() {
const upgraded = [
@@ -27,10 +27,10 @@ async function upgrade_sapper_main() {
if (/\%sapper\.main\%/.test(template)) {
if (!pattern.test(template)) {
console.log(chalk.red(`Could not replace %sapper.main% in ${file}`));
console.log(`${clorox.red(`Could not replace %sapper.main% in ${file}`)}`);
} else {
write(file, template.replace(pattern, `%sapper.scripts%`));
console.log(chalk.green(`Replaced %sapper.main% in ${file}`));
console.log(`${clorox.green(`Replaced %sapper.main% in ${file}`)}`);
replaced = true;
}
}
@@ -50,4 +50,4 @@ function read(file: string) {
function write(file: string, data: string) {
fs.writeFileSync(file, data);
}
}

View File

@@ -1,25 +0,0 @@
import * as net from 'net';
export function wait_for_port(port: number, timeout = 5000) {
return new Promise((fulfil, reject) => {
get_connection(port, fulfil);
setTimeout(() => reject(new Error(`timed out waiting for connection`)), timeout);
});
}
export function get_connection(port: number, cb: () => void) {
const socket = net.createConnection(port, 'localhost', () => {
cb();
socket.destroy();
});
socket.on('error', err => {
setTimeout(() => {
get_connection(port, cb);
}, 10);
});
setTimeout(() => {
socket.destroy();
}, 1000);
}

View File

@@ -0,0 +1,21 @@
import { minify } from 'html-minifier';
export function minify_html(html: string) {
return minify(html, {
collapseBooleanAttributes: true,
collapseWhitespace: true,
conservativeCollapse: true,
decodeEntities: true,
html5: true,
minifyCSS: true,
minifyJS: true,
removeAttributeQuotes: true,
removeComments: true,
removeOptionalTags: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
sortAttributes: true,
sortClassName: true
});
}

View File

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

3
src/core.ts Normal file
View File

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

View File

@@ -1,36 +1,45 @@
import * as fs from 'fs';
import * as path from 'path';
import mkdirp from 'mkdirp';
import * as glob from 'glob';
import create_routes from './create_routes';
import { fudge_mtime, posixify, write } from './utils';
import { dev } from '../config';
import { posixify, write_if_changed } from './utils';
import { dev, locations } from '../config';
import { Route } from '../interfaces';
// in dev mode, we avoid touching the fs unnecessarily
let last_client_manifest: string = null;
let last_server_manifest: string = null;
export default function create_app({ routes, dev_port }: {
export function create_main_manifests({ routes, dev_port }: {
routes: Route[];
dev_port: number;
dev_port?: number;
}) {
mkdirp.sync('app/manifest');
const path_to_routes = path.relative(`${locations.app()}/manifest`, locations.routes());
const client_manifest = generate_client(routes, dev_port);
const server_manifest = generate_server(routes);
const client_manifest = generate_client(routes, path_to_routes, dev_port);
const server_manifest = generate_server(routes, path_to_routes);
if (client_manifest !== last_client_manifest) {
write(`app/manifest/client.js`, client_manifest);
last_client_manifest = client_manifest;
}
if (server_manifest !== last_server_manifest) {
write(`app/manifest/server.js`, server_manifest);
last_server_manifest = server_manifest;
}
write_if_changed(`${locations.app()}/manifest/client.js`, client_manifest);
write_if_changed(`${locations.app()}/manifest/server.js`, server_manifest);
}
function generate_client(routes: Route[], dev_port?: number) {
export function create_serviceworker_manifest({ routes, client_files }: {
routes: Route[];
client_files: string[];
}) {
const assets = glob.sync('**', { cwd: 'assets', nodir: true });
let code = `
// This file is generated by Sapper — do not edit it!
export const timestamp = ${Date.now()};
export const assets = [\n\t${assets.map((x: string) => `"${x}"`).join(',\n\t')}\n];
export const shell = [\n\t${client_files.map((x: string) => `"${x}"`).join(',\n\t')}\n];
export const routes = [\n\t${routes.filter((r: Route) => r.type === 'page' && !/^_[45]xx$/.test(r.id)).map((r: Route) => `{ pattern: ${r.pattern} }`).join(',\n\t')}\n];
`.replace(/^\t\t/gm, '').trim();
write_if_changed(`${locations.app()}/manifest/service-worker.js`, code);
}
function generate_client(routes: Route[], path_to_routes: string, dev_port?: number) {
let code = `
// This file is generated by Sapper — do not edit it!
export const routes = [
@@ -40,7 +49,7 @@ function generate_client(routes: Route[], dev_port?: number) {
return `{ pattern: ${route.pattern}, ignore: true }`;
}
const file = posixify(`../../routes/${route.file}`);
const file = posixify(`${path_to_routes}/${route.file}`);
if (route.id === '_4xx' || route.id === '_5xx') {
return `{ error: '${route.id.slice(1)}', load: () => import(/* webpackChunkName: "${route.id}" */ '${file}') }`;
@@ -57,7 +66,7 @@ function generate_client(routes: Route[], dev_port?: number) {
if (dev()) {
const sapper_dev_client = posixify(
path.resolve(__dirname, 'sapper-dev-client.js')
path.resolve(__dirname, '../sapper-dev-client.js')
);
code += `
@@ -72,12 +81,12 @@ function generate_client(routes: Route[], dev_port?: number) {
return code;
}
function generate_server(routes: Route[]) {
function generate_server(routes: Route[], path_to_routes: string) {
let code = `
// This file is generated by Sapper — do not edit it!
${routes
.map(route => {
const file = posixify(`../../routes/${route.file}`);
const file = posixify(`${path_to_routes}/${route.file}`);
return route.type === 'page'
? `import ${route.id} from '${file}';`
: `import * as ${route.id} from '${file}';`;

View File

@@ -1,9 +1,9 @@
import * as path from 'path';
import glob from 'glob';
import { src } from '../config';
import { locations } from '../config';
import { Route } from '../interfaces';
export default function create_routes({ files } = { files: glob.sync('**/*.+(html|js|mjs)', { cwd: src() }) }) {
export default function create_routes({ files } = { files: glob.sync('**/*.*', { cwd: locations.routes(), dot: true, nodir: true }) }) {
const routes: Route[] = files
.map((file: string) => {
if (/(^|\/|\\)_/.test(file)) return;
@@ -33,7 +33,7 @@ export default function create_routes({ files } = { files: glob.sync('**/*.+(htm
let i = parts.length;
let nested = true;
while (i--) {
const part = encodeURIComponent(parts[i].normalize()).replace(/%5B/g, '[').replace(/%5D/g, ']');
const part = encodeURI(parts[i].normalize()).replace(/\?/g, '%3F').replace(/#/g, '%23').replace(/%5B/g, '[').replace(/%5D/g, ']');
const dynamic = ~part.indexOf('[');
if (dynamic) {
@@ -74,30 +74,27 @@ export default function create_routes({ files } = { files: glob.sync('**/*.+(htm
})
.filter(Boolean)
.sort((a: Route, b: Route) => {
let same = true;
if (a.file === '4xx.html' || a.file === '5xx.html') return -1;
if (b.file === '4xx.html' || b.file === '5xx.html') return 1;
for (let i = 0; true; i += 1) {
const max = Math.max(a.parts.length, b.parts.length);
for (let i = 0; i < max; i += 1) {
const a_part = a.parts[i];
const b_part = b.parts[i];
if (!a_part && !b_part) {
if (same) throw new Error(`The ${a.file} and ${b.file} routes clash`);
return 0;
}
if (!a_part) return -1;
if (!b_part) return 1;
const a_sub_parts = get_sub_parts(a_part);
const b_sub_parts = get_sub_parts(b_part);
const max = Math.max(a_sub_parts.length, b_sub_parts.length);
for (let i = 0; true; i += 1) {
for (let i = 0; i < max; i += 1) {
const a_sub_part = a_sub_parts[i];
const b_sub_part = b_sub_parts[i];
if (!a_sub_part && !b_sub_part) break;
if (!a_sub_part) return 1; // note this is reversed from above — match [foo].json before [foo]
if (!a_sub_part) return 1; // b is more specific, so goes first
if (!b_sub_part) return -1;
if (a_sub_part.dynamic !== b_sub_part.dynamic) {
@@ -105,10 +102,15 @@ export default function create_routes({ files } = { files: glob.sync('**/*.+(htm
}
if (!a_sub_part.dynamic && a_sub_part.content !== b_sub_part.content) {
return b_sub_part.content.length - a_sub_part.content.length;
return (
(b_sub_part.content.length - a_sub_part.content.length) ||
(a_sub_part.content < b_sub_part.content ? -1 : 1)
);
}
}
}
throw new Error(`The ${a.file} and ${b.file} routes clash`);
});
return routes;

View File

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

View File

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

View File

@@ -1,8 +1,13 @@
import * as fs from 'fs';
import * as sander from 'sander';
export function write(file: string, code: string) {
fs.writeFileSync(file, code);
fudge_mtime(file);
const previous_contents = new Map();
export function write_if_changed(file: string, code: string) {
if (code !== previous_contents.get(file)) {
previous_contents.set(file, code);
sander.writeFileSync(file, code);
fudge_mtime(file);
}
}
export function posixify(file: string) {
@@ -11,8 +16,8 @@ export function posixify(file: string) {
export function fudge_mtime(file: string) {
// need to fudge the mtime so that webpack doesn't go doolally
const { atime, mtime } = fs.statSync(file);
fs.utimesSync(
const { atime, mtime } = sander.statSync(file);
sander.utimesSync(
file,
new Date(atime.getTime() - 999999),
new Date(mtime.getTime() - 999999)

View File

@@ -12,4 +12,8 @@ export type Route = {
export type Template = {
render: (data: Record<string, string>) => string;
stream: (req, res, data: Record<string, string | Promise<string>>) => void;
};
export type Store = {
get: () => any;
};

View File

@@ -1,14 +1,16 @@
import * as fs from 'fs';
import * as path from 'path';
import { resolve, URL } from 'url';
import { ClientRequest, ServerResponse } from 'http';
import cookie from 'cookie';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import serialize from 'serialize-javascript';
import escape_html from 'escape-html';
import { lookup } from './mime';
import { create_routes, templates, create_compilers } from 'sapper/core.js';
import { dest, dev } from '../config';
import { Route, Template } from '../interfaces';
import devalue from 'devalue';
import fetch from 'node-fetch';
import { lookup } from './middleware/mime';
import { create_routes, create_compilers } from './core';
import { locations, dev } from './config';
import { Route, Template } from './interfaces';
import sourceMapSupport from 'source-map-support';
sourceMapSupport.install();
@@ -19,7 +21,7 @@ type RouteObject = {
pattern: RegExp;
params: (match: RegExpMatchArray) => Record<string, string>;
module: {
render: (data: any) => {
render: (data: any, opts: { store: Store }) => {
head: string;
css: { code: string, map: any };
html: string
@@ -31,42 +33,64 @@ type RouteObject = {
type Handler = (req: Req, res: ServerResponse, next: () => void) => void;
type Store = {
get: () => any
};
interface Req extends ClientRequest {
url: string;
baseUrl: string;
originalUrl: string;
method: string;
pathname: string;
path: string;
params: Record<string, string>;
headers: Record<string, string>;
}
export default function middleware({ routes }: {
routes: RouteObject[]
export default function middleware({ routes, store }: {
routes: RouteObject[],
store: (req: Req) => Store
}) {
const output = dest();
const output = locations.dest();
const client_info = JSON.parse(fs.readFileSync(path.join(output, 'client_info.json'), 'utf-8'));
const middleware = compose_handlers([
(req: Req, res: ServerResponse, next: () => void) => {
req.pathname = req.url.replace(/\?.*/, '');
if (req.baseUrl === undefined) {
req.baseUrl = req.originalUrl
? req.originalUrl.slice(0, -req.url.length)
: '';
}
if (req.path === undefined) {
req.path = req.url.replace(/\?.*/, '');
}
next();
},
exists(path.join(output, 'index.html')) && serve({
fs.existsSync(path.join(output, 'index.html')) && serve({
pathname: '/index.html',
cache_control: 'max-age=600'
}),
exists(path.join(output, 'service-worker.js')) && serve({
fs.existsSync(path.join(output, 'service-worker.js')) && serve({
pathname: '/service-worker.js',
cache_control: 'max-age=600'
}),
fs.existsSync(path.join(output, 'service-worker.js.map')) && serve({
pathname: '/service-worker.js.map',
cache_control: 'max-age=600'
}),
serve({
prefix: '/client/',
cache_control: 'max-age=31536000'
}),
get_route_handler(client_info.assetsByChunkName, routes)
get_route_handler(client_info.assets, routes, store)
].filter(Boolean));
return middleware;
@@ -78,10 +102,10 @@ function serve({ prefix, pathname, cache_control }: {
cache_control: string
}) {
const filter = pathname
? (req: Req) => req.pathname === pathname
: (req: Req) => req.pathname.startsWith(prefix);
? (req: Req) => req.path === pathname
: (req: Req) => req.path.startsWith(prefix);
const output = dest();
const output = locations.dest();
const cache: Map<string, Buffer> = new Map();
@@ -91,10 +115,10 @@ function serve({ prefix, pathname, cache_control }: {
return (req: Req, res: ServerResponse, next: () => void) => {
if (filter(req)) {
const type = lookup(req.pathname);
const type = lookup(req.path);
try {
const data = read(req.pathname.slice(1));
const data = read(req.path.slice(1));
res.setHeader('Content-Type', type);
res.setHeader('Cache-Control', cache_control);
@@ -111,13 +135,13 @@ function serve({ prefix, pathname, cache_control }: {
const resolved = Promise.resolve();
function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]) {
function get_route_handler(chunks: Record<string, string>, routes: RouteObject[], store_getter: (req: Req) => Store) {
const template = dev()
? () => fs.readFileSync('app/template.html', 'utf-8')
: (str => () => str)(fs.readFileSync('app/template.html', 'utf-8'));
? () => fs.readFileSync(`${locations.app()}/template.html`, 'utf-8')
: (str => () => str)(fs.readFileSync(`${locations.dest()}/template.html`, 'utf-8'));
function handle_route(route: RouteObject, req: Req, res: ServerResponse) {
req.params = route.params(route.pattern.exec(req.pathname));
req.params = route.params(route.pattern.exec(req.path));
const mod = route.module;
@@ -128,11 +152,13 @@ function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]
// TODO detect other stuff we can preload? images, CSS, fonts?
const link = []
.concat(chunks.main, chunks[route.id])
.map(file => `</client/${file}>;rel="preload";as="script"`)
.filter(file => !file.match(/\.map$/))
.map(file => `<${req.baseUrl}/client/${file}>;rel="preload";as="script"`)
.join(', ');
res.setHeader('Link', link);
const store = store_getter ? store_getter(req) : null;
const data = { params: req.params, query: req.query };
let redirect: { statusCode: number, location: string };
@@ -145,14 +171,50 @@ function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]
},
error: (statusCode: number, message: Error | string) => {
error = { statusCode, message };
}
},
fetch: (url: string, opts?: any) => {
const parsed = new URL(url, `http://127.0.0.1:${process.env.PORT}${req.baseUrl ? req.baseUrl + '/' :''}`);
if (opts) {
opts = Object.assign({}, opts);
const include_cookies = (
opts.credentials === 'include' ||
opts.credentials === 'same-origin' && parsed.origin === `http://127.0.0.1:${process.env.PORT}`
);
if (include_cookies) {
const cookies: Record<string, string> = {};
if (!opts.headers) opts.headers = {};
const str = []
.concat(
cookie.parse(req.headers.cookie || ''),
cookie.parse(opts.headers.cookie || ''),
cookie.parse(res.getHeader('Set-Cookie') || '')
)
.map(cookie => {
return Object.keys(cookie)
.map(name => `${name}=${encodeURIComponent(cookie[name])}`)
.join('; ');
})
.filter(Boolean)
.join(', ');
opts.headers.cookie = str;
}
}
return fetch(parsed.href, opts);
},
store
}, req) : {}
).catch(err => {
error = { statusCode: 500, message: err };
}).then(preloaded => {
if (redirect) {
res.statusCode = redirect.statusCode;
res.setHeader('Location', redirect.location);
res.setHeader('Location', `${req.baseUrl}/${redirect.location}`);
res.end();
return;
@@ -163,20 +225,36 @@ function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]
return;
}
const serialized = try_serialize(preloaded); // TODO bail on non-POJOs
const serialized = {
preloaded: mod.preload && try_serialize(preloaded),
store: store && try_serialize(store.get())
};
Object.assign(data, preloaded);
const { html, head, css } = mod.render(data);
const { html, head, css } = mod.render(data, {
store
});
let scripts = []
.concat(chunks.main) // chunks main might be an array. it might not! thanks, webpack
.map(file => `<script src='/client/${file}'></script>`)
.filter(file => !file.match(/\.map$/))
.map(file => `<script src='${req.baseUrl}/client/${file}'></script>`)
.join('');
scripts = `<script>__SAPPER__ = { preloaded: ${serialized} };</script>${scripts}`;
let inline_script = `__SAPPER__={${[
`baseUrl: "${req.baseUrl}"`,
serialized.preloaded && `preloaded: ${serialized.preloaded}`,
serialized.store && `store: ${serialized.store}`
].filter(Boolean).join(',')}};`;
const has_service_worker = fs.existsSync(path.join(locations.dest(), 'service-worker.js'));
if (has_service_worker) {
inline_script += `if ('serviceWorker' in navigator) navigator.serviceWorker.register('${req.baseUrl}/service-worker.js');`;
}
const page = template()
.replace('%sapper.scripts%', scripts)
.replace('%sapper.base%', `<base href="${req.baseUrl}/">`)
.replace('%sapper.scripts%', `<script>${inline_script}</script>${scripts}`)
.replace('%sapper.html%', html)
.replace('%sapper.head%', `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`)
.replace('%sapper.styles%', (css && css.code ? `<style>${css.code}</style>` : ''));
@@ -278,12 +356,15 @@ function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]
const rendered = route ? route.module.render({
status: statusCode,
error
}, {
store: store_getter && store_getter(req)
}) : { head: '', css: null, html: title };
const { head, css, html } = rendered;
const page = template()
.replace('%sapper.scripts%', `<script src='/client/${chunks.main}'></script>`)
.replace('%sapper.base%', `<base href="${req.baseUrl}/">`)
.replace('%sapper.scripts%', `<script>__SAPPER__={baseUrl: "${req.baseUrl}"}</script><script src='${req.baseUrl}/client/${chunks.main}'></script>`)
.replace('%sapper.html%', html)
.replace('%sapper.head%', `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`)
.replace('%sapper.styles%', (css && css.code ? `<style>${css.code}</style>` : ''));
@@ -292,11 +373,9 @@ function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]
}
return function find_route(req: Req, res: ServerResponse) {
const url = req.pathname;
try {
for (const route of routes) {
if (!route.error && route.pattern.test(url)) return handle_route(route, req, res);
if (!route.error && route.pattern.test(req.path)) return handle_route(route, req, res);
}
handle_error(req, res, 404, 'Not found');
@@ -332,17 +411,8 @@ function read_json(file: string) {
function try_serialize(data: any) {
try {
return serialize(data);
return devalue(data);
} catch (err) {
return null;
}
}
function exists(file: string) {
try {
fs.statSync(file);
return true;
} catch (err) {
return false;
}
}

View File

@@ -29,7 +29,7 @@ application/java-archive jar
application/java-serialized-object ser
application/java-vm class
application/javascript js
application/json json
application/json json map
application/jsonml+json jsonml
application/lost+xml lostxml
application/mac-binhex40 hqx

View File

@@ -1,8 +1,11 @@
import { detach, findAnchor, scroll_state, which } from './utils';
import { Component, ComponentConstructor, Params, Query, Route, RouteData, ScrollPosition, Target } from './interfaces';
import { Component, ComponentConstructor, Params, Query, Route, RouteData, ScrollPosition, Store, Target } from './interfaces';
const manifest = typeof window !== 'undefined' && window.__SAPPER__;
export let component: Component;
let target: Node;
let store: Store;
let routes: Route[];
let errors: { '4xx': Route, '5xx': Route };
@@ -22,9 +25,12 @@ if ('scrollRestoration' in history) {
function select_route(url: URL): Target {
if (url.origin !== window.location.origin) return null;
if (!url.pathname.startsWith(manifest.baseUrl)) return null;
const pathname = url.pathname.slice(manifest.baseUrl.length);
for (const route of routes) {
const match = route.pattern.exec(url.pathname);
const match = route.pattern.exec(pathname);
if (match) {
if (route.ignore) return null;
@@ -64,6 +70,7 @@ function render(Component: ComponentConstructor, data: any, scroll: ScrollPositi
component = new Component({
target,
data,
store,
hydrate: !component
});
@@ -80,11 +87,13 @@ function prepare_route(Component: ComponentConstructor, data: RouteData) {
return { Component, data, redirect, error };
}
if (!component && window.__SAPPER__ && window.__SAPPER__.preloaded) {
return { Component, data: Object.assign(data, window.__SAPPER__.preloaded), redirect, error };
if (!component && manifest.preloaded) {
return { Component, data: Object.assign(data, manifest.preloaded), redirect, error };
}
return Promise.resolve(Component.preload.call({
store,
fetch: (url: string, opts?: any) => window.fetch(url, opts),
redirect: (statusCode: number, location: string) => {
redirect = { statusCode, location };
},
@@ -203,9 +212,9 @@ let prefetching: {
} = null;
export function prefetch(href: string) {
const selected = select_route(new URL(href));
const selected = select_route(new URL(href, document.baseURI));
if (selected) {
if (selected && (!prefetching || href !== prefetching.href)) {
prefetching = {
href,
promise: selected.route.load().then(mod => prepare_route(mod.default, selected.data))
@@ -213,7 +222,16 @@ export function prefetch(href: string) {
}
}
function handle_touchstart_mouseover(event: MouseEvent | TouchEvent) {
let mousemove_timeout: NodeJS.Timer;
function handle_mousemove(event: MouseEvent) {
clearTimeout(mousemove_timeout);
mousemove_timeout = setTimeout(() => {
trigger_prefetch(event);
}, 20);
}
function trigger_prefetch(event: MouseEvent | TouchEvent) {
const a: HTMLAnchorElement = <HTMLAnchorElement>findAnchor(<Node>event.target);
if (!a || a.rel !== 'prefetch') return;
@@ -222,7 +240,7 @@ function handle_touchstart_mouseover(event: MouseEvent | TouchEvent) {
let inited: boolean;
export function init(_target: Node, _routes: Route[]) {
export function init(_target: Node, _routes: Route[], opts?: { store?: (data: any) => Store }) {
target = _target;
routes = _routes.filter(r => !r.error);
errors = {
@@ -230,13 +248,17 @@ export function init(_target: Node, _routes: Route[]) {
'5xx': _routes.find(r => r.error === '5xx')
};
if (opts && opts.store) {
store = opts.store(manifest.store);
}
if (!inited) { // this check makes HMR possible
window.addEventListener('click', handle_click);
window.addEventListener('popstate', handle_popstate);
// prefetch
window.addEventListener('touchstart', handle_touchstart_mouseover);
window.addEventListener('mouseover', handle_touchstart_mouseover);
window.addEventListener('touchstart', trigger_prefetch);
window.addEventListener('mousemove', handle_mousemove);
inited = true;
}
@@ -257,7 +279,8 @@ export function init(_target: Node, _routes: Route[]) {
}
export function goto(href: string, opts = { replaceState: false }) {
const target = select_route(new URL(href, window.location.href));
const target = select_route(new URL(href, document.baseURI));
if (target) {
navigate(target, null);
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
@@ -266,7 +289,7 @@ export function goto(href: string, opts = { replaceState: false }) {
}
}
export function preloadRoutes(pathnames: string[]) {
export function prefetchRoutes(pathnames: string[]) {
if (!routes) throw new Error(`You must call init() first`);
return routes
@@ -281,4 +304,7 @@ export function preloadRoutes(pathnames: string[]) {
.reduce((promise: Promise<any>, route) => {
return promise.then(route.load);
}, Promise.resolve());
}
}
// remove this in 0.9
export { prefetchRoutes as preloadRoutes };

View File

@@ -1,9 +1,12 @@
import { Store } from '../interfaces';
export { Store };
export type Params = Record<string, string>;
export type Query = Record<string, string | true>;
export type RouteData = { params: Params, query: Query };
export interface ComponentConstructor {
new (options: { target: Node, data: any, hydrate: boolean }): Component;
new (options: { target: Node, data: any, store: Store, hydrate: boolean }): Component;
preload: (data: { params: Params, query: Query }) => Promise<any>;
};

View File

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

View File

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

View File

@@ -1,8 +1,9 @@
import fs from 'fs';
import { resolve } from 'url';
import express from 'express';
import compression from 'compression';
import serve from 'serve-static';
import sapper from '../../../middleware';
import sapper from '../../../dist/middleware.ts.js';
import { Store } from 'svelte/store.js';
import { routes } from './manifest/server.js';
let pending;
@@ -30,56 +31,74 @@ process.on('message', message => {
const app = express();
app.use((req, res, next) => {
if (pending) pending.add(req.url);
const { write, end } = res;
const chunks = [];
res.write = function(chunk) {
chunks.push(new Buffer(chunk));
write.apply(res, arguments);
};
res.end = function(chunk) {
if (chunk) chunks.push(new Buffer(chunk));
end.apply(res, arguments);
if (pending) pending.delete(req.url);
process.send({
method: req.method,
url: req.url,
status: res.statusCode,
headers: res._headers,
body: Buffer.concat(chunks).toString()
});
if (pending && pending.size === 0 && ended) {
process.send({ type: 'done' });
}
};
next();
});
const { PORT = 3000 } = process.env;
const { PORT = 3000, BASEPATH = '' } = process.env;
const base = `http://localhost:${PORT}${BASEPATH}/`;
// this allows us to do e.g. `fetch('/api/blog')` on the server
const fetch = require('node-fetch');
global.fetch = (url, opts) => {
if (url[0] === '/') url = `http://localhost:${PORT}${url}`;
return fetch(url, opts);
return fetch(resolve(base, url), opts);
};
app.use(compression({ threshold: 0 }));
const middlewares = [
serve('assets'),
app.use(serve('assets'));
// set test cookie
(req, res, next) => {
res.setHeader('Set-Cookie', 'test=woohoo!; Max-Age=3600');
next();
},
app.use(sapper({
routes
}));
// emit messages so we can capture requests
(req, res, next) => {
if (!pending) return next();
app.listen(PORT, () => {
console.log(`listening on port ${PORT}`);
});
pending.add(req.url);
const { write, end } = res;
const chunks = [];
res.write = function(chunk) {
chunks.push(new Buffer(chunk));
write.apply(res, arguments);
};
res.end = function(chunk) {
if (chunk) chunks.push(new Buffer(chunk));
end.apply(res, arguments);
if (pending) pending.delete(req.url);
process.send({
method: req.method,
url: req.url,
status: res.statusCode,
headers: res._headers,
body: Buffer.concat(chunks).toString()
});
if (pending && pending.size === 0 && ended) {
process.send({ type: 'done' });
}
};
next();
},
sapper({
routes,
store: () => {
return new Store({
title: 'Stored title'
});
}
})
];
if (BASEPATH) {
app.use(BASEPATH, ...middlewares);
} else {
app.use(...middlewares);
}
app.listen(PORT);

View File

@@ -5,15 +5,11 @@
<meta name='viewport' content='width=device-width'>
<meta name='theme-color' content='#aa1e1e'>
<link rel='stylesheet' href='/global.css'>
<link rel='manifest' href='/manifest.json'>
<link rel='icon' type='image/png' href='/favicon.png'>
%sapper.base%
<script>
// if ('serviceWorker' in navigator) {
// navigator.serviceWorker.register('/service-worker.js');
// }
</script>
<link rel='stylesheet' href='global.css'>
<link rel='manifest' href='manifest.json'>
<link rel='icon' type='image/png' href='favicon.png'>
<!-- Sapper generates a <style> tag containing critical CSS
for the current page. CSS for the rest of the app is

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +0,0 @@
{
"name": "TODO",
"description": "TODO",
"version": "0.0.1",
"scripts": {
"dev": "node server.js",
"build": "sapper build",
"start": "cross-env NODE_ENV=production node server.js",
"prestart": "npm run build"
},
"dependencies": {
"compression": "^1.7.2",
"cross-env": "^5.1.3",
"css-loader": "^0.28.10",
"express": "^4.16.2",
"extract-text-webpack-plugin": "^3.0.2",
"glob": "^7.1.2",
"marked": "^0.3.17",
"node-fetch": "^1.7.3",
"npm-run-all": "^4.1.2",
"serve-static": "^1.13.2",
"style-loader": "^0.19.0",
"svelte": "^1.56.0",
"svelte-loader": "^2.3.3",
"uglifyjs-webpack-plugin": "^1.2.2",
"webpack": "^4.1.0"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
export function del(req, res) {
res.set({
res.writeHead(200, {
'Content-Type': 'application/json'
});

View File

@@ -8,7 +8,7 @@ const contents = JSON.stringify(posts.map(post => {
}));
export function get(req, res) {
res.set({
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': `max-age=${30 * 60 * 1e3}` // cache for 30 minutes
});

View File

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

View File

@@ -11,7 +11,7 @@ export function get(req, res, next) {
const { slug } = req.params;
if (slug in lookup) {
res.set({
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': `no-cache`
});

View File

@@ -14,7 +14,7 @@ const posts = [
html: `
<p>First, you have to know what <a href='https://svelte.technology'>Svelte</a> is. Svelte is a UI framework with a bold new idea: rather than providing a library that you write code with (like React or Vue, for example), it's a compiler that turns your components into highly optimized vanilla JavaScript. If you haven't already read the <a href='https://svelte.technology/blog/frameworks-without-the-framework'>introductory blog post</a>, you should!</p>
<p>Sapper is a Next.js-style framework (<a href='/blog/how-is-sapper-different-from-next'>more on that here</a>) built around Svelte. It makes it embarrassingly easy to create extremely high performance web apps. Out of the box, you get:</p>
<p>Sapper is a Next.js-style framework (<a href='blog/how-is-sapper-different-from-next'>more on that here</a>) built around Svelte. It makes it embarrassingly easy to create extremely high performance web apps. Out of the box, you get:</p>
<ul>
<li>Code-splitting, dynamic imports and hot module replacement, powered by webpack</li>
@@ -70,8 +70,8 @@ const posts = [
<ul>
<li>It's powered by <a href='https://svelte.technology'>Svelte</a> instead of React, so it's faster and your apps are smaller</li>
<li>Instead of route masking, we encode route parameters in filenames. For example, the page you're looking at right now is <code>routes/blog/[slug].html</code></li>
<li>As well as pages (Svelte components, which render on server or client), you can create <em>server routes</em> in your <code>routes</code> directory. These are just <code>.js</code> files that export functions corresponding to HTTP methods, and receive Express <code>request</code> and <code>response</code> objects as arguments. This makes it very easy to, for example, add a JSON API such as the one <a href='/blog/how-is-sapper-different-from-next.json'>powering this very page</a></li>
<li>Links are just <code>&lt;a&gt;</code> elements, rather than framework-specific <code>&lt;Link&gt;</code> components. That means, for example, that <a href='/blog/how-can-i-get-involved'>this link right here</a>, despite being inside a blob of HTML, works with the router as you'd expect.</li>
<li>As well as pages (Svelte components, which render on server or client), you can create <em>server routes</em> in your <code>routes</code> directory. These are just <code>.js</code> files that export functions corresponding to HTTP methods, and receive Express <code>request</code> and <code>response</code> objects as arguments. This makes it very easy to, for example, add a JSON API such as the one <a href='blog/how-is-sapper-different-from-next.json'>powering this very page</a></li>
<li>Links are just <code>&lt;a&gt;</code> elements, rather than framework-specific <code>&lt;Link&gt;</code> components. That means, for example, that <a href='blog/how-can-i-get-involved'>this link right here</a>, despite being inside a blob of HTML, works with the router as you'd expect.</li>
</ul>
`
},

View File

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

View File

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

View File

@@ -0,0 +1,28 @@
export function get(req, res) {
const cookies = req.headers.cookie
? req.headers.cookie.split(/,\s+/).reduce((cookies, cookie) => {
const [pair] = cookie.split('; ');
const [name, value] = pair.split('=');
cookies[name] = value;
return cookies;
}, {})
: {};
if (cookies.test) {
res.writeHead(200, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({
message: cookies.test
}));
} else {
res.writeHead(403, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({
message: 'unauthorized'
}));
}
}

View File

@@ -4,7 +4,7 @@
export default {
methods: {
del() {
fetch(`/api/delete/42`, { method: 'DELETE' })
fetch(`api/delete/42`, { method: 'DELETE' })
.then(r => r.json())
.then(data => {
window.deleted = data;

View File

@@ -1,60 +1,26 @@
<:Head>
<svelte:head>
<title>Sapper project template</title>
</:Head>
</svelte:head>
<Layout page='home'>
<h1>Great success!</h1>
<h1>Great success!</h1>
<figure>
<img alt='borat' src='/great-success.png'>
<figcaption>HIGH FIVE!</figcaption>
</figure>
<p><strong>Try editing this file (routes/index.html) to test hot module reloading.</strong></p>
</Layout>
<a href='.'>home</a>
<a href='about'>about</a>
<a href='slow-preload'>slow preload</a>
<a href='redirect-from'>redirect</a>
<a href='blog/nope'>broken link</a>
<a href='blog/throw-an-error'>error link</a>
<a href='credentials?creds=include'>credentials</a>
<a rel=prefetch class='{page === "blog" ? "selected" : ""}' href='blog'>blog</a>
<div class='hydrate-test'></div>
<style>
h1, figure, p {
text-align: center;
margin: 0 auto;
}
h1 {
text-align: center;
font-size: 2.8em;
text-transform: uppercase;
font-weight: 700;
margin: 0 0 0.5em 0;
}
figure {
margin: 0 0 1em 0;
}
img {
width: 100%;
max-width: 400px;
margin: 0 0 1em 0;
}
p {
margin: 1em auto;
}
@media (min-width: 480px) {
h1 {
font-size: 4em;
}
}
</style>
<script>
import Layout from './_components/Layout.html';
export default {
components: {
Layout
}
};
</script>
</style>

View File

@@ -0,0 +1,17 @@
<h1>{foo.bar()}</h1>
<script>
export default {
preload() {
class Foo {
bar() {
return 42;
}
}
return {
foo: new Foo()
};
}
};
</script>

View File

@@ -0,0 +1,11 @@
<h1>{set.has('x')}</h1>
<script>
export default {
preload() {
return {
set: new Set(['x'])
};
}
};
</script>

View File

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

View File

@@ -1,4 +1,4 @@
<p>URL is {{url}}</p>
<p>URL is {url}</p>
<script>
export default {

View File

@@ -0,0 +1 @@
<h1>{$title}</h1>

View File

@@ -1,5 +1,4 @@
const config = require('../../../webpack/config.js');
const pkg = require('../package.json');
const sapper_pkg = require('../../../package.json');
module.exports = {
@@ -9,7 +8,10 @@ module.exports = {
resolve: {
extensions: ['.js', '.html']
},
externals: Object.keys(pkg.dependencies).concat(Object.keys(sapper_pkg.dependencies)),
externals: [].concat(
Object.keys(sapper_pkg.dependencies),
Object.keys(sapper_pkg.devDependencies)
),
module: {
rules: [
{

View File

@@ -1,19 +1,22 @@
const fs = require('fs');
const path = require('path');
const assert = require('assert');
const Nightmare = require('nightmare');
const express = require('express');
const serve = require('serve-static');
const walkSync = require('walk-sync');
const fetch = require('node-fetch');
run('production');
run('development');
const rimraf = require('rimraf');
const ports = require('port-authority');
Nightmare.action('page', {
title(done) {
this.evaluate_now(() => document.querySelector('h1').textContent, done);
},
html(done) {
this.evaluate_now(() => document.documentElement.innerHTML, done);
},
text(done) {
this.evaluate_now(() => document.body.textContent, done);
}
@@ -23,44 +26,152 @@ Nightmare.action('init', function(done) {
this.evaluate_now(() => window.init(), done);
});
Nightmare.action('preloadRoutes', function(done) {
this.evaluate_now(() => window.preloadRoutes(), done);
Nightmare.action('prefetchRoutes', function(done) {
this.evaluate_now(() => window.prefetchRoutes(), done);
});
function run(env) {
describe(`env=${env}`, function () {
this.timeout(30000);
const cli = path.resolve(__dirname, '../../sapper');
let PORT;
describe('sapper', function() {
process.chdir(path.resolve(__dirname, '../app'));
// clean up after previous test runs
rimraf.sync('export');
rimraf.sync('build');
rimraf.sync('.sapper');
rimraf.sync('start.js');
this.timeout(process.env.CI ? 30000 : 10000);
// TODO reinstate dev tests
// run({
// mode: 'development'
// });
run({
mode: 'production'
});
run({
mode: 'production',
basepath: '/custom-basepath'
});
describe('export', () => {
before(() => {
return exec(`node ${cli} export`);
});
it('export all pages', () => {
const dest = path.resolve(__dirname, '../app/export');
// Pages that should show up in the extraction directory.
const expectedPages = [
'index.html',
'about/index.html',
'slow-preload/index.html',
'blog/index.html',
'blog/a-very-long-post/index.html',
'blog/how-can-i-get-involved/index.html',
'blog/how-is-sapper-different-from-next/index.html',
'blog/how-to-use-sapper/index.html',
'blog/what-is-sapper/index.html',
'blog/why-the-name/index.html',
'blog.json',
'blog/a-very-long-post.json',
'blog/how-can-i-get-involved.json',
'blog/how-is-sapper-different-from-next.json',
'blog/how-to-use-sapper.json',
'blog/what-is-sapper.json',
'blog/why-the-name.json',
'favicon.png',
'global.css',
'great-success.png',
'manifest.json',
'service-worker.js',
'svelte-logo-192.png',
'svelte-logo-512.png',
];
// Client scripts that should show up in the extraction directory.
const expectedClientRegexes = [
/client\/[^/]+\/_(\.\d+)?\.js/,
/client\/[^/]+\/about(\.\d+)?\.js/,
/client\/[^/]+\/blog_\$slug\$(\.\d+)?\.js/,
/client\/[^/]+\/blog(\.\d+)?\.js/,
/client\/[^/]+\/main(\.\d+)?\.js/,
/client\/[^/]+\/show_url(\.\d+)?\.js/,
/client\/[^/]+\/slow_preload(\.\d+)?\.js/,
];
const allPages = walkSync(dest);
expectedPages.forEach((expectedPage) => {
assert.ok(allPages.includes(expectedPage),`Could not find page matching ${expectedPage}`);
});
expectedClientRegexes.forEach((expectedRegex) => {
// Ensure each client page regular expression matches at least one
// generated page.
let matched = false;
for (const page of allPages) {
if (expectedRegex.test(page)) {
matched = true;
break;
}
}
assert.ok(matched, `Could not find client page matching ${expectedRegex}`);
});
});
});
});
function run({ mode, basepath = '' }) {
describe(`mode=${mode}`, function () {
let proc;
let nightmare;
let capture;
let base;
before(() => {
process.chdir(path.resolve(__dirname, '../app'));
const nightmare = new Nightmare();
let exec_promise = Promise.resolve();
nightmare.on('console', (type, ...args) => {
console[type](...args);
});
if (env === 'production') {
const cli = path.resolve(__dirname, '../../cli.js');
exec_promise = exec(`node ${cli} export`);
nightmare.on('page', (type, ...args) => {
if (type === 'error') {
console.error(args[1]);
} else {
console.warn(type, args);
}
});
return exec_promise.then(() => {
const resolved = require.resolve('../../middleware.js');
delete require.cache[resolved];
delete require.cache[require.resolve('../../core.js')]; // TODO remove this
before(() => {
const promise = mode === 'production'
? exec(`node ${cli} build -l`).then(() => ports.find(3000))
: ports.find(3000).then(port => {
exec(`node ${cli} dev`);
return ports.wait(port).then(() => port);
});
return require('get-port')();
}).then(port => {
return promise.then(port => {
base = `http://localhost:${port}`;
if (basepath) base += basepath;
proc = require('child_process').fork('.sapper/server.js', {
const dir = mode === 'production' ? 'build' : '.sapper';
if (mode === 'production') {
assert.ok(fs.existsSync('build/index.js'));
}
proc = require('child_process').fork(`${dir}/server.js`, {
cwd: process.cwd(),
env: {
NODE_ENV: env,
NODE_ENV: mode,
BASEPATH: basepath,
SAPPER_DEST: dir,
PORT: port
}
});
@@ -107,30 +218,13 @@ function run(env) {
after(() => {
// give a chance to clean up
return new Promise(fulfil => {
proc.on('exit', fulfil);
proc.kill();
});
});
beforeEach(() => {
nightmare = new Nightmare();
nightmare.on('console', (type, ...args) => {
console[type](...args);
});
nightmare.on('page', (type, ...args) => {
if (type === 'error') {
console.error(args[1]);
} else {
console.warn(type, args);
}
});
});
afterEach(() => {
return nightmare.end();
return Promise.all([
nightmare.end(),
new Promise(fulfil => {
proc.on('exit', fulfil);
proc.kill();
})
]);
});
describe('basic functionality', () => {
@@ -159,16 +253,16 @@ function run(env) {
});
it('navigates to a new page without reloading', () => {
return capture(() => nightmare.goto(base).init().preloadRoutes())
return nightmare.goto(base).init().prefetchRoutes()
.then(() => {
return capture(() => nightmare.click('a[href="/about"]'));
return capture(() => nightmare.click('a[href="about"]'));
})
.then(requests => {
assert.deepEqual(requests.map(r => r.url), []);
return nightmare.path();
})
.then(path => {
assert.equal(path, '/about');
assert.equal(path, `${basepath}/about`);
return nightmare.title();
})
.then(title => {
@@ -181,7 +275,7 @@ function run(env) {
.goto(`${base}/about`)
.init()
.click('.goto')
.wait(() => window.location.pathname === '/blog/what-is-sapper')
.wait(url => window.location.pathname === url, `${basepath}/blog/what-is-sapper`)
.wait(100)
.title()
.then(title => {
@@ -190,9 +284,7 @@ function run(env) {
});
it('prefetches programmatically', () => {
return nightmare
.goto(`${base}/about`)
.init()
return capture(() => nightmare.goto(`${base}/about`).init())
.then(() => {
return capture(() => {
return nightmare
@@ -201,7 +293,7 @@ function run(env) {
});
})
.then(requests => {
assert.ok(!!requests.find(r => r.url === '/blog/why-the-name.json'));
assert.ok(!!requests.find(r => r.url === `/blog/why-the-name.json`));
});
});
@@ -215,28 +307,31 @@ function run(env) {
});
});
it('reuses prefetch promise', () => {
it.skip('reuses prefetch promise', () => {
return nightmare
.goto(`${base}/blog`)
.init()
.then(() => {
return capture(() => {
return nightmare
.mouseover('[href="/blog/what-is-sapper"]')
.evaluate(() => {
const a = document.querySelector('[href="blog/what-is-sapper"]');
a.dispatchEvent(new MouseEvent('mousemove'));
})
.wait(200);
});
})
.then(mouseover_requests => {
assert.ok(mouseover_requests.findIndex(r => r.url === '/blog/what-is-sapper.json') !== -1);
assert.ok(mouseover_requests.findIndex(r => r.url === `/blog/what-is-sapper.json`) !== -1);
return capture(() => {
return nightmare
.click('[href="/blog/what-is-sapper"]')
.click('[href="blog/what-is-sapper"]')
.wait(200);
});
})
.then(click_requests => {
assert.ok(click_requests.findIndex(r => r.url === '/blog/what-is-sapper.json') === -1);
assert.ok(click_requests.findIndex(r => r.url === `/blog/what-is-sapper.json`) === -1);
});
});
@@ -244,13 +339,13 @@ function run(env) {
return nightmare
.goto(base)
.init()
.click('a[href="/slow-preload"]')
.click('a[href="slow-preload"]')
.wait(100)
.click('a[href="/about"]')
.click('a[href="about"]')
.wait(100)
.then(() => nightmare.path())
.then(path => {
assert.equal(path, '/about');
assert.equal(path, `${basepath}/about`);
return nightmare.title();
})
.then(title => {
@@ -259,7 +354,7 @@ function run(env) {
})
.then(() => nightmare.path())
.then(path => {
assert.equal(path, '/about');
assert.equal(path, `${basepath}/about`);
return nightmare.title();
})
.then(title => {
@@ -272,7 +367,7 @@ function run(env) {
.goto(`${base}/show-url`)
.init()
.evaluate(() => document.querySelector('p').innerHTML)
.end().then(html => {
.then(html => {
assert.equal(html, `URL is /show-url`);
});
});
@@ -308,7 +403,7 @@ function run(env) {
return nightmare.goto(`${base}/redirect-from`)
.path()
.then(path => {
assert.equal(path, '/redirect-to');
assert.equal(path, `${basepath}/redirect-to`);
})
.then(() => nightmare.page.title())
.then(title => {
@@ -318,12 +413,12 @@ function run(env) {
it('redirects in client', () => {
return nightmare.goto(base)
.wait('[href="/redirect-from"]')
.click('[href="/redirect-from"]')
.wait('[href="redirect-from"]')
.click('[href="redirect-from"]')
.wait(200)
.path()
.then(path => {
assert.equal(path, '/redirect-to');
assert.equal(path, `${basepath}/redirect-to`);
})
.then(() => nightmare.page.title())
.then(title => {
@@ -335,7 +430,7 @@ function run(env) {
return nightmare.goto(`${base}/blog/nope`)
.path()
.then(path => {
assert.equal(path, '/blog/nope');
assert.equal(path, `${basepath}/blog/nope`);
})
.then(() => nightmare.page.title())
.then(title => {
@@ -346,11 +441,11 @@ function run(env) {
it('handles 4xx error in client', () => {
return nightmare.goto(base)
.init()
.click('[href="/blog/nope"]')
.click('[href="blog/nope"]')
.wait(200)
.path()
.then(path => {
assert.equal(path, '/blog/nope');
assert.equal(path, `${basepath}/blog/nope`);
})
.then(() => nightmare.page.title())
.then(title => {
@@ -362,7 +457,7 @@ function run(env) {
return nightmare.goto(`${base}/blog/throw-an-error`)
.path()
.then(path => {
assert.equal(path, '/blog/throw-an-error');
assert.equal(path, `${basepath}/blog/throw-an-error`);
})
.then(() => nightmare.page.title())
.then(title => {
@@ -373,11 +468,11 @@ function run(env) {
it('handles non-4xx error in client', () => {
return nightmare.goto(base)
.init()
.click('[href="/blog/throw-an-error"]')
.click('[href="blog/throw-an-error"]')
.wait(200)
.path()
.then(path => {
assert.equal(path, '/blog/throw-an-error');
assert.equal(path, `${basepath}/blog/throw-an-error`);
})
.then(() => nightmare.page.title())
.then(title => {
@@ -388,7 +483,7 @@ function run(env) {
it('does not attempt client-side navigation to server routes', () => {
return nightmare.goto(`${base}/blog/how-is-sapper-different-from-next`)
.init()
.click(`[href="/blog/how-is-sapper-different-from-next.json"]`)
.click(`[href="blog/how-is-sapper-different-from-next.json"]`)
.wait(200)
.page.text()
.then(text => {
@@ -411,11 +506,79 @@ function run(env) {
assert.equal(title, `I'm afraid I just blue myself`);
});
});
it('serializes Set objects returned from preload', () => {
return nightmare.goto(`${base}/preload-values/set`)
.page.title()
.then(title => {
assert.equal(title, 'true');
return nightmare.init().page.title();
})
.then(title => {
assert.equal(title, 'true');
});
});
it('bails on custom classes returned from preload', () => {
return nightmare.goto(`${base}/preload-values/custom-class`)
.page.title()
.then(title => {
assert.equal(title, '42');
return nightmare.init().page.title();
})
.then(title => {
assert.equal(title, '42');
});
});
it('renders store props', () => {
return nightmare.goto(`${base}/store`)
.page.title()
.then(title => {
assert.equal(title, 'Stored title');
return nightmare.init().page.title();
})
.then(title => {
assert.equal(title, 'Stored title');
});
});
it('sends cookies when using this.fetch with credentials: "include"', () => {
return nightmare.goto(`${base}/credentials?creds=include`)
.page.title()
.then(title => {
assert.equal(title, 'woohoo!');
});
});
it('does not send cookies when using this.fetch without credentials', () => {
return nightmare.goto(`${base}/credentials`)
.page.title()
.then(title => {
assert.equal(title, 'unauthorized');
});
});
it('delegates to fetch on the client', () => {
return nightmare.goto(base).init()
.click('[href="credentials?creds=include"]')
.wait(100)
.page.title()
.then(title => {
assert.equal(title, 'woohoo!');
});
});
it('includes service worker', () => {
return nightmare.goto(base).page.html().then(html => {
assert.ok(html.indexOf('service-worker.js') !== -1);
});
});
});
describe('headers', () => {
it('sets Content-Type and Link...preload headers', () => {
return capture(() => nightmare.goto(base).end()).then(requests => {
return capture(() => nightmare.goto(base)).then(requests => {
const { headers } = requests[0];
assert.equal(
@@ -423,86 +586,27 @@ function run(env) {
'text/html'
);
const str = ['main', '_\\.\\d+']
.map(file => {
return `<${basepath}/client/[^/]+/${file}\\.js>;rel="preload";as="script"`;
})
.join(', ');
const regex = new RegExp(str);
assert.ok(
/<\/client\/[^/]+\/main\.js>;rel="preload";as="script", <\/client\/[^/]+\/_\.\d+\.js>;rel="preload";as="script"/.test(headers['link']),
regex.test(headers['link']),
headers['link']
);
});
});
});
if (env === 'production') {
describe('export', () => {
it('export all pages', () => {
const dest = path.resolve(__dirname, '../app/export');
// Pages that should show up in the extraction directory.
const expectedPages = [
'index.html',
'about/index.html',
'slow-preload/index.html',
'blog/index.html',
'blog/a-very-long-post/index.html',
'blog/how-can-i-get-involved/index.html',
'blog/how-is-sapper-different-from-next/index.html',
'blog/how-to-use-sapper/index.html',
'blog/what-is-sapper/index.html',
'blog/why-the-name/index.html',
'blog.json',
'blog/a-very-long-post.json',
'blog/how-can-i-get-involved.json',
'blog/how-is-sapper-different-from-next.json',
'blog/how-to-use-sapper.json',
'blog/what-is-sapper.json',
'blog/why-the-name.json',
'favicon.png',
'global.css',
'great-success.png',
'manifest.json',
'service-worker.js',
'svelte-logo-192.png',
'svelte-logo-512.png',
];
// Client scripts that should show up in the extraction directory.
const expectedClientRegexes = [
/client\/[^/]+\/_(\.\d+)?\.js/,
/client\/[^/]+\/about(\.\d+)?\.js/,
/client\/[^/]+\/blog_\$slug\$(\.\d+)?\.js/,
/client\/[^/]+\/blog(\.\d+)?\.js/,
/client\/[^/]+\/main(\.\d+)?\.js/,
/client\/[^/]+\/show_url(\.\d+)?\.js/,
/client\/[^/]+\/slow_preload(\.\d+)?\.js/,
];
const allPages = walkSync(dest);
expectedPages.forEach((expectedPage) => {
assert.ok(allPages.includes(expectedPage),`Could not find page matching ${expectedPage}`);
});
expectedClientRegexes.forEach((expectedRegex) => {
// Ensure each client page regular expression matches at least one
// generated page.
let matched = false;
for (const page of allPages) {
if (expectedRegex.test(page)) {
matched = true;
break;
}
}
assert.ok(matched, `Could not find client page matching ${expectedRegex}`);
});
});
});
}
});
}
function exec(cmd) {
return new Promise((fulfil, reject) => {
const parts = cmd.split(' ');
const parts = cmd.trim().split(' ');
const proc = require('child_process').spawn(parts.shift(), parts);
proc.stdout.on('data', data => {

View File

@@ -1,7 +1,26 @@
const assert = require('assert');
const { create_routes } = require('../../core.js');
const { create_routes } = require('../../dist/core.ts.js');
describe('create_routes', () => {
it('encodes caharcters not allowed in path', () => {
const routes = create_routes({
files: [
'"',
'#',
'?'
]
});
assert.deepEqual(
routes.map(r => r.pattern),
[
/^\/%22\/?$/,
/^\/%23\/?$/,
/^\/%3F\/?$/
]
);
});
it('sorts routes correctly', () => {
const routes = create_routes({
files: ['index.html', 'about.html', 'post/f[xx].html', '[wildcard].html', 'post/foo.html', 'post/[id].html', 'post/bar.html', 'post/[id].json.js']
@@ -12,8 +31,8 @@ describe('create_routes', () => {
[
'index.html',
'about.html',
'post/foo.html',
'post/bar.html',
'post/foo.html',
'post/f[xx].html',
'post/[id].json.js',
'post/[id].html',
@@ -22,6 +41,80 @@ describe('create_routes', () => {
);
});
it('prefers index page to nested route', () => {
let routes = create_routes({
files: [
'api/examples/[slug].js',
'api/examples/index.js',
'blog/[slug].html',
'api/gists/[id].js',
'api/gists/index.js',
'4xx.html',
'5xx.html',
'blog/index.html',
'blog/rss.xml.js',
'guide/index.html',
'index.html'
]
});
assert.deepEqual(
routes.map(r => r.file),
[
'4xx.html',
'5xx.html',
'index.html',
'guide/index.html',
'blog/index.html',
'blog/rss.xml.js',
'blog/[slug].html',
'api/examples/index.js',
'api/examples/[slug].js',
'api/gists/index.js',
'api/gists/[id].js',
]
);
routes = create_routes({
files: [
'4xx.html',
'5xx.html',
'api/blog/[slug].js',
'api/blog/index.js',
'api/guide/contents.js',
'api/guide/index.js',
'blog/[slug].html',
'blog/index.html',
'blog/rss.xml.js',
'gist/[id].js',
'gist/create.js',
'guide/index.html',
'index.html',
'repl/index.html'
]
});
assert.deepEqual(
routes.map(r => r.file),
[
'4xx.html',
'5xx.html',
'index.html',
'guide/index.html',
'blog/index.html',
'blog/rss.xml.js',
'blog/[slug].html',
'gist/create.js',
'gist/[id].js',
'repl/index.html',
'api/guide/index.js',
'api/guide/contents.js',
'api/blog/index.js',
'api/blog/[slug].js',
]
);
});
it('generates params', () => {
const routes = create_routes({
files: ['index.html', 'about.html', '[wildcard].html', 'post/[id].html']

2
webpack.js Normal file
View File

@@ -0,0 +1,2 @@
// TODO write to this file, instead of webpack.ts.js
module.exports = require('./dist/webpack.ts.js');

2
webpack/config.js Normal file
View File

@@ -0,0 +1,2 @@
// TODO deprecate this file in favour of sapper/webpack.js
module.exports = require('../dist/webpack.ts.js');