mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-13 11:35:28 +00:00
Compare commits
145 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f1d2e0a04 | ||
|
|
dfb8692d78 | ||
|
|
09d3c4d85e | ||
|
|
1e623dde29 | ||
|
|
5104abf329 | ||
|
|
6554fc8616 | ||
|
|
cd01b7e6db | ||
|
|
bfa3da6d3d | ||
|
|
6ee092f8d4 | ||
|
|
ac70004f77 | ||
|
|
3449f1eb37 | ||
|
|
16cb1fccc6 | ||
|
|
b20c1c029f | ||
|
|
7abfb1aab1 | ||
|
|
205c2defe4 | ||
|
|
09a6eec83e | ||
|
|
2cabf61ea7 | ||
|
|
71cfdd2907 | ||
|
|
297f4276de | ||
|
|
422e31e183 | ||
|
|
b53ee061c0 | ||
|
|
8bad37205d | ||
|
|
fd0dd4fe58 | ||
|
|
4940644ae3 | ||
|
|
fb8d952eeb | ||
|
|
fc631c4866 | ||
|
|
03ce2ea998 | ||
|
|
dd8deb2d8a | ||
|
|
7d721abb2a | ||
|
|
39b1fa89ce | ||
|
|
7a3506420f | ||
|
|
72ae4a1c64 | ||
|
|
a09c33d6a5 | ||
|
|
4590aa313c | ||
|
|
d11bd954e0 | ||
|
|
c15959710b | ||
|
|
bb8ff74f68 | ||
|
|
2cbbe91490 | ||
|
|
faeddd8add | ||
|
|
d77722c042 | ||
|
|
61daba7a64 | ||
|
|
54ff8cc2e6 | ||
|
|
e6fcafe09b | ||
|
|
a305d3cea1 | ||
|
|
75e70207b8 | ||
|
|
8a8526d9ed | ||
|
|
9a76229bb6 | ||
|
|
f4e46e6e6c | ||
|
|
90cd347112 | ||
|
|
5adfdd6fe0 | ||
|
|
a6dc61a182 | ||
|
|
96666d05ec | ||
|
|
6390ba692b | ||
|
|
0e131cc81e | ||
|
|
bd3d5713cb | ||
|
|
9ec23c47ad | ||
|
|
b7bb69925e | ||
|
|
25124f6ee7 | ||
|
|
73d491cd19 | ||
|
|
e25fceb4b8 | ||
|
|
3807147c57 | ||
|
|
a523ba58ff | ||
|
|
fe03fd3a52 | ||
|
|
89c430a0cb | ||
|
|
8ef312849c | ||
|
|
4200446684 | ||
|
|
681ed005b8 | ||
|
|
d457af8d51 | ||
|
|
0c158b9e1f | ||
|
|
50011e2077 | ||
|
|
f27b7973e3 | ||
|
|
2af2ab3cb9 | ||
|
|
6a4dc1901c | ||
|
|
fbbc0e9e19 | ||
|
|
1213c3da46 | ||
|
|
4cc2104088 | ||
|
|
d6dda371ca | ||
|
|
304c06085e | ||
|
|
33b6450e34 | ||
|
|
8faa98af6a | ||
|
|
14df138528 | ||
|
|
44285cdb2f | ||
|
|
bd656cfd5b | ||
|
|
c4b4bd587d | ||
|
|
2abfdb03d5 | ||
|
|
a80ac3a8b8 | ||
|
|
887cb09386 | ||
|
|
cfeeafded4 | ||
|
|
2cae674033 | ||
|
|
7c0f32662d | ||
|
|
b4fb1c3268 | ||
|
|
ecd0f673a9 | ||
|
|
40d16852f7 | ||
|
|
133be03791 | ||
|
|
727a76ebb5 | ||
|
|
e3c047831a | ||
|
|
81b5e0d764 | ||
|
|
98e904dcfc | ||
|
|
ca51372150 | ||
|
|
7cef1f1120 | ||
|
|
1b73baabce | ||
|
|
5aa01b922b | ||
|
|
f0bc68be88 | ||
|
|
be7c53becc | ||
|
|
9ea4137b87 | ||
|
|
7588911108 | ||
|
|
fc8280adea | ||
|
|
d08f9eb5a4 | ||
|
|
2b3472b1b1 | ||
|
|
30ddb3dd7e | ||
|
|
0c891ba79e | ||
|
|
ee94f355d5 | ||
|
|
bea9b7965a | ||
|
|
1312aede1f | ||
|
|
50e307e0c0 | ||
|
|
e87ac1f367 | ||
|
|
5da9d0926a | ||
|
|
9538499d51 | ||
|
|
ff1e632057 | ||
|
|
aeeb231477 | ||
|
|
d1940db8c0 | ||
|
|
98f9a64b64 | ||
|
|
b9bef802d3 | ||
|
|
a7024b3806 | ||
|
|
423e02aeae | ||
|
|
12b73ecebf | ||
|
|
e1bc38b5a7 | ||
|
|
b66f624f01 | ||
|
|
502dd547d1 | ||
|
|
4c343490d2 | ||
|
|
b3027c5816 | ||
|
|
c29e8022cc | ||
|
|
e4cd4c9cb0 | ||
|
|
feddad42b2 | ||
|
|
3c4ebcda30 | ||
|
|
75aedf4663 | ||
|
|
c8366dec74 | ||
|
|
9a936669c6 | ||
|
|
0226bd90c6 | ||
|
|
e1926e1bcb | ||
|
|
db1c1f332a | ||
|
|
e8d510b261 | ||
|
|
f8e237b265 | ||
|
|
68c2f2e388 | ||
|
|
0bcb61650b |
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"root": true,
|
"root": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
"indent": [ 2, "tab", { "SwitchCase": 1 } ],
|
|
||||||
"semi": [ 2, "always" ],
|
"semi": [ 2, "always" ],
|
||||||
"space-before-blocks": [ 2, "always" ],
|
"space-before-blocks": [ 2, "always" ],
|
||||||
"no-mixed-spaces-and-tabs": [ 2, "smart-tabs" ],
|
"no-mixed-spaces-and-tabs": [ 2, "smart-tabs" ],
|
||||||
@@ -33,7 +32,7 @@
|
|||||||
"plugin:import/warnings"
|
"plugin:import/warnings"
|
||||||
],
|
],
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 6,
|
"ecmaVersion": 8,
|
||||||
"sourceType": "module"
|
"sourceType": "module"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1,4 +1,15 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
yarn.lock
|
||||||
node_modules
|
node_modules
|
||||||
cypress/screenshots
|
cypress/screenshots
|
||||||
test/app/.sapper
|
test/app/.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
|
||||||
12
.travis.yml
12
.travis.yml
@@ -1,10 +1,22 @@
|
|||||||
sudo: false
|
sudo: false
|
||||||
|
|
||||||
language: node_js
|
language: node_js
|
||||||
|
|
||||||
node_js:
|
node_js:
|
||||||
|
- "6"
|
||||||
- "stable"
|
- "stable"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
- BUILD_TIMEOUT=10000
|
- BUILD_TIMEOUT=10000
|
||||||
|
|
||||||
|
addons:
|
||||||
|
apt:
|
||||||
|
packages:
|
||||||
|
- xvfb
|
||||||
|
|
||||||
install:
|
install:
|
||||||
|
- export DISPLAY=':99.0'
|
||||||
|
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||||
- npm install
|
- npm install
|
||||||
- (cd test/app && npm install)
|
- (cd test/app && npm install)
|
||||||
|
|||||||
65
CHANGELOG.md
65
CHANGELOG.md
@@ -1,5 +1,70 @@
|
|||||||
# sapper changelog
|
# sapper changelog
|
||||||
|
|
||||||
|
## 0.6.2
|
||||||
|
|
||||||
|
* Handle unspecified type in `sapper export`
|
||||||
|
|
||||||
|
## 0.6.1
|
||||||
|
|
||||||
|
* Fix `pkg.files` and `pkg.bin`
|
||||||
|
|
||||||
|
## 0.6.0
|
||||||
|
|
||||||
|
* Hydrate on first load, and only on first load ([#93](https://github.com/sveltejs/sapper/pull/93))
|
||||||
|
* Identify clashes between page and server routes ([#96](https://github.com/sveltejs/sapper/pull/96))
|
||||||
|
* Remove Express-specific utilities, for compatbility with Polka et al ([#94](https://github.com/sveltejs/sapper/issues/94))
|
||||||
|
* Return a promise from `init` when first page has rendered ([#99](https://github.com/sveltejs/sapper/issues/99))
|
||||||
|
* Handle invalid hash links ([#104](https://github.com/sveltejs/sapper/pull/104))
|
||||||
|
* Avoid `URLSearchParams` ([#107](https://github.com/sveltejs/sapper/pull/107))
|
||||||
|
* Don't automatically set `Content-Type` for server routes ([#111](https://github.com/sveltejs/sapper/pull/111))
|
||||||
|
* Handle empty query string routes, e.g. `/?` ([#105](https://github.com/sveltejs/sapper/pull/105))
|
||||||
|
|
||||||
|
## 0.5.1
|
||||||
|
|
||||||
|
* Only write service-worker.js to filesystem in dev mode ([#90](https://github.com/sveltejs/sapper/issues/90))
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
* Experimental support for `sapper export` ([#9](https://github.com/sveltejs/sapper/issues/9))
|
||||||
|
* Lazily load chokidar, for faster startup ([#64](https://github.com/sveltejs/sapper/pull/64))
|
||||||
|
|
||||||
|
## 0.4.0
|
||||||
|
|
||||||
|
* `%sapper.main%` has been replaced with `%sapper.scripts%` ([#86](https://github.com/sveltejs/sapper/issues/86))
|
||||||
|
* Node 6 support ([#67](https://github.com/sveltejs/sapper/pull/67))
|
||||||
|
* Explicitly load css-loader and style-loader ([#72](https://github.com/sveltejs/sapper/pull/72))
|
||||||
|
* DELETE requests are handled with `del` exports ([#77](https://github.com/sveltejs/sapper/issues/77))
|
||||||
|
* Send preloaded data for first route to client, where possible ([#3](https://github.com/sveltejs/sapper/issues/3))
|
||||||
|
|
||||||
|
## 0.3.2
|
||||||
|
|
||||||
|
* Expose `prefetch` function ([#61](https://github.com/sveltejs/sapper/pull/61))
|
||||||
|
|
||||||
|
## 0.3.1
|
||||||
|
|
||||||
|
* Fix missing `runtime.js`
|
||||||
|
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
* Move `sapper/runtime/app.js` to `sapper/runtime.js`
|
||||||
|
* Cancel navigation if overtaken by second navigation ([#48](https://github.com/sveltejs/sapper/issues/48))
|
||||||
|
* Store preloaded data, to avoiding double prefetching ([#49](https://github.com/sveltejs/sapper/issues/49))
|
||||||
|
* Pass server request object to `preload` ([#54](https://github.com/sveltejs/sapper/pull/54))
|
||||||
|
* Nested routes ([#55](https://github.com/sveltejs/sapper/issues/55))
|
||||||
|
|
||||||
|
## 0.2.10
|
||||||
|
|
||||||
|
* Handle deep links correctly ([#44](https://github.com/sveltejs/sapper/issues/44))
|
||||||
|
|
||||||
|
## 0.2.9
|
||||||
|
|
||||||
|
* Don't write files to disk in prod mode
|
||||||
|
|
||||||
|
## 0.2.8
|
||||||
|
|
||||||
|
* Add `goto` function ([#29](https://github.com/sveltejs/sapper/issues/29))
|
||||||
|
* Don't use `/tmp` as destination in Now environments
|
||||||
|
|
||||||
## 0.2.7
|
## 0.2.7
|
||||||
|
|
||||||
* Fix streaming bug
|
* Fix streaming bug
|
||||||
|
|||||||
9
LICENSE
Normal file
9
LICENSE
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Copyright (c) 2017 [these people](https://github.com/sveltejs/sapper/graphs/contributors).
|
||||||
|
|
||||||
|
Permission is hereby granted by the authors of this software, to any person, to use the software for any purpose, free of charge, including the rights to run, read, copy, change, distribute and sell it, and including usage rights to any patents the authors may hold on it, subject to the following conditions:
|
||||||
|
|
||||||
|
This license, or a link to its text, must be included with all copies of the software and any derivative works.
|
||||||
|
|
||||||
|
Any modification to the software submitted to the authors may be incorporated into the software under the terms of this license.
|
||||||
|
|
||||||
|
The software is provided "as is", without warranty of any kind, including but not limited to the warranties of title, fitness, merchantability and non-infringement. The authors have no obligation to provide support or updates for the software, and may not be held liable for any damages, claims or other liability arising from its use.
|
||||||
151
README.md
151
README.md
@@ -1,150 +1,37 @@
|
|||||||
# sapper
|
# sapper
|
||||||
|
|
||||||
Combat-ready apps, engineered by Svelte.
|
[Military-grade progressive web apps, powered by Svelte.](https://sapper.svelte.technology)
|
||||||
|
|
||||||
## This is not a thing yet
|
|
||||||
|
|
||||||
If you visit this README in a few weeks, hopefully it will have blossomed into the app development framework we deserve. Right now, it's just a set of ideas.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
[Next.js](https://github.com/zeit/next.js/) introduced a beautiful idea — that you should be able to build your app as universal React components in a special `pages` directory, and the framework should take care of routing and rendering on both client and server. What if we did the same thing for Svelte?
|
|
||||||
|
|
||||||
High-level goals:
|
|
||||||
|
|
||||||
* Extreme ease of development
|
|
||||||
* Code-splitting and HMR out of the box (probably via webpack)
|
|
||||||
* Best-in-class performance
|
|
||||||
* As little magic as possible. Anyone should be able to understand how everything fits together, and e.g. make changes to the webpack config
|
|
||||||
* Links are just `<a>` tags, no special `<Link>` components
|
|
||||||
|
|
||||||
|
|
||||||
## Design
|
## What is Sapper?
|
||||||
|
|
||||||
A Sapper app is just an Express app (conventionally, `server.js`) that uses the `sapper` middleware:
|
Sapper is a framework for building high-performance universal web apps. [Read the guide](https://sapper.svelte.technology/guide) or the [introductory blog post](https://svelte.technology/blog/sapper-towards-the-ideal-web-app-framework) to learn more.
|
||||||
|
|
||||||
```js
|
|
||||||
const app = require('express')();
|
|
||||||
const sapper = require('sapper');
|
|
||||||
|
|
||||||
app.use(sapper());
|
## Get started
|
||||||
|
|
||||||
const { PORT = 3000 } = process.env;
|
Clone the [starter project template](https://github.com/sveltejs/sapper-template) with [degit](https://github.com/rich-harris/degit)...
|
||||||
app.listen(PORT, () => {
|
|
||||||
console.log(`listening on port ${PORT}`);
|
```bash
|
||||||
});
|
npx degit sveltejs/sapper-template my-app
|
||||||
```
|
```
|
||||||
|
|
||||||
The middleware serves pages that match files in the `routes` directory, and assets generated by webpack. In development mode, the middleware once activated watches `routes` to keep the app up-to-date.
|
...then install dependencies and start the dev server...
|
||||||
|
|
||||||
|
```bash
|
||||||
## Routing
|
cd my-app
|
||||||
|
npm install
|
||||||
Like Next, routes are defined by the project directory structure, but with some crucial differences:
|
npm run dev
|
||||||
|
|
||||||
* Files with an `.html` extension are treated as Svelte components. The `routes/about.html` (or `routes/about/index.html`) would create the `/about` route.
|
|
||||||
* Files with a `.js` or `.mjs` extension are more generic route handlers. These files should export functions corresponding to the HTTP methods they support (example below).
|
|
||||||
* Instead of route masking, we embed parameters in the filename. For example `post/[id].html` maps to `/post/:id`, and the component will be rendered with the appropriate parameter.
|
|
||||||
* Nested routes (read [this article](https://joshduff.com/2015-06-why-you-need-a-state-router.md)) can be handled by creating a file that matches the subroute — for example, `routes/app/settings/[submenu].html` would match `/app/settings/profile` *and* `app/settings`, but in the latter case the `submenu` parameter would be `null`.
|
|
||||||
|
|
||||||
An example of a generic route:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// routes/api/post/[id].js
|
|
||||||
export async function get(req, res) {
|
|
||||||
try {
|
|
||||||
const data = await getPostFromDatabase(req.params.id);
|
|
||||||
const json = JSON.stringify(data);
|
|
||||||
|
|
||||||
res.set({
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Content-Length': json.length
|
|
||||||
});
|
|
||||||
|
|
||||||
res.send(json);
|
|
||||||
} catch (err) {
|
|
||||||
res.status(500).send(err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Or, if you omit the `res` argument, it can use the return value:
|
...and navigate to [localhost:3000](http://localhost:3000). To build and run in production mode:
|
||||||
|
|
||||||
```js
|
```bash
|
||||||
// routes/api/post/[id].js
|
npm run build
|
||||||
export async function get(req) {
|
npm start
|
||||||
return await getPostFromDatabase(req.params.id);
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Client-side app
|
## License
|
||||||
|
|
||||||
Sapper will create (and in development mode, update) a barebones `main.js` file that dynamically imports individual routes and renders them — something like this:
|
[LIL](LICENSE)
|
||||||
|
|
||||||
```js
|
|
||||||
window.addEventListener('click', event => {
|
|
||||||
let a = event.target;
|
|
||||||
while (a && a.nodeName !== 'A') a = a.parentNode;
|
|
||||||
if (!a) return;
|
|
||||||
|
|
||||||
if (navigate(new URL(a.href))) event.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
const target = document.querySelector('#sapper');
|
|
||||||
let component;
|
|
||||||
|
|
||||||
function navigate(url) {
|
|
||||||
if (url.origin !== window.location.origin) return;
|
|
||||||
|
|
||||||
let match;
|
|
||||||
let params = {};
|
|
||||||
const query = {};
|
|
||||||
|
|
||||||
function render(mod) {
|
|
||||||
if (component) {
|
|
||||||
component.destroy();
|
|
||||||
} else {
|
|
||||||
target.innerHTML = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
component = new mod.default({
|
|
||||||
target,
|
|
||||||
data: { query, params },
|
|
||||||
hydrate: !!component
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.pathname === '/about') {
|
|
||||||
import('/about/index.html').then(render);
|
|
||||||
} else if (url.pathname === '/') {
|
|
||||||
import('/index.js').then(render);
|
|
||||||
} else if (match = /^\/post\/([^\/]+)$/.exec(url.pathname)) {
|
|
||||||
params.id = match[1];
|
|
||||||
import('/post/[id].html').then(render);
|
|
||||||
} else if (match = /^\/([^\/]+)$/.exec(url.pathname)) {
|
|
||||||
params.wildcard = match[1];
|
|
||||||
import('/[wildcard].html').then(render);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate(window.location);
|
|
||||||
```
|
|
||||||
|
|
||||||
We're glossing over a lot of important stuff here — e.g. handling `popstate` — but you get the idea. Knowledge of all the possible routes means we can generate optimal code, much in the same way that statically analysing Svelte templates allows the compiler to generate optimal code.
|
|
||||||
|
|
||||||
|
|
||||||
## Things to figure out
|
|
||||||
|
|
||||||
* How to customise the overall page template
|
|
||||||
* An equivalent of `getInitialProps`
|
|
||||||
* Critical CSS
|
|
||||||
* `store` integration
|
|
||||||
* Route transitions
|
|
||||||
* Equivalent of `next export`
|
|
||||||
* A good story for realtime/GraphQL stuff
|
|
||||||
* Service worker
|
|
||||||
* Using `Link...rel=preload` headers to push main.js/[route].js plus styles
|
|
||||||
* ...and lots of other things that haven't occurred to me yet.
|
|
||||||
@@ -15,3 +15,7 @@ environment:
|
|||||||
install:
|
install:
|
||||||
- ps: Install-Product node $env:nodejs_version
|
- ps: Install-Product node $env:nodejs_version
|
||||||
- npm install
|
- npm install
|
||||||
|
|
||||||
|
test_script:
|
||||||
|
- node --version && npm --version
|
||||||
|
- npm test
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const cmd = process.argv[2];
|
|
||||||
|
|
||||||
if (cmd === 'build') {
|
|
||||||
process.env.NODE_ENV = 'production';
|
|
||||||
require('../lib/build.js')();
|
|
||||||
}
|
|
||||||
43
lib/build.js
43
lib/build.js
@@ -1,43 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const glob = require('glob');
|
|
||||||
const mkdirp = require('mkdirp');
|
|
||||||
const rimraf = require('rimraf');
|
|
||||||
const { client, server } = require('./utils/compilers.js');
|
|
||||||
const create_app = require('./utils/create_app.js');
|
|
||||||
const generate_asset_cache = require('./utils/generate_asset_cache.js');
|
|
||||||
const { dest } = require('./config.js');
|
|
||||||
|
|
||||||
module.exports = () => {
|
|
||||||
mkdirp(dest);
|
|
||||||
rimraf.sync(path.join(dest, '**/*'));
|
|
||||||
|
|
||||||
// create main.js and server-routes.js
|
|
||||||
create_app();
|
|
||||||
|
|
||||||
function handleErrors(err, stats) {
|
|
||||||
if (err) {
|
|
||||||
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stats.hasErrors()) {
|
|
||||||
console.log(stats.toString({ colors: true }));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
client.run((err, clientStats) => {
|
|
||||||
handleErrors(err, clientStats);
|
|
||||||
const clientInfo = clientStats.toJson();
|
|
||||||
fs.writeFileSync(path.join(dest, 'stats.client.json'), JSON.stringify(clientInfo, null, ' '));
|
|
||||||
|
|
||||||
server.run((err, serverStats) => {
|
|
||||||
handleErrors(err, serverStats);
|
|
||||||
const serverInfo = serverStats.toJson();
|
|
||||||
fs.writeFileSync(path.join(dest, 'stats.server.json'), JSON.stringify(serverInfo, null, ' '));
|
|
||||||
|
|
||||||
generate_asset_cache(clientInfo, serverInfo);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const mkdirp = require('mkdirp');
|
|
||||||
const rimraf = require('rimraf');
|
|
||||||
|
|
||||||
exports.dev = process.env.NODE_ENV !== 'production';
|
|
||||||
|
|
||||||
exports.templates = path.resolve(process.env.SAPPER_TEMPLATES || 'templates');
|
|
||||||
|
|
||||||
exports.src = path.resolve(process.env.SAPPER_ROUTES || 'routes');
|
|
||||||
|
|
||||||
exports.dest = path.resolve(
|
|
||||||
process.env.NOW ? '/tmp' :
|
|
||||||
process.env.SAPPER_DEST || '.sapper'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (exports.dev) {
|
|
||||||
mkdirp(exports.dest);
|
|
||||||
rimraf.sync(path.join(exports.dest, '**/*'));
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.entry = {
|
|
||||||
client: path.resolve(exports.templates, '.main.rendered.js'),
|
|
||||||
server: path.resolve(exports.dest, 'server-entry.js')
|
|
||||||
};
|
|
||||||
227
lib/index.js
227
lib/index.js
@@ -1,227 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const glob = require('glob');
|
|
||||||
const rimraf = require('rimraf');
|
|
||||||
const mkdirp = require('mkdirp');
|
|
||||||
const webpack = require('webpack');
|
|
||||||
const route_manager = require('./route_manager.js');
|
|
||||||
const templates = require('./templates.js');
|
|
||||||
const create_app = require('./utils/create_app.js');
|
|
||||||
const create_watcher = require('./utils/create_watcher.js');
|
|
||||||
const compilers = require('./utils/compilers.js');
|
|
||||||
const generate_asset_cache = require('./utils/generate_asset_cache.js');
|
|
||||||
const escape_html = require('escape-html');
|
|
||||||
const { src, dest, dev } = require('./config.js');
|
|
||||||
|
|
||||||
function connect_dev() {
|
|
||||||
create_app();
|
|
||||||
|
|
||||||
const watcher = create_watcher();
|
|
||||||
|
|
||||||
let asset_cache;
|
|
||||||
|
|
||||||
return compose_handlers([
|
|
||||||
require('webpack-hot-middleware')(compilers.client, {
|
|
||||||
reload: true,
|
|
||||||
path: '/__webpack_hmr',
|
|
||||||
heartbeat: 10 * 1000
|
|
||||||
}),
|
|
||||||
|
|
||||||
async (req, res, next) => {
|
|
||||||
asset_cache = await watcher.ready;
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
|
|
||||||
set_req_pathname,
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname === '/index.html',
|
|
||||||
type: 'text/html',
|
|
||||||
cache: 'max-age=600',
|
|
||||||
fn: () => asset_cache.client.index
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname === '/service-worker.js',
|
|
||||||
type: 'application/javascript',
|
|
||||||
cache: 'max-age=600',
|
|
||||||
fn: () => asset_cache.client.service_worker
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname.startsWith('/client/'),
|
|
||||||
type: 'application/javascript',
|
|
||||||
cache: 'max-age=31536000',
|
|
||||||
fn: pathname => asset_cache.client.chunks[pathname]
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_route_handler(() => asset_cache),
|
|
||||||
|
|
||||||
get_not_found_handler(() => asset_cache)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function connect_prod() {
|
|
||||||
const asset_cache = generate_asset_cache(
|
|
||||||
read_json(path.join(dest, 'stats.client.json')),
|
|
||||||
read_json(path.join(dest, 'stats.server.json'))
|
|
||||||
);
|
|
||||||
|
|
||||||
return compose_handlers([
|
|
||||||
set_req_pathname,
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname === '/index.html',
|
|
||||||
type: 'text/html',
|
|
||||||
cache: 'max-age=600',
|
|
||||||
fn: () => asset_cache.client.index
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname === '/service-worker.js',
|
|
||||||
type: 'application/javascript',
|
|
||||||
cache: 'max-age=600',
|
|
||||||
fn: () => asset_cache.client.service_worker
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname.startsWith('/client/'),
|
|
||||||
type: 'application/javascript',
|
|
||||||
cache: 'max-age=31536000',
|
|
||||||
fn: pathname => asset_cache.client.chunks[pathname]
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_route_handler(() => asset_cache),
|
|
||||||
|
|
||||||
get_not_found_handler(() => asset_cache)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = dev ? connect_dev : connect_prod;
|
|
||||||
|
|
||||||
function set_req_pathname(req, res, next) {
|
|
||||||
req.pathname = req.url.replace(/\?.+/, '');
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_asset_handler(opts) {
|
|
||||||
return (req, res, next) => {
|
|
||||||
if (!opts.filter(req.pathname)) return next();
|
|
||||||
|
|
||||||
res.set({
|
|
||||||
'Content-Type': opts.type,
|
|
||||||
'Cache-Control': opts.cache
|
|
||||||
});
|
|
||||||
res.end(opts.fn(req.pathname));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_route_handler(fn) {
|
|
||||||
return async function handle_route(req, res, next) {
|
|
||||||
const url = req.pathname;
|
|
||||||
|
|
||||||
const { client, server } = fn();
|
|
||||||
|
|
||||||
// whatever happens, we're going to serve some HTML
|
|
||||||
res.set({
|
|
||||||
'Content-Type': 'text/html'
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const route of route_manager.routes) {
|
|
||||||
if (route.test(url)) {
|
|
||||||
req.params = route.exec(url);
|
|
||||||
|
|
||||||
const mod = require(server.entry)[route.id];
|
|
||||||
|
|
||||||
if (route.type === 'page') {
|
|
||||||
// preload main.js and current route
|
|
||||||
// TODO detect other stuff we can preload? images, CSS, fonts?
|
|
||||||
res.set('Link', `<${client.main_file}>;rel="preload";as="script", <${client.routes[route.id]}>;rel="preload";as="script"`);
|
|
||||||
|
|
||||||
let data = { params: req.params, query: req.query };
|
|
||||||
|
|
||||||
if (mod.preload) {
|
|
||||||
const promise = Promise.resolve(mod.preload(data)).then(preloaded => {
|
|
||||||
Object.assign(data, preloaded);
|
|
||||||
return mod.render(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
templates.stream(res, 200, {
|
|
||||||
main: client.main_file,
|
|
||||||
html: promise.then(rendered => rendered.html),
|
|
||||||
head: promise.then(({ head }) => `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`),
|
|
||||||
styles: promise.then(({ css }) => (css && css.code ? `<style>${css.code}</style>` : ''))
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const { html, head, css } = mod.render(data);
|
|
||||||
|
|
||||||
const page = templates.render(200, {
|
|
||||||
main: client.main_file,
|
|
||||||
html,
|
|
||||||
head: `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`,
|
|
||||||
styles: (css && css.code ? `<style>${css.code}</style>` : '')
|
|
||||||
});
|
|
||||||
|
|
||||||
res.end(page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
const handler = mod[req.method.toLowerCase()];
|
|
||||||
if (handler) handler(req, res, next);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
} catch(err) {
|
|
||||||
res.status(500).end(templates.render(500, {
|
|
||||||
title: (err && err.name) || 'Internal server error',
|
|
||||||
url,
|
|
||||||
error: escape_html(err && (err.details || err.message || err) || 'Unknown error'),
|
|
||||||
stack: err && err.stack.split('\n').slice(1).join('\n')
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_not_found_handler(fn) {
|
|
||||||
return function handle_not_found(req, res) {
|
|
||||||
const asset_cache = fn();
|
|
||||||
|
|
||||||
res.status(404).end(templates.render(404, {
|
|
||||||
title: 'Not found',
|
|
||||||
status: 404,
|
|
||||||
method: req.method,
|
|
||||||
main: asset_cache.client.main_file,
|
|
||||||
url: req.url
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function compose_handlers(handlers) {
|
|
||||||
return (req, res, next) => {
|
|
||||||
let i = 0;
|
|
||||||
function go() {
|
|
||||||
const handler = handlers[i];
|
|
||||||
|
|
||||||
if (handler) {
|
|
||||||
handler(req, res, () => {
|
|
||||||
i += 1;
|
|
||||||
go();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
go();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function read_json(file) {
|
|
||||||
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
const glob = require('glob');
|
|
||||||
const chokidar = require('chokidar');
|
|
||||||
const create_routes = require('./utils/create_routes.js');
|
|
||||||
const { src, dev } = require('./config.js');
|
|
||||||
|
|
||||||
const callbacks = [];
|
|
||||||
|
|
||||||
exports.onchange = fn => {
|
|
||||||
callbacks.push(fn);
|
|
||||||
};
|
|
||||||
|
|
||||||
function update() {
|
|
||||||
exports.routes = create_routes(
|
|
||||||
glob.sync('**/*.+(html|js|mjs)', { cwd: src })
|
|
||||||
);
|
|
||||||
|
|
||||||
callbacks.forEach(fn => fn());
|
|
||||||
}
|
|
||||||
|
|
||||||
update();
|
|
||||||
|
|
||||||
if (dev) {
|
|
||||||
const watcher = chokidar.watch(`${src}/**/*.+(html|js|mjs)`, {
|
|
||||||
ignoreInitial: true
|
|
||||||
});
|
|
||||||
|
|
||||||
watcher.on('add', update);
|
|
||||||
watcher.on('change', update);
|
|
||||||
watcher.on('unlink', update);
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const relative = require('require-relative');
|
|
||||||
const webpack = relative('webpack', process.cwd());
|
|
||||||
|
|
||||||
exports.client = webpack(
|
|
||||||
require(path.resolve('webpack.client.config.js'))
|
|
||||||
);
|
|
||||||
|
|
||||||
exports.server = webpack(
|
|
||||||
require(path.resolve('webpack.server.config.js'))
|
|
||||||
);
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const chokidar = require('chokidar');
|
|
||||||
const route_manager = require('../route_manager.js');
|
|
||||||
const { src, dest, entry, dev } = require('../config.js');
|
|
||||||
|
|
||||||
function posixify(file) {
|
|
||||||
return file.replace(/[\/\\]/g, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
function create_app() {
|
|
||||||
const { routes } = route_manager;
|
|
||||||
|
|
||||||
function create_client_main() {
|
|
||||||
const template = fs.readFileSync('templates/main.js', 'utf-8');
|
|
||||||
|
|
||||||
const code = `[${
|
|
||||||
routes
|
|
||||||
.filter(route => route.type === 'page')
|
|
||||||
.map(route => {
|
|
||||||
const params = route.dynamic.length === 0 ?
|
|
||||||
'{}' :
|
|
||||||
`{ ${route.dynamic.map((part, i) => `${part}: match[${i + 1}]`).join(', ') } }`;
|
|
||||||
|
|
||||||
const file = posixify(`${src}/${route.file}`);
|
|
||||||
return `{ pattern: ${route.pattern}, params: match => (${params}), load: () => import(/* webpackChunkName: "${route.id}" */ '${file}') }`
|
|
||||||
})
|
|
||||||
.join(', ')
|
|
||||||
}]`;
|
|
||||||
|
|
||||||
let main = template
|
|
||||||
.replace(/__app__/g, posixify(path.resolve(__dirname, '../../runtime/app.js')))
|
|
||||||
.replace(/__routes__/g, code)
|
|
||||||
.replace(/__dev__/g, String(dev));
|
|
||||||
|
|
||||||
if (dev) {
|
|
||||||
const hmr_client = posixify(require.resolve(`webpack-hot-middleware/client`));
|
|
||||||
main += `\n\nimport('${hmr_client}?path=/__webpack_hmr&timeout=20000'); if (module.hot) module.hot.accept();`
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(entry.client, main);
|
|
||||||
|
|
||||||
// need to fudge the mtime, because webpack is soft in the head
|
|
||||||
const { atime, mtime } = fs.statSync(entry.client);
|
|
||||||
fs.utimesSync(entry.client, new Date(atime.getTime() - 999999), new Date(mtime.getTime() - 999999));
|
|
||||||
}
|
|
||||||
|
|
||||||
function create_server_routes() {
|
|
||||||
const imports = routes
|
|
||||||
.map(route => {
|
|
||||||
const file = posixify(`${src}/${route.file}`);
|
|
||||||
return route.type === 'page' ?
|
|
||||||
`import ${route.id} from '${file}';` :
|
|
||||||
`import * as ${route.id} from '${file}';`;
|
|
||||||
})
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
const exports = `export { ${routes.map(route => route.id)} };`;
|
|
||||||
|
|
||||||
fs.writeFileSync(entry.server, `${imports}\n\n${exports}`);
|
|
||||||
|
|
||||||
const { atime, mtime } = fs.statSync(entry.server);
|
|
||||||
fs.utimesSync(entry.server, new Date(atime.getTime() - 999999), new Date(mtime.getTime() - 999999));
|
|
||||||
}
|
|
||||||
|
|
||||||
create_client_main();
|
|
||||||
create_server_routes();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dev) {
|
|
||||||
route_manager.onchange(create_app);
|
|
||||||
|
|
||||||
const watcher = chokidar.watch(`templates/main.js`, {
|
|
||||||
ignoreInitial: true
|
|
||||||
});
|
|
||||||
|
|
||||||
watcher.on('add', create_app);
|
|
||||||
watcher.on('change', create_app);
|
|
||||||
watcher.on('unlink', create_app);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = create_app;
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const chalk = require('chalk');
|
|
||||||
const compilers = require('./compilers.js');
|
|
||||||
const generate_asset_cache = require('./generate_asset_cache.js');
|
|
||||||
const { dest } = require('../config.js');
|
|
||||||
|
|
||||||
function deferred() {
|
|
||||||
const d = {};
|
|
||||||
|
|
||||||
d.promise = new Promise((fulfil, reject) => {
|
|
||||||
d.fulfil = fulfil;
|
|
||||||
d.reject = reject;
|
|
||||||
});
|
|
||||||
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function create_watcher() {
|
|
||||||
const deferreds = {
|
|
||||||
client: deferred(),
|
|
||||||
server: deferred()
|
|
||||||
};
|
|
||||||
|
|
||||||
const invalidate = () => Promise.all([
|
|
||||||
deferreds.client.promise,
|
|
||||||
deferreds.server.promise
|
|
||||||
]).then(([client_stats, server_stats]) => {
|
|
||||||
const client_info = client_stats.toJson();
|
|
||||||
fs.writeFileSync(path.join(dest, 'stats.client.json'), JSON.stringify(client_info, null, ' '));
|
|
||||||
|
|
||||||
const server_info = server_stats.toJson();
|
|
||||||
fs.writeFileSync(path.join(dest, 'stats.server.json'), JSON.stringify(server_info, null, ' '));
|
|
||||||
|
|
||||||
return generate_asset_cache(
|
|
||||||
client_stats.toJson(),
|
|
||||||
server_stats.toJson()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
watcher = {
|
|
||||||
ready: invalidate()
|
|
||||||
};
|
|
||||||
|
|
||||||
function watch_compiler(type) {
|
|
||||||
const compiler = compilers[type];
|
|
||||||
|
|
||||||
compiler.plugin('invalid', filename => {
|
|
||||||
console.log(chalk.red(`${type} bundle invalidated, file changed: ${chalk.bold(filename)}`));
|
|
||||||
deferreds[type] = deferred();
|
|
||||||
watcher.ready = invalidate();
|
|
||||||
});
|
|
||||||
|
|
||||||
compiler.plugin('failed', err => {
|
|
||||||
deferreds[type].reject(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
compiler.watch({}, (err, stats) => {
|
|
||||||
if (stats.hasErrors()) {
|
|
||||||
deferreds[type].reject(stats.toJson().errors[0]);
|
|
||||||
} else {
|
|
||||||
deferreds[type].fulfil(stats);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
watch_compiler('client');
|
|
||||||
watch_compiler('server');
|
|
||||||
|
|
||||||
return watcher;
|
|
||||||
};
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const glob = require('glob');
|
|
||||||
const templates = require('../templates.js');
|
|
||||||
const route_manager = require('../route_manager.js');
|
|
||||||
const { dest } = require('../config.js');
|
|
||||||
|
|
||||||
function ensure_array(thing) {
|
|
||||||
return Array.isArray(thing) ? thing : [thing]; // omg webpack what the HELL are you doing
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function generate_asset_cache(clientInfo, serverInfo) {
|
|
||||||
const main_file = `/client/${ensure_array(clientInfo.assetsByChunkName.main)[0]}`;
|
|
||||||
|
|
||||||
const chunk_files = clientInfo.assets.map(chunk => `/client/${chunk.name}`);
|
|
||||||
|
|
||||||
const service_worker = generate_service_worker(chunk_files);
|
|
||||||
const index = generate_index(main_file);
|
|
||||||
|
|
||||||
fs.writeFileSync(path.join(dest, 'service-worker.js'), service_worker);
|
|
||||||
fs.writeFileSync(path.join(dest, 'index.html'), index);
|
|
||||||
|
|
||||||
return {
|
|
||||||
client: {
|
|
||||||
main_file,
|
|
||||||
chunk_files,
|
|
||||||
|
|
||||||
main: read(`${dest}${main_file}`),
|
|
||||||
chunks: chunk_files.reduce((lookup, file) => {
|
|
||||||
lookup[file] = read(`${dest}${file}`);
|
|
||||||
return lookup;
|
|
||||||
}, {}),
|
|
||||||
|
|
||||||
routes: route_manager.routes.reduce((lookup, route) => {
|
|
||||||
lookup[route.id] = `/client/${ensure_array(clientInfo.assetsByChunkName[route.id])[0]}`;
|
|
||||||
return lookup;
|
|
||||||
}, {}),
|
|
||||||
|
|
||||||
index,
|
|
||||||
service_worker
|
|
||||||
},
|
|
||||||
|
|
||||||
server: {
|
|
||||||
entry: path.resolve(dest, 'server', serverInfo.assetsByChunkName.main)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
function generate_service_worker(chunk_files) {
|
|
||||||
const assets = glob.sync('**', { cwd: 'assets', nodir: true });
|
|
||||||
|
|
||||||
const route_code = `[${
|
|
||||||
route_manager.routes
|
|
||||||
.filter(route => route.type === 'page')
|
|
||||||
.map(route => `{ pattern: ${route.pattern} }`)
|
|
||||||
.join(', ')
|
|
||||||
}]`;
|
|
||||||
|
|
||||||
return read('templates/service-worker.js')
|
|
||||||
.replace('__timestamp__', Date.now())
|
|
||||||
.replace('__assets__', JSON.stringify(assets))
|
|
||||||
.replace('__shell__', JSON.stringify(chunk_files.concat('/index.html')))
|
|
||||||
.replace('__routes__', route_code);
|
|
||||||
}
|
|
||||||
|
|
||||||
function generate_index(main_file) {
|
|
||||||
return templates.render(200, {
|
|
||||||
styles: '',
|
|
||||||
head: '',
|
|
||||||
html: '<noscript>Please enable JavaScript!</noscript>',
|
|
||||||
main: main_file
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function read(file) {
|
|
||||||
return fs.readFileSync(file, 'utf-8');
|
|
||||||
}
|
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
|
--require source-map-support/register
|
||||||
--recursive
|
--recursive
|
||||||
test/unit/**/*.js
|
test/unit/**/*.js
|
||||||
|
test/common/test.js
|
||||||
3660
package-lock.json
generated
3660
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
55
package.json
55
package.json
@@ -1,42 +1,73 @@
|
|||||||
{
|
{
|
||||||
"name": "sapper",
|
"name": "sapper",
|
||||||
"version": "0.2.7",
|
"version": "0.6.2",
|
||||||
"description": "Military-grade apps, engineered by Svelte",
|
"description": "Military-grade apps, engineered by Svelte",
|
||||||
"main": "lib/index.js",
|
"main": "middleware.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"sapper": "cli/index.js"
|
"sapper": "cli.js"
|
||||||
},
|
},
|
||||||
|
"files": [
|
||||||
|
"cli.js",
|
||||||
|
"core.js",
|
||||||
|
"middleware.js",
|
||||||
|
"runtime",
|
||||||
|
"runtime.js",
|
||||||
|
"webpack"
|
||||||
|
],
|
||||||
"directories": {
|
"directories": {
|
||||||
"test": "test"
|
"test": "test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^2.3.0",
|
"chalk": "^2.3.0",
|
||||||
|
"cheerio": "^1.0.0-rc.2",
|
||||||
"chokidar": "^1.7.0",
|
"chokidar": "^1.7.0",
|
||||||
|
"code-frame": "^5.0.0",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
|
"express": "^4.16.2",
|
||||||
|
"glob": "^7.1.2",
|
||||||
|
"locate-character": "^2.0.5",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
|
"node-fetch": "^1.7.3",
|
||||||
"relative": "^3.0.2",
|
"relative": "^3.0.2",
|
||||||
"require-relative": "^0.8.7",
|
"require-relative": "^0.8.7",
|
||||||
"rimraf": "^2.6.2",
|
"rimraf": "^2.6.2",
|
||||||
|
"sander": "^0.6.0",
|
||||||
|
"serialize-javascript": "^1.4.0",
|
||||||
|
"url-parse": "^1.2.0",
|
||||||
|
"walk-sync": "^0.3.2",
|
||||||
"webpack": "^3.10.0",
|
"webpack": "^3.10.0",
|
||||||
"webpack-hot-middleware": "^2.21.0"
|
"webpack-hot-middleware": "^2.21.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cypress": "^1.3.0",
|
"@std/esm": "^0.19.7",
|
||||||
|
"@types/glob": "^5.0.34",
|
||||||
|
"@types/mkdirp": "^0.5.2",
|
||||||
|
"@types/rimraf": "^2.0.2",
|
||||||
|
"css-loader": "^0.28.7",
|
||||||
"eslint": "^4.13.1",
|
"eslint": "^4.13.1",
|
||||||
|
"eslint-plugin-import": "^2.8.0",
|
||||||
|
"get-port": "^3.2.0",
|
||||||
"mocha": "^4.0.1",
|
"mocha": "^4.0.1",
|
||||||
|
"nightmare": "^2.10.0",
|
||||||
"npm-run-all": "^4.1.2",
|
"npm-run-all": "^4.1.2",
|
||||||
|
"rollup": "^0.53.0",
|
||||||
|
"rollup-plugin-typescript": "^0.8.1",
|
||||||
|
"source-map-support": "^0.5.2",
|
||||||
|
"style-loader": "^0.19.1",
|
||||||
|
"svelte": "^1.49.1",
|
||||||
|
"svelte-loader": "^2.3.2",
|
||||||
|
"ts-node": "^4.1.0",
|
||||||
|
"tslib": "^1.8.1",
|
||||||
|
"typescript": "^2.6.2",
|
||||||
"wait-on": "^2.0.2"
|
"wait-on": "^2.0.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"cy:open": "cypress open",
|
"cy:open": "cypress open",
|
||||||
"test": "run-s test:unit test:dev test:prod",
|
"test": "mocha --opts mocha.opts",
|
||||||
"test:unit": "mocha --opts mocha.opts",
|
"pretest": "npm run build",
|
||||||
"test:dev": "run-p --race test:launch:dev cy:run:dev",
|
"build": "rollup -c",
|
||||||
"test:launch:dev": "node test/launch.js --dev",
|
"dev": "rollup -cw",
|
||||||
"cy:run:dev": "wait-on http://localhost:3000 && cypress run -s test/cypress/integration/dev.js",
|
"prepublish": "npm test"
|
||||||
"test:prod": "run-p --race test:launch:prod cy:run:prod",
|
|
||||||
"test:launch:prod": "node test/launch.js --prod",
|
|
||||||
"cy:run:prod": "wait-on http://localhost:3000 && cypress run -s test/cypress/integration/prod.js"
|
|
||||||
},
|
},
|
||||||
"repository": "https://github.com/sveltejs/sapper",
|
"repository": "https://github.com/sveltejs/sapper",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
101
rollup.config.js
Normal file
101
rollup.config.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import typescript from 'rollup-plugin-typescript';
|
||||||
|
import pkg from './package.json';
|
||||||
|
|
||||||
|
const external = [].concat(
|
||||||
|
Object.keys(pkg.dependencies),
|
||||||
|
Object.keys(process.binding('natives')),
|
||||||
|
'sapper/core.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
const paths = {
|
||||||
|
'sapper/core.js': './core.js'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default [
|
||||||
|
// cli.js
|
||||||
|
{
|
||||||
|
input: 'src/cli/index.ts',
|
||||||
|
output: {
|
||||||
|
file: 'cli.js',
|
||||||
|
format: 'cjs',
|
||||||
|
banner: '#!/usr/bin/env node',
|
||||||
|
paths,
|
||||||
|
sourcemap: true
|
||||||
|
},
|
||||||
|
external,
|
||||||
|
plugins: [
|
||||||
|
typescript({
|
||||||
|
typescript: require('typescript')
|
||||||
|
})
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// core.js
|
||||||
|
{
|
||||||
|
input: 'src/core/index.ts',
|
||||||
|
output: {
|
||||||
|
file: 'core.js',
|
||||||
|
format: 'cjs',
|
||||||
|
banner: '#!/usr/bin/env node',
|
||||||
|
paths,
|
||||||
|
sourcemap: true
|
||||||
|
},
|
||||||
|
external,
|
||||||
|
plugins: [
|
||||||
|
typescript({
|
||||||
|
typescript: require('typescript')
|
||||||
|
})
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// middleware.js
|
||||||
|
{
|
||||||
|
input: 'src/middleware/index.ts',
|
||||||
|
output: {
|
||||||
|
file: 'middleware.js',
|
||||||
|
format: 'cjs',
|
||||||
|
paths,
|
||||||
|
sourcemap: true
|
||||||
|
},
|
||||||
|
external,
|
||||||
|
plugins: [
|
||||||
|
typescript({
|
||||||
|
typescript: require('typescript')
|
||||||
|
})
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// runtime.js
|
||||||
|
{
|
||||||
|
input: 'src/runtime/index.ts',
|
||||||
|
output: {
|
||||||
|
file: 'runtime.js',
|
||||||
|
format: 'es',
|
||||||
|
paths,
|
||||||
|
sourcemap: true
|
||||||
|
},
|
||||||
|
external,
|
||||||
|
plugins: [
|
||||||
|
typescript({
|
||||||
|
typescript: require('typescript')
|
||||||
|
})
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// webpack/config.js
|
||||||
|
{
|
||||||
|
input: 'src/webpack/index.ts',
|
||||||
|
output: {
|
||||||
|
file: 'webpack/config.js',
|
||||||
|
format: 'cjs',
|
||||||
|
paths,
|
||||||
|
sourcemap: true
|
||||||
|
},
|
||||||
|
external,
|
||||||
|
plugins: [
|
||||||
|
typescript({
|
||||||
|
typescript: require('typescript')
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
1
runtime/README.md
Normal file
1
runtime/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
This directory exists for legacy reasons and should be deleted before releasing version 1.
|
||||||
194
runtime/app.js
194
runtime/app.js
@@ -1,192 +1,2 @@
|
|||||||
const detach = node => {
|
console.error('sapper/runtime/app.js has been deprecated in favour of sapper/runtime.js');
|
||||||
node.parentNode.removeChild(node);
|
export * from '../runtime.js';
|
||||||
};
|
|
||||||
|
|
||||||
export let component;
|
|
||||||
let target;
|
|
||||||
let routes;
|
|
||||||
|
|
||||||
const scroll_history = {};
|
|
||||||
let uid = 1;
|
|
||||||
let cid;
|
|
||||||
|
|
||||||
if ('scrollRestoration' in history) {
|
|
||||||
history.scrollRestoration = 'manual'
|
|
||||||
}
|
|
||||||
|
|
||||||
function select_route(url) {
|
|
||||||
if (url.origin !== window.location.origin) return null;
|
|
||||||
|
|
||||||
for (const route of routes) {
|
|
||||||
const match = route.pattern.exec(url.pathname);
|
|
||||||
if (match) {
|
|
||||||
const params = route.params(match);
|
|
||||||
|
|
||||||
const query = {};
|
|
||||||
for (const [key, value] of url.searchParams) query[key] = value || true;
|
|
||||||
|
|
||||||
return { route, data: { params, query } };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function render(Component, data, scroll) {
|
|
||||||
Promise.resolve(
|
|
||||||
Component.preload ? Component.preload(data) : {}
|
|
||||||
).then(preloaded => {
|
|
||||||
if (component) {
|
|
||||||
component.destroy();
|
|
||||||
} else {
|
|
||||||
// first load — remove SSR'd <head> contents
|
|
||||||
const start = document.querySelector('#sapper-head-start');
|
|
||||||
let end = document.querySelector('#sapper-head-end');
|
|
||||||
|
|
||||||
if (start && end) {
|
|
||||||
while (start.nextSibling !== end) detach(start.nextSibling);
|
|
||||||
detach(start);
|
|
||||||
detach(end);
|
|
||||||
}
|
|
||||||
|
|
||||||
// preload additional routes
|
|
||||||
routes.reduce((promise, route) => promise.then(route.load), Promise.resolve());
|
|
||||||
}
|
|
||||||
|
|
||||||
component = new Component({
|
|
||||||
target,
|
|
||||||
data: Object.assign(data, preloaded),
|
|
||||||
hydrate: !!component
|
|
||||||
});
|
|
||||||
|
|
||||||
if (scroll) {
|
|
||||||
window.scrollTo(scroll.x, scroll.y);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigate(url, id) {
|
|
||||||
const selected = select_route(url);
|
|
||||||
if (selected) {
|
|
||||||
if (id) {
|
|
||||||
// popstate or initial navigation
|
|
||||||
cid = id;
|
|
||||||
} else {
|
|
||||||
// clicked on a link. preserve scroll state
|
|
||||||
scroll_history[cid] = scroll_state();
|
|
||||||
|
|
||||||
id = cid = ++uid;
|
|
||||||
scroll_history[cid] = { x: 0, y: 0 };
|
|
||||||
|
|
||||||
history.pushState({ id }, '', url.href);
|
|
||||||
}
|
|
||||||
|
|
||||||
selected.route.load().then(mod => {
|
|
||||||
render(mod.default, selected.data, scroll_history[id]);
|
|
||||||
});
|
|
||||||
|
|
||||||
cid = id;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handle_click(event) {
|
|
||||||
// Adapted from https://github.com/visionmedia/page.js
|
|
||||||
// MIT license https://github.com/visionmedia/page.js#license
|
|
||||||
if (which(event) !== 1) return;
|
|
||||||
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
|
|
||||||
if (event.defaultPrevented) return;
|
|
||||||
|
|
||||||
const a = findAnchor(event.target);
|
|
||||||
if (!a) return;
|
|
||||||
|
|
||||||
// check if link is inside an svg
|
|
||||||
// in this case, both href and target are always inside an object
|
|
||||||
const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
|
|
||||||
const href = svg ? a.href.baseVal : a.href;
|
|
||||||
|
|
||||||
if (href === window.location.href) {
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore if tag has
|
|
||||||
// 1. 'download' attribute
|
|
||||||
// 2. rel='external' attribute
|
|
||||||
if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
|
|
||||||
|
|
||||||
// Ignore if <a> has a target
|
|
||||||
if (svg ? a.target.baseVal : a.target) return;
|
|
||||||
|
|
||||||
const url = new URL(href);
|
|
||||||
|
|
||||||
// Don't handle hash changes
|
|
||||||
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
|
|
||||||
|
|
||||||
if (navigate(url, null)) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handle_popstate(event) {
|
|
||||||
scroll_history[cid] = scroll_state();
|
|
||||||
|
|
||||||
if (event.state) {
|
|
||||||
navigate(new URL(window.location), event.state.id);
|
|
||||||
} else {
|
|
||||||
// hashchange
|
|
||||||
cid = ++uid;
|
|
||||||
history.replaceState({ id: cid }, '', window.location.href);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function prefetch(event) {
|
|
||||||
const a = findAnchor(event.target);
|
|
||||||
if (!a || a.rel !== 'prefetch') return;
|
|
||||||
|
|
||||||
const selected = select_route(new URL(a.href));
|
|
||||||
|
|
||||||
if (selected) {
|
|
||||||
selected.route.load().then(mod => {
|
|
||||||
if (mod.default.preload) mod.default.preload(selected.data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function findAnchor(node) {
|
|
||||||
while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG <a> elements have a lowercase name
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
let inited;
|
|
||||||
|
|
||||||
export function init(_target, _routes) {
|
|
||||||
target = _target;
|
|
||||||
routes = _routes;
|
|
||||||
|
|
||||||
if (!inited) { // this check makes HMR possible
|
|
||||||
window.addEventListener('click', handle_click);
|
|
||||||
window.addEventListener('popstate', handle_popstate);
|
|
||||||
|
|
||||||
// prefetch
|
|
||||||
window.addEventListener('touchstart', prefetch);
|
|
||||||
window.addEventListener('mouseover', prefetch);
|
|
||||||
|
|
||||||
inited = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scroll = scroll_history[uid] = scroll_state();
|
|
||||||
|
|
||||||
history.replaceState({ id: uid }, '', window.location.href);
|
|
||||||
navigate(new URL(window.location), uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
function which(event) {
|
|
||||||
event = event || window.event;
|
|
||||||
return event.which === null ? event.button : event.which;
|
|
||||||
}
|
|
||||||
|
|
||||||
function scroll_state() {
|
|
||||||
return {
|
|
||||||
x: window.scrollX,
|
|
||||||
y: window.scrollY
|
|
||||||
};
|
|
||||||
}
|
|
||||||
28
src/cli/index.ts
Executable file
28
src/cli/index.ts
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
import { build, export as exporter } from 'sapper/core.js';
|
||||||
|
import { dest, dev, entry, src } from '../config';
|
||||||
|
|
||||||
|
const cmd = process.argv[2];
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
if (cmd === 'build') {
|
||||||
|
build({ dest, dev, entry, src })
|
||||||
|
.then(() => {
|
||||||
|
const elapsed = Date.now() - start;
|
||||||
|
console.error(`built in ${elapsed}ms`); // TODO beautify this, e.g. 'built in 4.7 seconds'
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
||||||
|
});
|
||||||
|
} else if (cmd === 'export') {
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
build({ dest, dev, entry, src })
|
||||||
|
.then(() => exporter({ src, dest }))
|
||||||
|
.then(() => {
|
||||||
|
const elapsed = Date.now() - start;
|
||||||
|
console.error(`extracted in ${elapsed}ms`); // TODO beautify this, e.g. 'built in 4.7 seconds'
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
||||||
|
});
|
||||||
|
}
|
||||||
12
src/config.ts
Normal file
12
src/config.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export const dev = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
|
export const templates = path.resolve(process.env.SAPPER_TEMPLATES || 'templates');
|
||||||
|
export const src = path.resolve(process.env.SAPPER_ROUTES || 'routes');
|
||||||
|
export const dest = path.resolve(process.env.SAPPER_DEST || '.sapper');
|
||||||
|
|
||||||
|
export const entry = {
|
||||||
|
client: path.resolve(templates, '.main.rendered.js'),
|
||||||
|
server: path.resolve(dest, 'server-entry.js')
|
||||||
|
};
|
||||||
62
src/core/build.ts
Normal file
62
src/core/build.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import mkdirp from 'mkdirp';
|
||||||
|
import rimraf from 'rimraf';
|
||||||
|
import create_compilers from './create_compilers.js';
|
||||||
|
import create_app from './create_app.js';
|
||||||
|
import create_assets from './create_assets.js';
|
||||||
|
|
||||||
|
export default function build({
|
||||||
|
src,
|
||||||
|
dest,
|
||||||
|
dev,
|
||||||
|
entry
|
||||||
|
}: {
|
||||||
|
src: string;
|
||||||
|
dest: string;
|
||||||
|
dev: boolean;
|
||||||
|
entry: { client: string, server: string }
|
||||||
|
}) {
|
||||||
|
mkdirp.sync(dest);
|
||||||
|
rimraf.sync(path.join(dest, '**/*'));
|
||||||
|
|
||||||
|
// create main.js and server-routes.js
|
||||||
|
create_app({ dev, entry, src });
|
||||||
|
|
||||||
|
return new Promise((fulfil, reject) => {
|
||||||
|
function handleErrors(err, stats) {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.hasErrors()) {
|
||||||
|
console.error(stats.toString({ colors: true }));
|
||||||
|
reject(new Error(`Encountered errors while building app`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { client, server } = create_compilers();
|
||||||
|
|
||||||
|
client.run((err, client_stats) => {
|
||||||
|
handleErrors(err, client_stats);
|
||||||
|
const client_info = client_stats.toJson();
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dest, 'stats.client.json'),
|
||||||
|
JSON.stringify(client_info, null, ' ')
|
||||||
|
);
|
||||||
|
|
||||||
|
server.run((err, server_stats) => {
|
||||||
|
handleErrors(err, server_stats);
|
||||||
|
const server_info = server_stats.toJson();
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dest, 'stats.server.json'),
|
||||||
|
JSON.stringify(server_info, null, ' ')
|
||||||
|
);
|
||||||
|
|
||||||
|
create_assets({ src, dest, dev, client_info, server_info });
|
||||||
|
fulfil();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
90
src/core/create_app.ts
Normal file
90
src/core/create_app.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import create_routes from './create_routes';
|
||||||
|
|
||||||
|
function posixify(file: string) {
|
||||||
|
return file.replace(/[/\\]/g, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
function fudge_mtime(file: string) {
|
||||||
|
// need to fudge the mtime so that webpack doesn't go doolally
|
||||||
|
const { atime, mtime } = fs.statSync(file);
|
||||||
|
fs.utimesSync(
|
||||||
|
file,
|
||||||
|
new Date(atime.getTime() - 999999),
|
||||||
|
new Date(mtime.getTime() - 999999)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function create_app({
|
||||||
|
src,
|
||||||
|
dev,
|
||||||
|
entry
|
||||||
|
}: {
|
||||||
|
src: string;
|
||||||
|
dev: boolean;
|
||||||
|
entry: { client: string; server: string };
|
||||||
|
}) {
|
||||||
|
const routes = create_routes({ src });
|
||||||
|
|
||||||
|
function create_client_main() {
|
||||||
|
const code = `[${routes
|
||||||
|
.filter(route => route.type === 'page')
|
||||||
|
.map(route => {
|
||||||
|
const params =
|
||||||
|
route.dynamic.length === 0
|
||||||
|
? '{}'
|
||||||
|
: `{ ${route.dynamic
|
||||||
|
.map((part, i) => `${part}: match[${i + 1}]`)
|
||||||
|
.join(', ')} }`;
|
||||||
|
|
||||||
|
const file = posixify(`${src}/${route.file}`);
|
||||||
|
return `{ pattern: ${
|
||||||
|
route.pattern
|
||||||
|
}, params: match => (${params}), load: () => import(/* webpackChunkName: "${
|
||||||
|
route.id
|
||||||
|
}" */ '${file}') }`;
|
||||||
|
})
|
||||||
|
.join(', ')}]`;
|
||||||
|
|
||||||
|
let main = fs
|
||||||
|
.readFileSync('templates/main.js', 'utf-8')
|
||||||
|
.replace(
|
||||||
|
/__app__/g,
|
||||||
|
posixify(path.resolve(__dirname, '../../runtime/app.js'))
|
||||||
|
)
|
||||||
|
.replace(/__routes__/g, code)
|
||||||
|
.replace(/__dev__/g, String(dev));
|
||||||
|
|
||||||
|
if (dev) {
|
||||||
|
const hmr_client = posixify(
|
||||||
|
require.resolve(`webpack-hot-middleware/client`)
|
||||||
|
);
|
||||||
|
main += `\n\nimport('${hmr_client}?path=/__webpack_hmr&timeout=20000'); if (module.hot) module.hot.accept();`;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(entry.client, main);
|
||||||
|
fudge_mtime(entry.client);
|
||||||
|
}
|
||||||
|
|
||||||
|
function create_server_routes() {
|
||||||
|
const imports = routes
|
||||||
|
.map(route => {
|
||||||
|
const file = posixify(`${src}/${route.file}`);
|
||||||
|
return route.type === 'page'
|
||||||
|
? `import ${route.id} from '${file}';`
|
||||||
|
: `import * as ${route.id} from '${file}';`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const exports = `export { ${routes.map(route => route.id)} };`;
|
||||||
|
|
||||||
|
fs.writeFileSync(entry.server, `${imports}\n\n${exports}`);
|
||||||
|
fudge_mtime(entry.server);
|
||||||
|
}
|
||||||
|
|
||||||
|
create_client_main();
|
||||||
|
create_server_routes();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default create_app;
|
||||||
87
src/core/create_assets.ts
Normal file
87
src/core/create_assets.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import glob from 'glob';
|
||||||
|
import { create_templates, render } from './templates';
|
||||||
|
import create_routes from './create_routes';
|
||||||
|
|
||||||
|
function ensure_array(thing) {
|
||||||
|
return Array.isArray(thing) ? thing : [thing]; // omg webpack what the HELL are you doing
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function create_assets({ src, dest, dev, client_info, server_info }) {
|
||||||
|
create_templates(); // TODO refactor this...
|
||||||
|
|
||||||
|
const main_file = `/client/${ensure_array(client_info.assetsByChunkName.main)[0]}`;
|
||||||
|
|
||||||
|
const chunk_files = client_info.assets.map(chunk => `/client/${chunk.name}`);
|
||||||
|
|
||||||
|
const service_worker = generate_service_worker({ chunk_files, src });
|
||||||
|
const index = generate_index(main_file);
|
||||||
|
|
||||||
|
const routes = create_routes({ src });
|
||||||
|
|
||||||
|
if (dev) { // TODO move this into calling code
|
||||||
|
fs.writeFileSync(path.join(dest, 'service-worker.js'), service_worker);
|
||||||
|
fs.writeFileSync(path.join(dest, 'index.html'), index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
client: {
|
||||||
|
main_file,
|
||||||
|
chunk_files,
|
||||||
|
|
||||||
|
main: read(`${dest}${main_file}`),
|
||||||
|
chunks: chunk_files.reduce((lookup, file) => {
|
||||||
|
lookup[file] = read(`${dest}${file}`);
|
||||||
|
return lookup;
|
||||||
|
}, {}),
|
||||||
|
|
||||||
|
// TODO confusing that `routes` refers to an array *and* a lookup
|
||||||
|
routes: routes.reduce((lookup, route) => {
|
||||||
|
lookup[route.id] = `/client/${ensure_array(client_info.assetsByChunkName[route.id])[0]}`;
|
||||||
|
return lookup;
|
||||||
|
}, {}),
|
||||||
|
|
||||||
|
index,
|
||||||
|
service_worker
|
||||||
|
},
|
||||||
|
|
||||||
|
server: {
|
||||||
|
entry: path.resolve(dest, 'server', server_info.assetsByChunkName.main)
|
||||||
|
},
|
||||||
|
|
||||||
|
service_worker
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate_service_worker({ chunk_files, src }) {
|
||||||
|
const assets = glob.sync('**', { cwd: 'assets', nodir: true });
|
||||||
|
|
||||||
|
const routes = create_routes({ src });
|
||||||
|
|
||||||
|
const route_code = `[${
|
||||||
|
routes
|
||||||
|
.filter(route => route.type === 'page')
|
||||||
|
.map(route => `{ pattern: ${route.pattern} }`)
|
||||||
|
.join(', ')
|
||||||
|
}]`;
|
||||||
|
|
||||||
|
return read('templates/service-worker.js')
|
||||||
|
.replace(/__timestamp__/g, Date.now())
|
||||||
|
.replace(/__assets__/g, JSON.stringify(assets))
|
||||||
|
.replace(/__shell__/g, JSON.stringify(chunk_files.concat('/index.html')))
|
||||||
|
.replace(/__routes__/g, route_code);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate_index(main_file) {
|
||||||
|
return render(200, {
|
||||||
|
styles: '',
|
||||||
|
head: '',
|
||||||
|
html: '<noscript>Please enable JavaScript!</noscript>',
|
||||||
|
main: main_file
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function read(file) {
|
||||||
|
return fs.readFileSync(file, 'utf-8');
|
||||||
|
}
|
||||||
16
src/core/create_compilers.ts
Normal file
16
src/core/create_compilers.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import relative from 'require-relative';
|
||||||
|
|
||||||
|
export default function create_compilers() {
|
||||||
|
const webpack = relative('webpack', process.cwd());
|
||||||
|
|
||||||
|
return {
|
||||||
|
client: webpack(
|
||||||
|
require(path.resolve('webpack.client.config.js'))
|
||||||
|
),
|
||||||
|
|
||||||
|
server: webpack(
|
||||||
|
require(path.resolve('webpack.server.config.js'))
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
const path = require('path');
|
import * as path from 'path';
|
||||||
|
import glob from 'glob';
|
||||||
|
|
||||||
module.exports = function create_matchers(files) {
|
export default function create_routes({ src, files = glob.sync('**/*.+(html|js|mjs)', { cwd: src }) }) {
|
||||||
const routes = files
|
const routes = files
|
||||||
.map(file => {
|
.map(file => {
|
||||||
if (/(^|\/|\\)_/.test(file)) return;
|
if (/(^|\/|\\)_/.test(file)) return;
|
||||||
@@ -16,9 +17,22 @@ module.exports = function create_matchers(files) {
|
|||||||
.filter(part => part[0] === '[')
|
.filter(part => part[0] === '[')
|
||||||
.map(part => part.slice(1, -1));
|
.map(part => part.slice(1, -1));
|
||||||
|
|
||||||
const pattern = new RegExp(
|
let pattern_string = '';
|
||||||
`^\\/${parts.map(p => p[0] === '[' ? '([^/]+)' : p).join('\\/')}$`
|
let i = parts.length;
|
||||||
);
|
let nested = true;
|
||||||
|
while (i--) {
|
||||||
|
const part = parts[i];
|
||||||
|
const dynamic = part[0] === '[';
|
||||||
|
|
||||||
|
if (dynamic) {
|
||||||
|
pattern_string = nested ? `(?:\\/([^/]+)${pattern_string})?` : `\\/([^/]+)${pattern_string}`;
|
||||||
|
} else {
|
||||||
|
nested = false;
|
||||||
|
pattern_string = `\\/${part}${pattern_string}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pattern = new RegExp(`^${pattern_string}\\/?$`);
|
||||||
|
|
||||||
const test = url => pattern.test(url);
|
const test = url => pattern.test(url);
|
||||||
|
|
||||||
@@ -58,14 +72,14 @@ module.exports = function create_matchers(files) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!a_part) return 1;
|
if (!a_part) return -1;
|
||||||
if (!b_part) return -1;
|
if (!b_part) return 1;
|
||||||
|
|
||||||
const a_is_dynamic = a_part[0] === '[';
|
const a_is_dynamic = a_part[0] === '[';
|
||||||
const b_is_dynamic = b_part[0] === '[';
|
const b_is_dynamic = b_part[0] === '[';
|
||||||
|
|
||||||
if (a_is_dynamic === b_is_dynamic) {
|
if (a_is_dynamic === b_is_dynamic) {
|
||||||
if (!a_is_dynamic) same = false;
|
if (!a_is_dynamic && a_part !== b_part) same = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
101
src/core/export.ts
Normal file
101
src/core/export.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import * as sander from 'sander';
|
||||||
|
import express from 'express';
|
||||||
|
import cheerio from 'cheerio';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import URL from 'url-parse';
|
||||||
|
import create_assets from './create_assets.js';
|
||||||
|
// import middleware from '../middleware/index.js';
|
||||||
|
|
||||||
|
const { PORT = 3000, OUTPUT_DIR = 'dist' } = process.env;
|
||||||
|
|
||||||
|
const origin = `http://localhost:${PORT}`;
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
function read_json(file) {
|
||||||
|
return JSON.parse(sander.readFileSync(file, { encoding: 'utf-8' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function exporter({ src, dest }) { // TODO dest is a terrible name in this context
|
||||||
|
// Prep output directory
|
||||||
|
sander.rimrafSync(OUTPUT_DIR);
|
||||||
|
|
||||||
|
const { service_worker } = create_assets({
|
||||||
|
src, dest,
|
||||||
|
dev: false,
|
||||||
|
client_info: read_json(path.join(dest, 'stats.client.json')),
|
||||||
|
server_info: read_json(path.join(dest, 'stats.server.json'))
|
||||||
|
});
|
||||||
|
|
||||||
|
sander.copydirSync('assets').to(OUTPUT_DIR);
|
||||||
|
sander.copydirSync(dest, 'client').to(OUTPUT_DIR, 'client');
|
||||||
|
sander.writeFileSync(OUTPUT_DIR, 'service-worker.js', service_worker);
|
||||||
|
|
||||||
|
// Intercept server route fetches
|
||||||
|
function save(res) {
|
||||||
|
res = res.clone();
|
||||||
|
|
||||||
|
return res.text().then(body => {
|
||||||
|
const { pathname } = new URL(res.url);
|
||||||
|
let dest = OUTPUT_DIR + pathname;
|
||||||
|
|
||||||
|
const type = res.headers.get('Content-Type');
|
||||||
|
if (type && type.startsWith('text/html')) dest += '/index.html';
|
||||||
|
|
||||||
|
sander.writeFileSync(dest, body);
|
||||||
|
|
||||||
|
return body;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
global.fetch = (url, opts) => {
|
||||||
|
if (url[0] === '/') {
|
||||||
|
url = `http://localhost:${PORT}${url}`;
|
||||||
|
|
||||||
|
return fetch(url, opts)
|
||||||
|
.then(r => {
|
||||||
|
save(r);
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(url, opts);
|
||||||
|
};
|
||||||
|
|
||||||
|
app.use(require('./middleware')()); // TODO this is filthy
|
||||||
|
const server = app.listen(PORT);
|
||||||
|
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
|
function handle(url) {
|
||||||
|
if (url.origin !== origin) return;
|
||||||
|
|
||||||
|
if (seen.has(url.pathname)) return;
|
||||||
|
seen.add(url.pathname);
|
||||||
|
|
||||||
|
return fetch(url.href)
|
||||||
|
.then(r => {
|
||||||
|
save(r);
|
||||||
|
return r.text();
|
||||||
|
})
|
||||||
|
.then(body => {
|
||||||
|
const $ = cheerio.load(body);
|
||||||
|
const hrefs = [];
|
||||||
|
|
||||||
|
$('a[href]').each((i, $a) => {
|
||||||
|
hrefs.push($a.attribs.href);
|
||||||
|
});
|
||||||
|
|
||||||
|
return hrefs.reduce((promise, href) => {
|
||||||
|
return promise.then(() => handle(new URL(href, url.href)));
|
||||||
|
}, Promise.resolve());
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(`Error rendering ${url.pathname}: ${err.message}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return handle(new URL(origin)) // TODO all static routes
|
||||||
|
.then(() => server.close());
|
||||||
|
}
|
||||||
11
src/core/index.ts
Normal file
11
src/core/index.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { create_templates, render, stream } from './templates'; // TODO templates is an anomaly... fix post-#91
|
||||||
|
|
||||||
|
export { default as build } from './build';
|
||||||
|
export { default as export } from './export.js';
|
||||||
|
|
||||||
|
export { default as create_app } from './create_app';
|
||||||
|
export { default as create_assets } from './create_assets';
|
||||||
|
export { default as create_compilers } from './create_compilers';
|
||||||
|
export { default as create_routes } from './create_routes';
|
||||||
|
|
||||||
|
export const templates = { create_templates, render, stream };
|
||||||
@@ -1,18 +1,45 @@
|
|||||||
const fs = require('fs');
|
import * as fs from 'fs';
|
||||||
const glob = require('glob');
|
import glob from 'glob';
|
||||||
const chokidar = require('chokidar');
|
import chalk from 'chalk';
|
||||||
const { dev } = require('./config.js');
|
import framer from 'code-frame';
|
||||||
|
import { locate } from 'locate-character';
|
||||||
|
|
||||||
let templates;
|
let templates;
|
||||||
|
|
||||||
function create_templates() {
|
function error(e) {
|
||||||
|
if (e.title) console.error(chalk.bold.red(e.title));
|
||||||
|
if (e.body) console.error(chalk.red(e.body));
|
||||||
|
if (e.url) console.error(chalk.cyan(e.url));
|
||||||
|
if (e.frame) console.error(chalk.grey(e.frame));
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function create_templates() {
|
||||||
templates = glob.sync('*.html', { cwd: 'templates' })
|
templates = glob.sync('*.html', { cwd: 'templates' })
|
||||||
.map(file => {
|
.map(file => {
|
||||||
const template = fs.readFileSync(`templates/${file}`, 'utf-8');
|
const template = fs.readFileSync(`templates/${file}`, 'utf-8');
|
||||||
const status = file.replace('.html', '').toLowerCase();
|
const status = file.replace('.html', '').toLowerCase();
|
||||||
|
|
||||||
if (!/^[0-9x]{3}$/.test(status)) {
|
if (!/^[0-9x]{3}$/.test(status)) {
|
||||||
throw new Error(`Bad template — should be a valid status code like 404.html, or a wildcard like 2xx.html`);
|
error({
|
||||||
|
title: `templates/${file}`,
|
||||||
|
body: `Bad template — should be a valid status code like 404.html, or a wildcard like 2xx.html`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = template.indexOf('%sapper.main%');
|
||||||
|
if (index !== -1) {
|
||||||
|
// TODO remove this in a future version
|
||||||
|
const { line, column } = locate(template, index, { offsetLine: 1 });
|
||||||
|
const frame = framer(template, line, column);
|
||||||
|
|
||||||
|
error({
|
||||||
|
title: `templates/${file}`,
|
||||||
|
body: `<script src='%sapper.main%'> is unsupported — use %sapper.scripts% (without the <script> tag) instead`,
|
||||||
|
url: 'https://github.com/sveltejs/sapper/issues/86',
|
||||||
|
frame
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const specificity = (
|
const specificity = (
|
||||||
@@ -31,10 +58,14 @@ function create_templates() {
|
|||||||
return key in data ? data[key] : '';
|
return key in data ? data[key] : '';
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
stream: async (res, data) => {
|
stream: (res, data) => {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
do {
|
function stream_inner() {
|
||||||
|
if (i >= template.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const start = template.indexOf('%sapper', i);
|
const start = template.indexOf('%sapper', i);
|
||||||
|
|
||||||
if (start === -1) {
|
if (start === -1) {
|
||||||
@@ -53,37 +84,32 @@ function create_templates() {
|
|||||||
const match = /sapper\.(\w+)/.exec(tag);
|
const match = /sapper\.(\w+)/.exec(tag);
|
||||||
if (!match || !(match[1] in data)) throw new Error(`Bad template`); // TODO ditto
|
if (!match || !(match[1] in data)) throw new Error(`Bad template`); // TODO ditto
|
||||||
|
|
||||||
res.write(await data[match[1]]);
|
return Promise.resolve(data[match[1]]).then(datamatch => {
|
||||||
i = end + 1;
|
res.write(datamatch);
|
||||||
} while (i < template.length);
|
i = end + 1;
|
||||||
|
return stream_inner();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve().then(stream_inner);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
})
|
})
|
||||||
.sort((a, b) => b.specificity - a.specificity);
|
.sort((a, b) => b.specificity - a.specificity);
|
||||||
|
|
||||||
|
return templates;
|
||||||
}
|
}
|
||||||
|
|
||||||
create_templates();
|
export function render(status, data) {
|
||||||
|
|
||||||
if (dev) {
|
|
||||||
const watcher = chokidar.watch('templates/**.html', {
|
|
||||||
ignoreInitial: true
|
|
||||||
});
|
|
||||||
|
|
||||||
watcher.on('add', create_templates);
|
|
||||||
watcher.on('change', create_templates);
|
|
||||||
watcher.on('unlink', create_templates);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.render = (status, data) => {
|
|
||||||
const template = templates.find(template => template.test(status));
|
const template = templates.find(template => template.test(status));
|
||||||
if (template) return template.render(data);
|
if (template) return template.render(data);
|
||||||
|
|
||||||
return `Missing template for status code ${status}`;
|
return `Missing template for status code ${status}`;
|
||||||
};
|
}
|
||||||
|
|
||||||
exports.stream = (res, status, data) => {
|
export function stream(res, status, data) {
|
||||||
const template = templates.find(template => template.test(status));
|
const template = templates.find(template => template.test(status));
|
||||||
if (template) return template.stream(res, data);
|
if (template) return template.stream(res, data);
|
||||||
|
|
||||||
return `Missing template for status code ${status}`;
|
return `Missing template for status code ${status}`;
|
||||||
};
|
}
|
||||||
103
src/middleware/create_watcher.ts
Normal file
103
src/middleware/create_watcher.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import { create_app, create_assets, create_routes, templates } from 'sapper/core.js';
|
||||||
|
import { dest } from '../config.js';
|
||||||
|
|
||||||
|
function deferred() {
|
||||||
|
const d = {};
|
||||||
|
|
||||||
|
d.promise = new Promise((fulfil, reject) => {
|
||||||
|
d.fulfil = fulfil;
|
||||||
|
d.reject = reject;
|
||||||
|
});
|
||||||
|
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function create_watcher({ compilers, dev, entry, src, onroutes }) {
|
||||||
|
const deferreds = {
|
||||||
|
client: deferred(),
|
||||||
|
server: deferred()
|
||||||
|
};
|
||||||
|
|
||||||
|
const invalidate = () => Promise.all([
|
||||||
|
deferreds.client.promise,
|
||||||
|
deferreds.server.promise
|
||||||
|
]).then(([client_stats, server_stats]) => {
|
||||||
|
const client_info = client_stats.toJson();
|
||||||
|
fs.writeFileSync(path.join(dest, 'stats.client.json'), JSON.stringify(client_info, null, ' '));
|
||||||
|
|
||||||
|
const server_info = server_stats.toJson();
|
||||||
|
fs.writeFileSync(path.join(dest, 'stats.server.json'), JSON.stringify(server_info, null, ' '));
|
||||||
|
|
||||||
|
return create_assets({
|
||||||
|
src, dest, dev,
|
||||||
|
client_info: client_stats.toJson(),
|
||||||
|
server_info: server_stats.toJson()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function watch_compiler(type) {
|
||||||
|
const compiler = compilers[type];
|
||||||
|
|
||||||
|
compiler.plugin('invalid', filename => {
|
||||||
|
console.log(chalk.cyan(`${type} bundle invalidated, file changed: ${chalk.bold(filename)}`));
|
||||||
|
deferreds[type] = deferred();
|
||||||
|
watcher.ready = invalidate();
|
||||||
|
});
|
||||||
|
|
||||||
|
compiler.plugin('failed', err => {
|
||||||
|
deferreds[type].reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return compiler.watch({}, (err, stats) => {
|
||||||
|
if (stats.hasErrors()) {
|
||||||
|
deferreds[type].reject(stats.toJson().errors[0]);
|
||||||
|
} else {
|
||||||
|
deferreds[type].fulfil(stats);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const chokidar = require('chokidar');
|
||||||
|
|
||||||
|
function watch_files(pattern, callback) {
|
||||||
|
const watcher = chokidar.watch(pattern, {
|
||||||
|
persistent: false
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.on('add', callback);
|
||||||
|
watcher.on('change', callback);
|
||||||
|
watcher.on('unlink', callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch_files('routes/**/*.+(html|js|mjs)', () => {
|
||||||
|
const routes = create_routes({ src });
|
||||||
|
onroutes(routes);
|
||||||
|
|
||||||
|
create_app({ dev, entry, src }); // TODO this calls `create_routes` again, we should pass `routes` to `create_app` instead
|
||||||
|
});
|
||||||
|
|
||||||
|
watch_files('templates/main.js', () => {
|
||||||
|
create_app({ dev, entry, src });
|
||||||
|
});
|
||||||
|
|
||||||
|
watch_files('templates/**.html', () => {
|
||||||
|
templates.create_templates();
|
||||||
|
// TODO reload current page?
|
||||||
|
});
|
||||||
|
|
||||||
|
const watcher = {
|
||||||
|
ready: invalidate(),
|
||||||
|
client: watch_compiler('client'),
|
||||||
|
server: watch_compiler('server'),
|
||||||
|
|
||||||
|
close: () => {
|
||||||
|
watcher.client.close();
|
||||||
|
watcher.server.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return watcher;
|
||||||
|
}
|
||||||
282
src/middleware/index.ts
Normal file
282
src/middleware/index.ts
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import mkdirp from 'mkdirp';
|
||||||
|
import rimraf from 'rimraf';
|
||||||
|
import serialize from 'serialize-javascript';
|
||||||
|
import escape_html from 'escape-html';
|
||||||
|
import { create_routes, templates, create_compilers, create_assets } from 'sapper/core.js';
|
||||||
|
import create_watcher from './create_watcher';
|
||||||
|
import { dest, dev, entry, src } from '../config';
|
||||||
|
|
||||||
|
function connect_dev() {
|
||||||
|
mkdirp.sync(dest);
|
||||||
|
rimraf.sync(path.join(dest, '**/*'));
|
||||||
|
|
||||||
|
const compilers = create_compilers();
|
||||||
|
|
||||||
|
let routes;
|
||||||
|
|
||||||
|
const watcher = create_watcher({
|
||||||
|
dev, entry, src,
|
||||||
|
compilers,
|
||||||
|
onroutes: _ => {
|
||||||
|
routes = _;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let asset_cache;
|
||||||
|
|
||||||
|
const middleware = compose_handlers([
|
||||||
|
require('webpack-hot-middleware')(compilers.client, {
|
||||||
|
reload: true,
|
||||||
|
path: '/__webpack_hmr',
|
||||||
|
heartbeat: 10 * 1000
|
||||||
|
}),
|
||||||
|
|
||||||
|
(req, res, next) => {
|
||||||
|
watcher.ready.then(cache => {
|
||||||
|
asset_cache = cache;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
set_req_pathname,
|
||||||
|
|
||||||
|
get_asset_handler({
|
||||||
|
filter: pathname => pathname === '/index.html',
|
||||||
|
type: 'text/html',
|
||||||
|
cache: 'max-age=600',
|
||||||
|
fn: () => asset_cache.client.index
|
||||||
|
}),
|
||||||
|
|
||||||
|
get_asset_handler({
|
||||||
|
filter: pathname => pathname === '/service-worker.js',
|
||||||
|
type: 'application/javascript',
|
||||||
|
cache: 'max-age=600',
|
||||||
|
fn: () => asset_cache.client.service_worker
|
||||||
|
}),
|
||||||
|
|
||||||
|
get_asset_handler({
|
||||||
|
filter: pathname => pathname.startsWith('/client/'),
|
||||||
|
type: 'application/javascript',
|
||||||
|
cache: 'max-age=31536000',
|
||||||
|
fn: pathname => asset_cache.client.chunks[pathname]
|
||||||
|
}),
|
||||||
|
|
||||||
|
get_route_handler(() => asset_cache, () => routes),
|
||||||
|
|
||||||
|
get_not_found_handler(() => asset_cache)
|
||||||
|
]);
|
||||||
|
|
||||||
|
middleware.close = () => {
|
||||||
|
watcher.close();
|
||||||
|
// TODO shut down chokidar
|
||||||
|
};
|
||||||
|
|
||||||
|
return middleware;
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect_prod() {
|
||||||
|
const asset_cache = create_assets({
|
||||||
|
src, dest,
|
||||||
|
dev: false,
|
||||||
|
client_info: read_json(path.join(dest, 'stats.client.json')),
|
||||||
|
server_info: read_json(path.join(dest, 'stats.server.json'))
|
||||||
|
});
|
||||||
|
|
||||||
|
const routes = create_routes({ src }); // TODO rename update
|
||||||
|
|
||||||
|
const middleware = compose_handlers([
|
||||||
|
set_req_pathname,
|
||||||
|
|
||||||
|
get_asset_handler({
|
||||||
|
filter: pathname => pathname === '/index.html',
|
||||||
|
type: 'text/html',
|
||||||
|
cache: 'max-age=600',
|
||||||
|
fn: () => asset_cache.client.index
|
||||||
|
}),
|
||||||
|
|
||||||
|
get_asset_handler({
|
||||||
|
filter: pathname => pathname === '/service-worker.js',
|
||||||
|
type: 'application/javascript',
|
||||||
|
cache: 'max-age=600',
|
||||||
|
fn: () => asset_cache.client.service_worker
|
||||||
|
}),
|
||||||
|
|
||||||
|
get_asset_handler({
|
||||||
|
filter: pathname => pathname.startsWith('/client/'),
|
||||||
|
type: 'application/javascript',
|
||||||
|
cache: 'max-age=31536000',
|
||||||
|
fn: pathname => asset_cache.client.chunks[pathname]
|
||||||
|
}),
|
||||||
|
|
||||||
|
get_route_handler(() => asset_cache, () => routes),
|
||||||
|
|
||||||
|
get_not_found_handler(() => asset_cache)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// here for API consistency between dev, and prod, but
|
||||||
|
// doesn't actually need to do anything
|
||||||
|
middleware.close = () => {};
|
||||||
|
|
||||||
|
return middleware;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default dev ? connect_dev : connect_prod;
|
||||||
|
|
||||||
|
function set_req_pathname(req, res, next) {
|
||||||
|
req.pathname = req.url.replace(/\?.*/, '');
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_asset_handler(opts) {
|
||||||
|
return (req, res, next) => {
|
||||||
|
if (!opts.filter(req.pathname)) return next();
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', opts.type);
|
||||||
|
res.setHeader('Cache-Control', opts.cache);
|
||||||
|
|
||||||
|
res.end(opts.fn(req.pathname));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = Promise.resolve();
|
||||||
|
|
||||||
|
function get_route_handler(get_assets, get_routes) {
|
||||||
|
function handle_route(route, req, res, next, { client, server }) {
|
||||||
|
req.params = route.exec(req.pathname);
|
||||||
|
|
||||||
|
const mod = require(server.entry)[route.id];
|
||||||
|
|
||||||
|
if (route.type === 'page') {
|
||||||
|
// for page routes, we're going to serve some HTML
|
||||||
|
res.setHeader('Content-Type', 'text/html');
|
||||||
|
|
||||||
|
// preload main.js and current route
|
||||||
|
// TODO detect other stuff we can preload? images, CSS, fonts?
|
||||||
|
res.setHeader('Link', `<${client.main_file}>;rel="preload";as="script", <${client.routes[route.id]}>;rel="preload";as="script"`);
|
||||||
|
|
||||||
|
const data = { params: req.params, query: req.query };
|
||||||
|
|
||||||
|
if (mod.preload) {
|
||||||
|
const promise = Promise.resolve(mod.preload(req)).then(preloaded => {
|
||||||
|
const serialized = try_serialize(preloaded);
|
||||||
|
Object.assign(data, preloaded);
|
||||||
|
|
||||||
|
return { rendered: mod.render(data), serialized };
|
||||||
|
});
|
||||||
|
|
||||||
|
return templates.stream(res, 200, {
|
||||||
|
scripts: promise.then(({ serialized }) => {
|
||||||
|
const main = `<script src='${client.main_file}'></script>`;
|
||||||
|
|
||||||
|
if (serialized) {
|
||||||
|
return `<script>__SAPPER__ = { preloaded: ${serialized} };</script>${main}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return main;
|
||||||
|
}),
|
||||||
|
html: promise.then(({ rendered }) => rendered.html),
|
||||||
|
head: promise.then(({ rendered }) => `<noscript id='sapper-head-start'></noscript>${rendered.head}<noscript id='sapper-head-end'></noscript>`),
|
||||||
|
styles: promise.then(({ rendered }) => (rendered.css && rendered.css.code ? `<style>${rendered.css.code}</style>` : ''))
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const { html, head, css } = mod.render(data);
|
||||||
|
|
||||||
|
const page = templates.render(200, {
|
||||||
|
scripts: `<script src='${client.main_file}'></script>`,
|
||||||
|
html,
|
||||||
|
head: `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`,
|
||||||
|
styles: (css && css.code ? `<style>${css.code}</style>` : '')
|
||||||
|
});
|
||||||
|
|
||||||
|
res.end(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
const method = req.method.toLowerCase();
|
||||||
|
// 'delete' cannot be exported from a module because it is a keyword,
|
||||||
|
// so check for 'del' instead
|
||||||
|
const method_export = method === 'delete' ? 'del' : method;
|
||||||
|
const handler = mod[method_export];
|
||||||
|
if (handler) {
|
||||||
|
handler(req, res, next);
|
||||||
|
} else {
|
||||||
|
// no matching handler for method — 404
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return function find_route(req, res, next) {
|
||||||
|
const url = req.pathname;
|
||||||
|
|
||||||
|
resolved
|
||||||
|
.then(() => {
|
||||||
|
const routes = get_routes();
|
||||||
|
for (const route of routes) {
|
||||||
|
if (route.test(url)) return handle_route(route, req, res, next, get_assets());
|
||||||
|
}
|
||||||
|
|
||||||
|
// no matching route — 404
|
||||||
|
next();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.statusCode = 500;
|
||||||
|
res.end(templates.render(500, {
|
||||||
|
title: (err && err.name) || 'Internal server error',
|
||||||
|
url,
|
||||||
|
error: escape_html(err && (err.details || err.message || err) || 'Unknown error'),
|
||||||
|
stack: err && err.stack.split('\n').slice(1).join('\n')
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_not_found_handler(fn) {
|
||||||
|
return function handle_not_found(req, res) {
|
||||||
|
const asset_cache = fn();
|
||||||
|
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end(templates.render(404, {
|
||||||
|
title: 'Not found',
|
||||||
|
status: 404,
|
||||||
|
method: req.method,
|
||||||
|
scripts: `<script src='${asset_cache.client.main_file}'></script>`,
|
||||||
|
url: req.url
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compose_handlers(handlers) {
|
||||||
|
return (req, res, next) => {
|
||||||
|
let i = 0;
|
||||||
|
function go() {
|
||||||
|
const handler = handlers[i];
|
||||||
|
|
||||||
|
if (handler) {
|
||||||
|
handler(req, res, () => {
|
||||||
|
i += 1;
|
||||||
|
go();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function read_json(file) {
|
||||||
|
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function try_serialize(data) {
|
||||||
|
try {
|
||||||
|
return serialize(data);
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
235
src/runtime/index.ts
Normal file
235
src/runtime/index.ts
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import { detach, findAnchor, scroll_state, which } from './utils';
|
||||||
|
import { Component, ComponentConstructor, Params, Query, Route, RouteData, ScrollPosition, Target } from './interfaces';
|
||||||
|
|
||||||
|
export let component: Component;
|
||||||
|
let target: Node;
|
||||||
|
let routes: Route[];
|
||||||
|
|
||||||
|
const history = typeof window !== 'undefined' ? window.history : {
|
||||||
|
pushState: (state: any, title: string, href: string) => {},
|
||||||
|
replaceState: (state: any, title: string, href: string) => {},
|
||||||
|
scrollRestoration: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const scroll_history: Record<string, ScrollPosition> = {};
|
||||||
|
let uid = 1;
|
||||||
|
let cid: number;
|
||||||
|
|
||||||
|
if ('scrollRestoration' in history) {
|
||||||
|
history.scrollRestoration = 'manual';
|
||||||
|
}
|
||||||
|
|
||||||
|
function select_route(url: URL): Target {
|
||||||
|
if (url.origin !== window.location.origin) return null;
|
||||||
|
|
||||||
|
for (const route of routes) {
|
||||||
|
const match = route.pattern.exec(url.pathname);
|
||||||
|
if (match) {
|
||||||
|
const params = route.params(match);
|
||||||
|
|
||||||
|
const query: Record<string, string | true> = {};
|
||||||
|
if (url.search.length > 0) {
|
||||||
|
url.search.slice(1).split('&').forEach(searchParam => {
|
||||||
|
const [, key, value] = /([^=]+)=(.*)/.exec(searchParam);
|
||||||
|
query[key] = value || true;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return { url, route, data: { params, query } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_token: {};
|
||||||
|
|
||||||
|
function render(Component: ComponentConstructor, data: any, scroll: ScrollPosition, token: {}) {
|
||||||
|
if (current_token !== token) return;
|
||||||
|
|
||||||
|
if (component) {
|
||||||
|
component.destroy();
|
||||||
|
} else {
|
||||||
|
// first load — remove SSR'd <head> contents
|
||||||
|
const start = document.querySelector('#sapper-head-start');
|
||||||
|
const end = document.querySelector('#sapper-head-end');
|
||||||
|
|
||||||
|
if (start && end) {
|
||||||
|
while (start.nextSibling !== end) detach(start.nextSibling);
|
||||||
|
detach(start);
|
||||||
|
detach(end);
|
||||||
|
}
|
||||||
|
|
||||||
|
// preload additional routes
|
||||||
|
routes.reduce((promise: Promise<any>, route) => promise.then(route.load), Promise.resolve());
|
||||||
|
}
|
||||||
|
|
||||||
|
component = new Component({
|
||||||
|
target,
|
||||||
|
data,
|
||||||
|
hydrate: !component
|
||||||
|
});
|
||||||
|
|
||||||
|
if (scroll) {
|
||||||
|
window.scrollTo(scroll.x, scroll.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepare_route(Component: ComponentConstructor, data: RouteData) {
|
||||||
|
if (!Component.preload) {
|
||||||
|
return { Component, data };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!component && window.__SAPPER__ && window.__SAPPER__.preloaded) {
|
||||||
|
return { Component, data: Object.assign(data, window.__SAPPER__.preloaded) };
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(Component.preload(data)).then(preloaded => {
|
||||||
|
Object.assign(data, preloaded)
|
||||||
|
return { Component, data };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigate(target: Target, id: number) {
|
||||||
|
if (id) {
|
||||||
|
// popstate or initial navigation
|
||||||
|
cid = id;
|
||||||
|
} else {
|
||||||
|
// clicked on a link. preserve scroll state
|
||||||
|
scroll_history[cid] = scroll_state();
|
||||||
|
|
||||||
|
id = cid = ++uid;
|
||||||
|
scroll_history[cid] = { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
cid = id;
|
||||||
|
|
||||||
|
const loaded = prefetching && prefetching.href === target.url.href ?
|
||||||
|
prefetching.promise :
|
||||||
|
target.route.load().then(mod => prepare_route(mod.default, target.data));
|
||||||
|
|
||||||
|
prefetching = null;
|
||||||
|
|
||||||
|
const token = current_token = {};
|
||||||
|
|
||||||
|
return loaded.then(({ Component, data }) => {
|
||||||
|
render(Component, data, scroll_history[id], token);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_click(event: MouseEvent) {
|
||||||
|
// Adapted from https://github.com/visionmedia/page.js
|
||||||
|
// MIT license https://github.com/visionmedia/page.js#license
|
||||||
|
if (which(event) !== 1) return;
|
||||||
|
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
|
||||||
|
if (event.defaultPrevented) return;
|
||||||
|
|
||||||
|
const a: HTMLAnchorElement | SVGAElement = <HTMLAnchorElement | SVGAElement>findAnchor(<Node>event.target);
|
||||||
|
if (!a) return;
|
||||||
|
|
||||||
|
// check if link is inside an svg
|
||||||
|
// in this case, both href and target are always inside an object
|
||||||
|
const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
|
||||||
|
const href = String(svg ? (<SVGAElement>a).href.baseVal : a.href);
|
||||||
|
|
||||||
|
if (href === window.location.href) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore if tag has
|
||||||
|
// 1. 'download' attribute
|
||||||
|
// 2. rel='external' attribute
|
||||||
|
if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
|
||||||
|
|
||||||
|
// Ignore if <a> has a target
|
||||||
|
if (svg ? (<SVGAElement>a).target.baseVal : a.target) return;
|
||||||
|
|
||||||
|
const url = new URL(href);
|
||||||
|
|
||||||
|
// Don't handle hash changes
|
||||||
|
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
|
||||||
|
|
||||||
|
const target = select_route(url);
|
||||||
|
if (target) {
|
||||||
|
navigate(target, null);
|
||||||
|
event.preventDefault();
|
||||||
|
history.pushState({ id: cid }, '', url.href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_popstate(event: PopStateEvent) {
|
||||||
|
scroll_history[cid] = scroll_state();
|
||||||
|
|
||||||
|
if (event.state) {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
const target = select_route(url);
|
||||||
|
navigate(target, event.state.id);
|
||||||
|
} else {
|
||||||
|
// hashchange
|
||||||
|
cid = ++uid;
|
||||||
|
history.replaceState({ id: cid }, '', window.location.href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prefetching: {
|
||||||
|
href: string;
|
||||||
|
promise: Promise<{ Component: ComponentConstructor, data: any }>;
|
||||||
|
} = null;
|
||||||
|
|
||||||
|
export function prefetch(href: string) {
|
||||||
|
const selected = select_route(new URL(href));
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
prefetching = {
|
||||||
|
href,
|
||||||
|
promise: selected.route.load().then(mod => prepare_route(mod.default, selected.data))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_touchstart_mouseover(event: MouseEvent | TouchEvent) {
|
||||||
|
const a: HTMLAnchorElement = <HTMLAnchorElement>findAnchor(<Node>event.target);
|
||||||
|
if (!a || a.rel !== 'prefetch') return;
|
||||||
|
|
||||||
|
prefetch(a.href);
|
||||||
|
}
|
||||||
|
|
||||||
|
let inited: boolean;
|
||||||
|
|
||||||
|
export function init(_target: Node, _routes: Route[]) {
|
||||||
|
target = _target;
|
||||||
|
routes = _routes;
|
||||||
|
|
||||||
|
if (!inited) { // this check makes HMR possible
|
||||||
|
window.addEventListener('click', handle_click);
|
||||||
|
window.addEventListener('popstate', handle_popstate);
|
||||||
|
|
||||||
|
// prefetch
|
||||||
|
window.addEventListener('touchstart', handle_touchstart_mouseover);
|
||||||
|
window.addEventListener('mouseover', handle_touchstart_mouseover);
|
||||||
|
|
||||||
|
inited = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve().then(() => {
|
||||||
|
const { hash, href } = window.location;
|
||||||
|
|
||||||
|
const deep_linked = hash && document.getElementById(hash.slice(1));
|
||||||
|
scroll_history[uid] = deep_linked ?
|
||||||
|
{ x: 0, y: deep_linked.getBoundingClientRect().top } :
|
||||||
|
scroll_state();
|
||||||
|
|
||||||
|
history.replaceState({ id: uid }, '', href);
|
||||||
|
|
||||||
|
const target = select_route(new URL(window.location.href));
|
||||||
|
return navigate(target, uid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function goto(href: string, opts = { replaceState: false }) {
|
||||||
|
const target = select_route(new URL(href, window.location.href));
|
||||||
|
if (target) {
|
||||||
|
navigate(target, null);
|
||||||
|
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
|
||||||
|
} else {
|
||||||
|
window.location.href = href;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/runtime/interfaces.ts
Normal file
29
src/runtime/interfaces.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
export type Params = Record<string, string>;
|
||||||
|
export type Query = Record<string, string | true>;
|
||||||
|
export type RouteData = { params: Params, query: Query };
|
||||||
|
|
||||||
|
export interface ComponentConstructor {
|
||||||
|
new (options: { target: Node, data: any, hydrate: boolean }): Component;
|
||||||
|
preload: (data: { params: Params, query: Query }) => Promise<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Component {
|
||||||
|
destroy: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Route = {
|
||||||
|
pattern: RegExp;
|
||||||
|
params: (match: RegExpExecArray) => Record<string, string>;
|
||||||
|
load: () => Promise<{ default: ComponentConstructor }>
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScrollPosition = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Target = {
|
||||||
|
url: URL;
|
||||||
|
route: Route;
|
||||||
|
data: RouteData;
|
||||||
|
};
|
||||||
19
src/runtime/utils.ts
Normal file
19
src/runtime/utils.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export function detach(node: Node) {
|
||||||
|
node.parentNode.removeChild(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findAnchor(node: Node) {
|
||||||
|
while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG <a> elements have a lowercase name
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function which(event: MouseEvent) {
|
||||||
|
return event.which === null ? event.button : event.which;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scroll_state() {
|
||||||
|
return {
|
||||||
|
x: window.scrollX,
|
||||||
|
y: window.scrollY
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
const path = require('path');
|
import { dest, dev, entry } from '../config';
|
||||||
const { src, dest, dev, entry } = require('../lib/config.js');
|
|
||||||
|
|
||||||
module.exports = {
|
export default {
|
||||||
dev,
|
dev,
|
||||||
|
|
||||||
client: {
|
client: {
|
||||||
entry: () => {
|
entry: () => {
|
||||||
return {
|
return {
|
||||||
main: entry.client
|
main: [
|
||||||
|
entry.client,
|
||||||
|
// workaround for https://github.com/webpack-contrib/extract-text-webpack-plugin/issues/456
|
||||||
|
'style-loader/lib/addStyles',
|
||||||
|
'css-loader/lib/css-base'
|
||||||
|
]
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -25,7 +29,7 @@ module.exports = {
|
|||||||
entry: () => {
|
entry: () => {
|
||||||
return {
|
return {
|
||||||
main: entry.server
|
main: entry.server
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
output: () => {
|
output: () => {
|
||||||
@@ -37,4 +41,4 @@ module.exports = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
2
test/app/.gitignore
vendored
2
test/app/.gitignore
vendored
@@ -3,3 +3,5 @@ node_modules
|
|||||||
.sapper
|
.sapper
|
||||||
yarn.lock
|
yarn.lock
|
||||||
cypress/screenshots
|
cypress/screenshots
|
||||||
|
templates/.*
|
||||||
|
dist
|
||||||
|
|||||||
6
test/app/package-lock.json
generated
6
test/app/package-lock.json
generated
@@ -2722,9 +2722,9 @@
|
|||||||
"integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ="
|
"integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ="
|
||||||
},
|
},
|
||||||
"marked": {
|
"marked": {
|
||||||
"version": "0.3.7",
|
"version": "0.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-0.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/marked/-/marked-0.3.9.tgz",
|
||||||
"integrity": "sha512-zBEP4qO1YQp5aXHt8S5wTiOv9i2X74V/LQL0zhUNvVaklt6Ywa6lChxIvS+ibYlCGgADwKwZFhjC3+XfpsvQvQ=="
|
"integrity": "sha512-nW5u0dxpXxHfkHzzrveY45gCbi+R4PaO4WRZYqZNl+vB0hVGeqlFn0aOg1c8AKL63TrNFn9Bm2UP4AdiZ9TPLw=="
|
||||||
},
|
},
|
||||||
"math-expression-evaluator": {
|
"math-expression-evaluator": {
|
||||||
"version": "1.2.17",
|
"version": "1.2.17",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"express": "^4.16.2",
|
"express": "^4.16.2",
|
||||||
"extract-text-webpack-plugin": "^3.0.2",
|
"extract-text-webpack-plugin": "^3.0.2",
|
||||||
"glob": "^7.1.2",
|
"glob": "^7.1.2",
|
||||||
"marked": "^0.3.7",
|
"marked": "^0.3.9",
|
||||||
"node-fetch": "^1.7.3",
|
"node-fetch": "^1.7.3",
|
||||||
"npm-run-all": "^4.1.2",
|
"npm-run-all": "^4.1.2",
|
||||||
"serve-static": "^1.13.1",
|
"serve-static": "^1.13.1",
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a class='{{page === "home" ? "selected" : ""}}' href='/'>home</a></li>
|
<li><a href='/'>home</a></li>
|
||||||
<li><a class='{{page === "about" ? "selected" : ""}}' href='/about'>about</a></li>
|
<li><a href='/about'>about</a></li>
|
||||||
|
<li><a href='/slow-preload'>slow preload</a></li>
|
||||||
<!-- for the blog link, we're using rel=prefetch so that Sapper prefetches
|
|
||||||
the blog data when we hover over the link or tap it on a touchscreen -->
|
|
||||||
<li><a rel=prefetch class='{{page === "blog" ? "selected" : ""}}' href='/blog'>blog</a></li>
|
<li><a rel=prefetch class='{{page === "blog" ? "selected" : ""}}' href='/blog'>blog</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -6,14 +6,23 @@
|
|||||||
<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>
|
</Layout>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Layout from './_components/Layout.html';
|
import Layout from './_components/Layout.html';
|
||||||
|
import { goto, prefetch } from '../../../runtime.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Layout
|
Layout
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
goto,
|
||||||
|
prefetch
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -13,7 +13,7 @@ export function get(req, res, next) {
|
|||||||
if (slug in lookup) {
|
if (slug in lookup) {
|
||||||
res.set({
|
res.set({
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Cache-Control': `max-age=${30 * 60 * 1e3}` // cache for 30 minutes
|
'Cache-Control': `no-cache`
|
||||||
});
|
});
|
||||||
|
|
||||||
res.end(lookup[slug]);
|
res.end(lookup[slug]);
|
||||||
|
|||||||
@@ -82,6 +82,30 @@ const posts = [
|
|||||||
html: `
|
html: `
|
||||||
<p>We're so glad you asked! Come on over to the <a href='https://github.com/sveltejs/svelte'>Svelte</a> and <a href='https://github.com/sveltejs/sapper'>Sapper</a> repos, and join us in the <a href='https://gitter.im/sveltejs/svelte'>Gitter chatroom</a>. Everyone is welcome, especially you!</p>
|
<p>We're so glad you asked! Come on over to the <a href='https://github.com/sveltejs/svelte'>Svelte</a> and <a href='https://github.com/sveltejs/sapper'>Sapper</a> repos, and join us in the <a href='https://gitter.im/sveltejs/svelte'>Gitter chatroom</a>. Everyone is welcome, especially you!</p>
|
||||||
`
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: 'A very long post with deep links',
|
||||||
|
slug: 'a-very-long-post',
|
||||||
|
html: `
|
||||||
|
<h2 id='one'>One</h2>
|
||||||
|
<p>I'll have a vodka rocks. (Mom, it's breakfast time.) And a piece of toast. Let me out that Queen. Fried cheese… with club sauce.</p>
|
||||||
|
<p>Her lawyers are claiming the seal is worth $250,000. And that's not even including Buster's Swatch. This was a big get for God. What, so the guy we are meeting with can't even grow his own hair? COME ON! She's always got to wedge herself in the middle of us so that she can control everything. Yeah. Mom's awesome. It's, like, Hey, you want to go down to the whirlpool? Yeah, I don't have a husband. I call it Swing City. The CIA should've just Googled for his hideout, evidently. There are dozens of us! DOZENS! Yeah, like I'm going to take a whiz through this $5,000 suit. COME ON.</p>
|
||||||
|
|
||||||
|
<h2 id='two'>Two</h2>
|
||||||
|
<p>Tobias Fünke costume. Heart attack never stopped old big bear.</p>
|
||||||
|
<p>Nellie is blowing them all AWAY. I will be a bigger and hairier mole than the one on your inner left thigh! I'll sacrifice anything for my children.</p>
|
||||||
|
<p>Up yours, granny! You couldn't handle it! Hey, Dad. Look at you. You're a year older…and a year closer to death. Buster: Oh yeah, I guess that's kind of funny. Bob Loblaw Law Blog. The guy runs a prison, he can have any piece of ass he wants.</p>
|
||||||
|
|
||||||
|
<h2 id='three'>Three</h2>
|
||||||
|
<p>I prematurely shot my wad on what was supposed to be a dry run, so now I'm afraid I have something of a mess on my hands. Dead Dove DO NOT EAT. Never once touched my per diem. I'd go to Craft Service, get some raw veggies, bacon, Cup-A-Soup…baby, I got a stew goin'. You're losing blood, aren't you? Gob: Probably, my socks are wet. Sure, let the little fruit do it. HUZZAH! Although George Michael had only got to second base, he'd gone in head first, like Pete Rose. I will pack your sweet pink mouth with so much ice cream you'll be the envy of every Jerry and Jane on the block!</p>
|
||||||
|
<p>Gosh Mom… after all these years, God's not going to take a call from you. Come on, this is a Bluth family celebration. It's no place for children.</p>
|
||||||
|
<p>And I wouldn't just lie there, if that's what you're thinking. That's not what I WAS thinking. Who? i just dont want him to point out my cracker ass in front of ann. When a man needs to prove to a woman that he's actually… When a man loves a woman… Heyyyyyy Uncle Father Oscar. [Stabbing Gob] White power! Gob: I'm white! Let me take off my assistant's skirt and put on my Barbra-Streisand-in-The-Prince-of-Tides ass-masking therapist pantsuit. In the mid '90s, Tobias formed a folk music band with Lindsay and Maebe which he called Dr. Funke's 100 Percent Natural Good Time Family Band Solution. The group was underwritten by the Natural Food Life Company, a division of Chem-Grow, an Allen Crayne acqusition, which was part of the Squimm Group. Their motto was simple: We keep you alive.</p>
|
||||||
|
|
||||||
|
<h2 id='four'>Four</h2>
|
||||||
|
<p>If you didn't have adult onset diabetes, I wouldn't mind giving you a little sugar. Everybody dance NOW. And the soup of the day is bread. Great, now I'm gonna smell to high heaven like a tuna melt!</p>
|
||||||
|
<p>That's how Tony Wonder lost a nut. She calls it a Mayonegg. Go ahead, touch the Cornballer. There's a new daddy in town. A discipline daddy.</p>
|
||||||
|
`
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
9
test/app/routes/api/delete/[id].js
Normal file
9
test/app/routes/api/delete/[id].js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export function del(req, res) {
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
});
|
||||||
|
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
id: req.params.id
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
preload({ params, query }) {
|
preload({ params, query }) {
|
||||||
return fetch(`/api/blog`).then(r => r.json()).then(posts => {
|
return fetch(`/api/blog/contents`).then(r => r.json()).then(posts => {
|
||||||
return { posts };
|
return { posts };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
15
test/app/routes/delete-test.html
Normal file
15
test/app/routes/delete-test.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<button class='del' on:click='del()'>delete</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
del() {
|
||||||
|
fetch(`/api/delete/42`, { method: 'DELETE' })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
window.deleted = data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -6,13 +6,15 @@
|
|||||||
<h1>Great success!</h1>
|
<h1>Great success!</h1>
|
||||||
|
|
||||||
<figure>
|
<figure>
|
||||||
<img src='/great-success.png'>
|
<img alt='borat' src='/great-success.png'>
|
||||||
<figcaption>HIGH FIVE!</figcaption>
|
<figcaption>HIGH FIVE!</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<p><strong>Try editing this file (routes/index.html) to test hot module reloading.</strong></p>
|
<p><strong>Try editing this file (routes/index.html) to test hot module reloading.</strong></p>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
<div class='hydrate-test'></div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
h1, figure, p {
|
h1, figure, p {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
9
test/app/routes/show-url.html
Normal file
9
test/app/routes/show-url.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<p>URL is {{url}}</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
preload({ url }) {
|
||||||
|
if (url) return { url };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
15
test/app/routes/slow-preload.html
Normal file
15
test/app/routes/slow-preload.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<h1>This page should never render</h1>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
preload() {
|
||||||
|
return new Promise(fulfil => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.fulfil = fulfil;
|
||||||
|
} else {
|
||||||
|
fulfil({});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -10,9 +10,9 @@
|
|||||||
<link rel='icon' type='image/png' href='/favicon.png'>
|
<link rel='icon' type='image/png' href='/favicon.png'>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
if ('serviceWorker' in navigator) {
|
// if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.register('/service-worker.js');
|
// navigator.serviceWorker.register('/service-worker.js');
|
||||||
}
|
// }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Sapper generates a <style> tag containing critical CSS
|
<!-- Sapper generates a <style> tag containing critical CSS
|
||||||
@@ -32,6 +32,6 @@
|
|||||||
<!-- Sapper creates a <script> tag containing `templates/main.js`
|
<!-- Sapper creates a <script> tag containing `templates/main.js`
|
||||||
and anything else it needs to hydrate the app and
|
and anything else it needs to hydrate the app and
|
||||||
initialise the router -->
|
initialise the router -->
|
||||||
<script src='%sapper.main%'></script>
|
%sapper.scripts%
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
import { init } from '__app__';
|
import { init } from '../../../runtime.js';
|
||||||
|
|
||||||
// `routes` is an array of route objects injected by Sapper
|
window.init = () => {
|
||||||
init(document.querySelector('#sapper'), __routes__);
|
return init(document.querySelector('#sapper'), __routes__);
|
||||||
|
};
|
||||||
// if (__dev__) {
|
|
||||||
// // Enable hot-module reloading
|
|
||||||
// import('sapper/webpack/hmr');
|
|
||||||
// if (module.hot) module.hot.accept();
|
|
||||||
// }
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
const config = require('../../webpack/config.js');
|
const config = require('../../webpack/config.js');
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
const ExtractTextPlugin = require("extract-text-webpack-plugin");
|
|
||||||
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: config.client.entry(),
|
entry: config.client.entry(),
|
||||||
@@ -24,27 +22,18 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
config.dev && {
|
{
|
||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
use: [
|
use: [
|
||||||
{ loader: "style-loader" },
|
{ loader: "style-loader" },
|
||||||
{ loader: "css-loader" }
|
{ loader: "css-loader" }
|
||||||
]
|
]
|
||||||
},
|
|
||||||
!config.dev && {
|
|
||||||
test: /\.css$/,
|
|
||||||
use: ExtractTextPlugin.extract({
|
|
||||||
fallback: 'style-loader',
|
|
||||||
use: [{ loader: 'css-loader', options: { sourceMap: config.dev } }]
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
config.dev && new webpack.HotModuleReplacementPlugin(),
|
config.dev && new webpack.HotModuleReplacementPlugin(),
|
||||||
!config.dev && new ExtractTextPlugin('main.css'),
|
!config.dev && new webpack.optimize.ModuleConcatenationPlugin()
|
||||||
!config.dev && new webpack.optimize.ModuleConcatenationPlugin(),
|
|
||||||
!config.dev && new UglifyJSPlugin()
|
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
devtool: config.dev ? 'inline-source-map' : false
|
devtool: config.dev ? 'inline-source-map' : false
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
const config = require('../../webpack/config.js');
|
const config = require('../../webpack/config.js');
|
||||||
const webpack = require('webpack');
|
|
||||||
const ExtractTextPlugin = require("extract-text-webpack-plugin");
|
|
||||||
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: config.server.entry(),
|
entry: config.server.entry(),
|
||||||
|
|||||||
444
test/common/test.js
Normal file
444
test/common/test.js
Normal file
@@ -0,0 +1,444 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const assert = require('assert');
|
||||||
|
const Nightmare = require('nightmare');
|
||||||
|
const express = require('express');
|
||||||
|
const serve = require('serve-static');
|
||||||
|
const walkSync = require('walk-sync');
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
|
run('production');
|
||||||
|
run('development');
|
||||||
|
|
||||||
|
Nightmare.action('page', {
|
||||||
|
title(done) {
|
||||||
|
this.evaluate_now(() => document.querySelector('h1').textContent, done);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function run(env) {
|
||||||
|
describe(`env=${env}`, function () {
|
||||||
|
this.timeout(20000);
|
||||||
|
|
||||||
|
let PORT;
|
||||||
|
let server;
|
||||||
|
let nightmare;
|
||||||
|
let middleware;
|
||||||
|
let capture;
|
||||||
|
|
||||||
|
let base;
|
||||||
|
|
||||||
|
function get(url) {
|
||||||
|
return new Promise(fulfil => {
|
||||||
|
const req = {
|
||||||
|
url,
|
||||||
|
method: 'GET'
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
headers: {},
|
||||||
|
body: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = {
|
||||||
|
setHeader(header, value) {
|
||||||
|
result.headers[header] = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
set(headers, value) {
|
||||||
|
if (typeof headers === 'string') {
|
||||||
|
return res.set({ [headers]: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(result.headers, headers);
|
||||||
|
},
|
||||||
|
|
||||||
|
status(code) {
|
||||||
|
result.status = code;
|
||||||
|
},
|
||||||
|
|
||||||
|
write(data) {
|
||||||
|
result.body += data;
|
||||||
|
},
|
||||||
|
|
||||||
|
end(data) {
|
||||||
|
result.body += data;
|
||||||
|
fulfil(result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
middleware(req, res, () => {
|
||||||
|
fulfil(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
process.chdir(path.resolve(__dirname, '../app'));
|
||||||
|
|
||||||
|
process.env.NODE_ENV = env;
|
||||||
|
|
||||||
|
let exec_promise = Promise.resolve();
|
||||||
|
let sapper;
|
||||||
|
|
||||||
|
if (env === 'production') {
|
||||||
|
const cli = path.resolve(__dirname, '../../cli.js');
|
||||||
|
exec_promise = exec(`node ${cli} export`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return exec_promise.then(() => {
|
||||||
|
const resolved = require.resolve('../../middleware.js');
|
||||||
|
delete require.cache[resolved];
|
||||||
|
delete require.cache[require.resolve('../../core.js')]; // TODO remove this
|
||||||
|
|
||||||
|
sapper = require(resolved);
|
||||||
|
|
||||||
|
return require('get-port')();
|
||||||
|
}).then(port => {
|
||||||
|
PORT = port;
|
||||||
|
base = `http://localhost:${PORT}`;
|
||||||
|
|
||||||
|
Nightmare.action('init', function(done) {
|
||||||
|
this.evaluate_now(() => window.init(), done);
|
||||||
|
});
|
||||||
|
|
||||||
|
global.fetch = (url, opts) => {
|
||||||
|
if (url[0] === '/') url = `${base}${url}`;
|
||||||
|
return fetch(url, opts);
|
||||||
|
};
|
||||||
|
|
||||||
|
let captured;
|
||||||
|
capture = fn => {
|
||||||
|
const result = captured = [];
|
||||||
|
return fn().then(() => {
|
||||||
|
captured = null;
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(serve('assets'));
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (captured) captured.push(req);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
middleware = sapper();
|
||||||
|
app.use(middleware);
|
||||||
|
|
||||||
|
return new Promise((fulfil, reject) => {
|
||||||
|
server = app.listen(PORT, err => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else fulfil();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
server.close();
|
||||||
|
middleware.close();
|
||||||
|
|
||||||
|
// give a chance to clean up
|
||||||
|
return new Promise(fulfil => setTimeout(fulfil, 500));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('basic functionality', () => {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serves /', () => {
|
||||||
|
return nightmare.goto(base).page.title().then(title => {
|
||||||
|
assert.equal(title, 'Great success!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serves /?', () => {
|
||||||
|
return nightmare.goto(`${base}?`).page.title().then(title => {
|
||||||
|
assert.equal(title, 'Great success!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serves static route', () => {
|
||||||
|
return nightmare.goto(`${base}/about`).page.title().then(title => {
|
||||||
|
assert.equal(title, 'About this site');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serves dynamic route', () => {
|
||||||
|
return nightmare.goto(`${base}/blog/what-is-sapper`).page.title().then(title => {
|
||||||
|
assert.equal(title, 'What is Sapper?');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('navigates to a new page without reloading', () => {
|
||||||
|
return nightmare.goto(base).init().wait(100)
|
||||||
|
.then(() => {
|
||||||
|
return capture(() => nightmare.click('a[href="/about"]'));
|
||||||
|
})
|
||||||
|
.then(requests => {
|
||||||
|
assert.deepEqual(requests.map(r => r.url), []);
|
||||||
|
return nightmare.path();
|
||||||
|
})
|
||||||
|
.then(path => {
|
||||||
|
assert.equal(path, '/about');
|
||||||
|
return nightmare.title();
|
||||||
|
})
|
||||||
|
.then(title => {
|
||||||
|
assert.equal(title, 'About');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('navigates programmatically', () => {
|
||||||
|
return nightmare
|
||||||
|
.goto(`${base}/about`)
|
||||||
|
.init()
|
||||||
|
.click('.goto')
|
||||||
|
.wait(() => window.location.pathname === '/blog/what-is-sapper')
|
||||||
|
.wait(100)
|
||||||
|
.title()
|
||||||
|
.then(title => {
|
||||||
|
assert.equal(title, 'What is Sapper?');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefetches programmatically', () => {
|
||||||
|
return nightmare
|
||||||
|
.goto(`${base}/about`)
|
||||||
|
.init()
|
||||||
|
.then(() => {
|
||||||
|
return capture(() => {
|
||||||
|
return nightmare
|
||||||
|
.click('.prefetch')
|
||||||
|
.wait(100);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(requests => {
|
||||||
|
assert.ok(!!requests.find(r => r.url === '/api/blog/why-the-name'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scrolls to active deeplink', () => {
|
||||||
|
return nightmare
|
||||||
|
.goto(`${base}/blog/a-very-long-post#four`)
|
||||||
|
.init()
|
||||||
|
.evaluate(() => window.scrollY)
|
||||||
|
.then(scrollY => {
|
||||||
|
assert.ok(scrollY > 0, scrollY);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reuses prefetch promise', () => {
|
||||||
|
return nightmare
|
||||||
|
.goto(`${base}/blog`)
|
||||||
|
.init().wait(100)
|
||||||
|
.then(() => {
|
||||||
|
return capture(() => {
|
||||||
|
return nightmare
|
||||||
|
.mouseover('[href="/blog/what-is-sapper"]')
|
||||||
|
.wait(200);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(mouseover_requests => {
|
||||||
|
assert.deepEqual(mouseover_requests.map(r => r.url), [
|
||||||
|
'/api/blog/what-is-sapper'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return capture(() => {
|
||||||
|
return nightmare
|
||||||
|
.click('[href="/blog/what-is-sapper"]')
|
||||||
|
.wait(200);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(click_requests => {
|
||||||
|
assert.deepEqual(click_requests.map(r => r.url), []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cancels navigation if subsequent navigation occurs during preload', () => {
|
||||||
|
return nightmare
|
||||||
|
.goto(base)
|
||||||
|
.init()
|
||||||
|
.click('a[href="/slow-preload"]')
|
||||||
|
.wait(100)
|
||||||
|
.click('a[href="/about"]')
|
||||||
|
.wait(100)
|
||||||
|
.then(() => nightmare.path())
|
||||||
|
.then(path => {
|
||||||
|
assert.equal(path, '/about');
|
||||||
|
return nightmare.title();
|
||||||
|
})
|
||||||
|
.then(title => {
|
||||||
|
assert.equal(title, 'About');
|
||||||
|
return nightmare.evaluate(() => window.fulfil({})).wait(100);
|
||||||
|
})
|
||||||
|
.then(() => nightmare.path())
|
||||||
|
.then(path => {
|
||||||
|
assert.equal(path, '/about');
|
||||||
|
return nightmare.title();
|
||||||
|
})
|
||||||
|
.then(title => {
|
||||||
|
assert.equal(title, 'About');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes entire request object to preload', () => {
|
||||||
|
return nightmare
|
||||||
|
.goto(`${base}/show-url`)
|
||||||
|
.init()
|
||||||
|
.evaluate(() => document.querySelector('p').innerHTML)
|
||||||
|
.end().then(html => {
|
||||||
|
assert.equal(html, `URL is /show-url`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls a delete handler', () => {
|
||||||
|
return nightmare
|
||||||
|
.goto(`${base}/delete-test`)
|
||||||
|
.init()
|
||||||
|
.click('.del')
|
||||||
|
.wait(() => window.deleted)
|
||||||
|
.evaluate(() => window.deleted.id)
|
||||||
|
.then(id => {
|
||||||
|
assert.equal(id, 42);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hydrates initial route', () => {
|
||||||
|
return nightmare.goto(base)
|
||||||
|
.wait('.hydrate-test')
|
||||||
|
.evaluate(() => {
|
||||||
|
window.el = document.querySelector('.hydrate-test');
|
||||||
|
})
|
||||||
|
.init()
|
||||||
|
.evaluate(() => {
|
||||||
|
return document.querySelector('.hydrate-test') === window.el;
|
||||||
|
})
|
||||||
|
.then(matches => {
|
||||||
|
assert.ok(matches);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('headers', () => {
|
||||||
|
it('sets Content-Type and Link...preload headers', () => {
|
||||||
|
return get('/').then(({ headers }) => {
|
||||||
|
assert.equal(
|
||||||
|
headers['Content-Type'],
|
||||||
|
'text/html'
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
/<\/client\/main.\w+\.js>;rel="preload";as="script", <\/client\/_.\d+.\w+.js>;rel="preload";as="script"/.test(headers['Link']),
|
||||||
|
headers['Link']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (env === 'production') {
|
||||||
|
describe('export', () => {
|
||||||
|
it('export all pages', () => {
|
||||||
|
const dest = path.resolve(__dirname, '../app/dist');
|
||||||
|
|
||||||
|
// Pages that should show up in the extraction directory.
|
||||||
|
const expectedPages = [
|
||||||
|
'index.html',
|
||||||
|
'about/index.html',
|
||||||
|
'slow-preload/index.html',
|
||||||
|
|
||||||
|
'blog/index.html',
|
||||||
|
'blog/a-very-long-post/index.html',
|
||||||
|
'blog/how-can-i-get-involved/index.html',
|
||||||
|
'blog/how-is-sapper-different-from-next/index.html',
|
||||||
|
'blog/how-to-use-sapper/index.html',
|
||||||
|
'blog/what-is-sapper/index.html',
|
||||||
|
'blog/why-the-name/index.html',
|
||||||
|
|
||||||
|
'api/blog/contents',
|
||||||
|
'api/blog/a-very-long-post',
|
||||||
|
'api/blog/how-can-i-get-involved',
|
||||||
|
'api/blog/how-is-sapper-different-from-next',
|
||||||
|
'api/blog/how-to-use-sapper',
|
||||||
|
'api/blog/what-is-sapper',
|
||||||
|
'api/blog/why-the-name',
|
||||||
|
|
||||||
|
'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\/_\..*?\.js/,
|
||||||
|
/client\/about\..*?\.js/,
|
||||||
|
/client\/blog_\$slug\$\..*?\.js/,
|
||||||
|
/client\/blog\..*?\.js/,
|
||||||
|
/client\/main\..*?\.js/,
|
||||||
|
/client\/show_url\..*?\.js/,
|
||||||
|
/client\/slow_preload\..*?\.js/,
|
||||||
|
];
|
||||||
|
const allPages = walkSync(dest);
|
||||||
|
|
||||||
|
expectedPages.forEach((expectedPage) => {
|
||||||
|
assert.ok(allPages.includes(expectedPage),
|
||||||
|
`Could not find page matching ${expectedPage}`);
|
||||||
|
});
|
||||||
|
expectedClientRegexes.forEach((expectedRegex) => {
|
||||||
|
// Ensure each client page regular expression matches at least one
|
||||||
|
// generated page.
|
||||||
|
let matched = false;
|
||||||
|
for (const page of allPages) {
|
||||||
|
if (expectedRegex.test(page)) {
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.ok(matched,
|
||||||
|
`Could not find client page matching ${expectedRegex}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function exec(cmd) {
|
||||||
|
return new Promise((fulfil, reject) => {
|
||||||
|
const parts = cmd.split(' ');
|
||||||
|
const proc = require('child_process').spawn(parts.shift(), parts);
|
||||||
|
|
||||||
|
proc.stdout.on('data', data => {
|
||||||
|
process.stdout.write(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.stderr.on('data', data => {
|
||||||
|
process.stderr.write(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on('error', reject);
|
||||||
|
|
||||||
|
proc.on('close', () => fulfil());
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Using fixtures to represent data",
|
|
||||||
"email": "hello@cypress.io",
|
|
||||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
describe('dev mode', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('/')
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has the correct <h1>', () => {
|
|
||||||
cy.contains('h1', 'Great success!')
|
|
||||||
});
|
|
||||||
|
|
||||||
it('navigates to /about', () => {
|
|
||||||
cy.get('nav a').contains('about').click();
|
|
||||||
cy.url().should('include', '/about');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('navigates to /blog', () => {
|
|
||||||
cy.get('nav a').contains('blog').click();
|
|
||||||
cy.url().should('include', '/blog');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
describe('prod mode', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('/')
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has the correct <h1>', () => {
|
|
||||||
cy.contains('h1', 'Great success!')
|
|
||||||
});
|
|
||||||
|
|
||||||
it('navigates to /about', () => {
|
|
||||||
cy.get('nav a').contains('about').click();
|
|
||||||
cy.url().should('include', '/about');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('navigates to /blog', () => {
|
|
||||||
cy.get('nav a').contains('blog').click();
|
|
||||||
cy.url().should('include', '/blog');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
// ***********************************************************
|
|
||||||
// This example plugins/index.js can be used to load plugins
|
|
||||||
//
|
|
||||||
// You can change the location of this file or turn off loading
|
|
||||||
// the plugins file with the 'pluginsFile' configuration option.
|
|
||||||
//
|
|
||||||
// You can read more here:
|
|
||||||
// https://on.cypress.io/plugins-guide
|
|
||||||
// ***********************************************************
|
|
||||||
|
|
||||||
// This function is called when a project is opened or re-opened (e.g. due to
|
|
||||||
// the project's config changing)
|
|
||||||
|
|
||||||
module.exports = (on, config) => {
|
|
||||||
// `on` is used to hook into various events Cypress emits
|
|
||||||
// `config` is the resolved Cypress config
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 331 KiB |
@@ -1,25 +0,0 @@
|
|||||||
// ***********************************************
|
|
||||||
// This example commands.js shows you how to
|
|
||||||
// create various custom commands and overwrite
|
|
||||||
// existing commands.
|
|
||||||
//
|
|
||||||
// For more comprehensive examples of custom
|
|
||||||
// commands please read more here:
|
|
||||||
// https://on.cypress.io/custom-commands
|
|
||||||
// ***********************************************
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is a parent command --
|
|
||||||
// Cypress.Commands.add("login", (email, password) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is a child command --
|
|
||||||
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is a dual command --
|
|
||||||
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is will overwrite an existing command --
|
|
||||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// ***********************************************************
|
|
||||||
// This example support/index.js is processed and
|
|
||||||
// loaded automatically before your test files.
|
|
||||||
//
|
|
||||||
// This is a great place to put global configuration and
|
|
||||||
// behavior that modifies Cypress.
|
|
||||||
//
|
|
||||||
// You can change the location of this file or turn off
|
|
||||||
// automatically serving support files with the
|
|
||||||
// 'supportFile' configuration option.
|
|
||||||
//
|
|
||||||
// You can read more here:
|
|
||||||
// https://on.cypress.io/configuration
|
|
||||||
// ***********************************************************
|
|
||||||
|
|
||||||
// Import commands.js using ES2015 syntax:
|
|
||||||
import './commands'
|
|
||||||
|
|
||||||
// Alternatively you can use CommonJS syntax:
|
|
||||||
// require('./commands')
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const rimraf = require('rimraf');
|
|
||||||
const child_process = require('child_process');
|
|
||||||
|
|
||||||
// ensure sapper doesn't exist in app/node_modules
|
|
||||||
rimraf.sync(
|
|
||||||
path.join(__dirname, 'app/node_modules/sapper')
|
|
||||||
);
|
|
||||||
|
|
||||||
rimraf.sync(
|
|
||||||
path.join(__dirname, 'app/node_modules/.bin/sapper')
|
|
||||||
);
|
|
||||||
|
|
||||||
// create symlinks
|
|
||||||
fs.symlinkSync(
|
|
||||||
path.join(__dirname, '..'),
|
|
||||||
path.join(__dirname, 'app/node_modules/sapper')
|
|
||||||
);
|
|
||||||
|
|
||||||
fs.symlinkSync(
|
|
||||||
path.join(__dirname, '../cli/index.js'),
|
|
||||||
path.join(__dirname, 'app/node_modules/.bin/sapper')
|
|
||||||
);
|
|
||||||
|
|
||||||
const app_dir = path.join(__dirname, 'app');
|
|
||||||
|
|
||||||
function start_server() {
|
|
||||||
const server = child_process.spawn(process.execPath, ['server.js'], {
|
|
||||||
cwd: app_dir,
|
|
||||||
env: {
|
|
||||||
NODE_ENV: 'development'
|
|
||||||
},
|
|
||||||
stdio: 'pipe'
|
|
||||||
});
|
|
||||||
|
|
||||||
server.stdout.on('data', (data) => {
|
|
||||||
process.stdout.write(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
server.stderr.on('data', (data) => {
|
|
||||||
process.stderr.write(data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function launch() {
|
|
||||||
if (process.argv[2] === '--dev') {
|
|
||||||
start_server();
|
|
||||||
} else {
|
|
||||||
child_process.exec(`npm run build`, {
|
|
||||||
cwd: app_dir
|
|
||||||
}, (err, stdout, stderr) => {
|
|
||||||
if (err) throw err;
|
|
||||||
|
|
||||||
process.stdout.write(stdout);
|
|
||||||
process.stderr.write(stderr);
|
|
||||||
start_server();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is a terrible hack
|
|
||||||
if (process.env.APPVEYOR) {
|
|
||||||
child_process.exec(`npm install`, {
|
|
||||||
cwd: app_dir
|
|
||||||
}, (err, stdout, stderr) => {
|
|
||||||
if (err) throw err;
|
|
||||||
|
|
||||||
process.stdout.write(stdout);
|
|
||||||
process.stderr.write(stderr);
|
|
||||||
launch();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
launch();
|
|
||||||
}
|
|
||||||
@@ -1,27 +1,29 @@
|
|||||||
const path = require('path');
|
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
const { create_routes } = require('../../core.js');
|
||||||
const create_routes = require('../../lib/utils/create_routes.js');
|
|
||||||
|
|
||||||
describe('create_routes', () => {
|
describe('create_routes', () => {
|
||||||
it('sorts routes correctly', () => {
|
it('sorts routes correctly', () => {
|
||||||
const routes = create_routes(['index.html', 'about.html', '[wildcard].html', 'post/foo.html', 'post/[id].html', 'post/bar.html']);
|
const routes = create_routes({
|
||||||
|
files: ['index.html', 'about.html', '[wildcard].html', 'post/foo.html', 'post/[id].html', 'post/bar.html']
|
||||||
|
});
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
routes.map(r => r.file),
|
routes.map(r => r.file),
|
||||||
[
|
[
|
||||||
|
'index.html',
|
||||||
|
'about.html',
|
||||||
'post/foo.html',
|
'post/foo.html',
|
||||||
'post/bar.html',
|
'post/bar.html',
|
||||||
'post/[id].html',
|
'post/[id].html',
|
||||||
'about.html',
|
'[wildcard].html'
|
||||||
'[wildcard].html',
|
|
||||||
'index.html'
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('generates params', () => {
|
it('generates params', () => {
|
||||||
const routes = create_routes(['index.html', 'about.html', '[wildcard].html', 'post/[id].html']);
|
const routes = create_routes({
|
||||||
|
files: ['index.html', 'about.html', '[wildcard].html', 'post/[id].html']
|
||||||
|
});
|
||||||
|
|
||||||
let file;
|
let file;
|
||||||
let params;
|
let params;
|
||||||
@@ -40,20 +42,26 @@ describe('create_routes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('ignores files and directories with leading underscores', () => {
|
it('ignores files and directories with leading underscores', () => {
|
||||||
const routes = create_routes(['index.html', '_foo.html', 'a/_b/c/d.html', 'e/f/g/h.html', 'i/_j.html']);
|
const routes = create_routes({
|
||||||
|
files: ['index.html', '_foo.html', 'a/_b/c/d.html', 'e/f/g/h.html', 'i/_j.html']
|
||||||
|
});
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
routes.map(r => r.file),
|
routes.map(r => r.file),
|
||||||
[
|
[
|
||||||
'e/f/g/h.html',
|
'index.html',
|
||||||
'index.html'
|
'e/f/g/h.html'
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('matches /foo/:bar before /:baz/qux', () => {
|
it('matches /foo/:bar before /:baz/qux', () => {
|
||||||
const a = create_routes(['foo/[bar].html', '[baz]/qux.html']);
|
const a = create_routes({
|
||||||
const b = create_routes(['[baz]/qux.html', 'foo/[bar].html']);
|
files: ['foo/[bar].html', '[baz]/qux.html']
|
||||||
|
});
|
||||||
|
const b = create_routes({
|
||||||
|
files: ['[baz]/qux.html', 'foo/[bar].html']
|
||||||
|
});
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
a.map(r => r.file),
|
a.map(r => r.file),
|
||||||
@@ -68,7 +76,61 @@ describe('create_routes', () => {
|
|||||||
|
|
||||||
it('fails if routes are indistinguishable', () => {
|
it('fails if routes are indistinguishable', () => {
|
||||||
assert.throws(() => {
|
assert.throws(() => {
|
||||||
create_routes(['[foo].html', '[bar]/index.html']);
|
create_routes({
|
||||||
|
files: ['[foo].html', '[bar]/index.html']
|
||||||
|
});
|
||||||
}, /The \[foo\].html and \[bar\]\/index.html routes clash/);
|
}, /The \[foo\].html and \[bar\]\/index.html routes clash/);
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
create_routes({
|
||||||
|
files: ['foo.html', 'foo.js']
|
||||||
|
});
|
||||||
|
}, /The foo.html and foo.js routes clash/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches nested routes', () => {
|
||||||
|
const route = create_routes({
|
||||||
|
files: ['settings/[submenu].html']
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
assert.deepEqual(route.exec('/settings/foo'), {
|
||||||
|
submenu: 'foo'
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(route.exec('/settings'), {
|
||||||
|
submenu: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefers index routes to nested routes', () => {
|
||||||
|
const routes = create_routes({
|
||||||
|
files: ['settings/[submenu].html', 'settings.html']
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
routes.map(r => r.file),
|
||||||
|
['settings.html', 'settings/[submenu].html']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches deeply nested routes', () => {
|
||||||
|
const route = create_routes({
|
||||||
|
files: ['settings/[a]/[b]/index.html']
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
assert.deepEqual(route.exec('/settings/foo/bar'), {
|
||||||
|
a: 'foo',
|
||||||
|
b: 'bar'
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(route.exec('/settings/foo'), {
|
||||||
|
a: 'foo',
|
||||||
|
b: null
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(route.exec('/settings'), {
|
||||||
|
a: null,
|
||||||
|
b: null
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"diagnostics": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"noEmitOnError": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"lib": ["es5", "es6", "dom"],
|
||||||
|
"importHelpers": true
|
||||||
|
},
|
||||||
|
"target": "ES5",
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
import 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000';
|
|
||||||
Reference in New Issue
Block a user