Compare commits

...

68 Commits

Author SHA1 Message Date
Rich Harris
a6dc61a182 -> v0.5.1 2018-01-16 08:47:50 -05:00
Rich Harris
96666d05ec only write to filesystem in dev mode 2018-01-16 08:47:44 -05:00
Rich Harris
6390ba692b -> v0.5.0 2018-01-15 10:28:48 -05:00
Rich Harris
0e131cc81e Merge pull request #88 from sveltejs/gh-9
Exporting
2018-01-15 10:24:35 -05:00
Rich Harris
bd3d5713cb Merge pull request #64 from lukeed/lazy-chokidar
Lazily Require Chokidar
2018-01-15 10:04:37 -05:00
Rich Harris
9ec23c47ad update test 2018-01-15 10:04:24 -05:00
Rich Harris
b7bb69925e rename extract to export, for familiarity to next.js devs 2018-01-15 10:03:27 -05:00
Rich Harris
25124f6ee7 Merge branch 'master' into lazy-chokidar 2018-01-15 09:58:39 -05:00
Rich Harris
73d491cd19 Merge branch 'master' into lazy-chokidar 2018-01-15 09:58:03 -05:00
Rich Harris
e25fceb4b8 node 6 is the new IE 2018-01-15 09:36:12 -05:00
Rich Harris
3807147c57 add missing dependency 2018-01-14 22:45:59 -05:00
Rich Harris
a523ba58ff save pages as index.html, ignore hashes 2018-01-14 22:45:51 -05:00
Rich Harris
fe03fd3a52 update lockfile 2018-01-14 18:21:37 -05:00
Rich Harris
89c430a0cb slightly different approach to extracting 2018-01-14 18:19:51 -05:00
Rich Harris
8ef312849c always write service-worker.js and shell index.html file 2018-01-14 18:16:10 -05:00
Rich Harris
4200446684 ignore trailing slash in pathnames 2018-01-14 18:15:40 -05:00
Rich Harris
681ed005b8 remove unused deps 2018-01-14 18:15:12 -05:00
Rich Harris
d457af8d51 update test 2018-01-14 18:14:07 -05:00
Rich Harris
0c158b9e1f Merge branch 'master' of https://github.com/freedmand/sapper into freedmand-master 2018-01-14 13:48:07 -05:00
Rich Harris
50011e2077 always print stdout/stderr, to avoid wild goose chase debugging 2018-01-14 13:47:23 -05:00
Rich Harris
f27b7973e3 Build before extracting 2018-01-14 13:46:25 -05:00
Rich Harris
2af2ab3cb9 Ensure output dir exists, return Promise 2018-01-14 13:45:47 -05:00
Rich Harris
6a4dc1901c Enforce prod mode, return a Promise so it can be used programmatically 2018-01-14 13:45:01 -05:00
Rich Harris
fbbc0e9e19 Merge branch 'master' of https://github.com/freedmand/sapper into freedmand-master 2018-01-14 12:29:15 -05:00
Rich Harris
1213c3da46 fix lockfile mess 2018-01-14 12:27:35 -05:00
Rich Harris
4cc2104088 add back test that got lost in a merge conflict somewhere 2018-01-14 12:23:07 -05:00
Rich Harris
d6dda371ca typo 2018-01-14 12:22:41 -05:00
Rich Harris
304c06085e Merge branch 'master' into master 2018-01-14 12:19:47 -05:00
Rich Harris
33b6450e34 Merge branch 'master' into master 2018-01-14 12:16:09 -05:00
Rich Harris
8faa98af6a -> v0.4.0 2018-01-14 12:07:06 -05:00
Rich Harris
14df138528 Merge pull request #87 from sveltejs/gh-3
Serialize preloaded data for initial page and serve to client
2018-01-14 11:50:18 -05:00
Rich Harris
44285cdb2f serialized preloaded data and send to client as initial payload - fixes #3 2018-01-14 11:22:00 -05:00
Rich Harris
bd656cfd5b Merge branch 'fix-tests-without-async' into gh-3 2018-01-14 10:45:55 -05:00
Rich Harris
c4b4bd587d use %sapper.scripts% 2018-01-14 10:45:21 -05:00
Rich Harris
2abfdb03d5 wait for window.READY so that this tests we dont double-preload 2018-01-14 10:44:38 -05:00
Rich Harris
a80ac3a8b8 change tests back to non-async versions 2018-01-14 00:43:57 -05:00
Rich Harris
887cb09386 merge 2018-01-14 00:21:55 -05:00
Rich Harris
cfeeafded4 Merge pull request #84 from sveltejs/test-in-node-6
test in node 6
2018-01-14 00:18:23 -05:00
Rich Harris
2cae674033 Merge branch 'master' into test-in-node-6 2018-01-14 00:14:40 -05:00
Rich Harris
7c0f32662d remove async from new test 2018-01-14 00:10:20 -05:00
Rich Harris
b4fb1c3268 Merge branch 'master' of github.com:sveltejs/sapper 2018-01-14 00:02:32 -05:00
Rich Harris
ecd0f673a9 test in node 6 2018-01-14 00:01:51 -05:00
Rich Harris
40d16852f7 prevent unnecessary promise chains, and hoist handler function 2018-01-14 00:01:10 -05:00
Rich Harris
133be03791 Merge pull request #72 from nolanlawson/fix-style-loader
Explicitly load style-loader/css-loader deps
2018-01-13 23:33:38 -05:00
Rich Harris
727a76ebb5 fall through to 404 if no handler exists for method 2018-01-13 23:29:41 -05:00
Rich Harris
e3c047831a add test for #77 2018-01-13 23:29:16 -05:00
Emil Tholin
81b5e0d764 Handle DELETE requests with del export 2018-01-10 19:24:22 +01:00
Emil Tholin
98e904dcfc Remove async/await from test.js 2018-01-07 12:22:40 +01:00
Nolan Lawson
ca51372150 Explicitly load style-loader/css-loader deps
This appears to fix sveltejs/sapper-template#27
2018-01-06 17:21:40 -08:00
Luke Edwards
7cef1f1120 only return from show-url if in server context 2018-01-06 16:11:39 -08:00
Luke Edwards
1b73baabce [temp - debug]: log out the preload value 2018-01-06 15:40:40 -08:00
Luke Edwards
5aa01b922b add page.title() Nightmare action 2018-01-06 15:16:18 -08:00
Luke Edwards
f0bc68be88 use built-in for document.title 2018-01-06 15:07:43 -08:00
Luke Edwards
be7c53becc quick attempt at fixing test for travis 2018-01-06 14:53:14 -08:00
freedmand
9ea4137b87 Add option to extract server-side routes at directories other than /api.
Also clarifies some texts and documentation.
2018-01-05 19:21:25 -08:00
freedmand
7588911108 Removes all async/await from the extraction pipeline, and adds unit tests for extracted client pages that match a regular expression 2018-01-05 14:56:58 -08:00
freedmand
fc8280adea Fixes small issue with reading chunk files 2018-01-05 14:42:04 -08:00
freedmand
d08f9eb5a4 Fixes funky indentation in extraction unit test 2018-01-05 14:29:46 -08:00
Emil Tholin
2b3472b1b1 Remove async/await from lib 2018-01-05 23:19:40 +01:00
freedmand
30ddb3dd7e Adds a sapper extract CLI command, which scrapes the server to run as a static website starting at the site's root.
TESTED=Basic unit test ensuring relevant routes are added.
2018-01-05 14:16:30 -08:00
Luke Edwards
0c891ba79e require chokidar lazily; dev-mode only 2018-01-05 00:39:17 -08:00
Rich Harris
ee94f355d5 update marked due to vulnerability 2018-01-04 17:17:08 -05:00
Rich Harris
bea9b7965a update package-lock 2018-01-04 17:13:17 -05:00
Rich Harris
1312aede1f -> v0.3.2 2018-01-03 13:48:03 -05:00
Rich Harris
50e307e0c0 add test for programmatic prefetch 2018-01-03 13:46:07 -05:00
Emil Tholin
e87ac1f367 Export prefetch 2018-01-03 18:39:51 +01:00
Rich Harris
5da9d0926a update README, add license 2018-01-01 16:50:21 -05:00
Rich Harris
9538499d51 -> v0.3.1 2017-12-31 16:36:02 -05:00
28 changed files with 1000 additions and 516 deletions

3
.gitignore vendored
View File

@@ -2,4 +2,5 @@
node_modules node_modules
cypress/screenshots cypress/screenshots
test/app/.sapper test/app/.sapper
runtime.js runtime.js
yarn.lock

View File

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

View File

@@ -1,5 +1,30 @@
# sapper changelog # sapper changelog
## 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 ## 0.3.0
* Move `sapper/runtime/app.js` to `sapper/runtime.js` * Move `sapper/runtime/app.js` to `sapper/runtime.js`

9
LICENSE Normal file
View File

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

151
README.md
View File

@@ -1,150 +1,37 @@
# sapper # 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.

View File

@@ -1,8 +1,29 @@
#!/usr/bin/env node #!/usr/bin/env node
const build = require('../lib/build.js');
const cmd = process.argv[2]; const cmd = process.argv[2];
const start = Date.now();
if (cmd === 'build') { if (cmd === 'build') {
process.env.NODE_ENV = 'production'; build()
require('../lib/build.js')(); .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()
.then(() => require('../lib/utils/export.js')())
.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');
});
}

View File

@@ -1,3 +1,5 @@
process.env.NODE_ENV = 'production';
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const mkdirp = require('mkdirp'); const mkdirp = require('mkdirp');
@@ -14,29 +16,32 @@ module.exports = () => {
// create main.js and server-routes.js // create main.js and server-routes.js
create_app(); create_app();
function handleErrors(err, stats) { return new Promise((fulfil, reject) => {
if (err) { function handleErrors(err, stats) {
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error'); if (err) {
process.exit(1); reject(err);
process.exit(1);
}
if (stats.hasErrors()) {
console.error(stats.toString({ colors: true }));
reject(new Error(`Encountered errors while building app`));
}
} }
if (stats.hasErrors()) { client.run((err, clientStats) => {
console.log(stats.toString({ colors: true })); handleErrors(err, clientStats);
process.exit(1); const clientInfo = clientStats.toJson();
} fs.writeFileSync(path.join(dest, 'stats.client.json'), JSON.stringify(clientInfo, null, ' '));
}
client.run((err, clientStats) => { server.run((err, serverStats) => {
handleErrors(err, clientStats); handleErrors(err, serverStats);
const clientInfo = clientStats.toJson(); const serverInfo = serverStats.toJson();
fs.writeFileSync(path.join(dest, 'stats.client.json'), JSON.stringify(clientInfo, null, ' ')); fs.writeFileSync(path.join(dest, 'stats.server.json'), JSON.stringify(serverInfo, null, ' '));
server.run((err, serverStats) => { generate_asset_cache(clientInfo, serverInfo);
handleErrors(err, serverStats); fulfil();
const serverInfo = serverStats.toJson(); });
fs.writeFileSync(path.join(dest, 'stats.server.json'), JSON.stringify(serverInfo, null, ' '));
generate_asset_cache(clientInfo, serverInfo);
}); });
}); });
}; };

View File

@@ -1,5 +1,6 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const serialize = require('serialize-javascript');
const route_manager = require('./route_manager.js'); const route_manager = require('./route_manager.js');
const templates = require('./templates.js'); const templates = require('./templates.js');
const create_app = require('./utils/create_app.js'); const create_app = require('./utils/create_app.js');
@@ -23,9 +24,11 @@ function connect_dev() {
heartbeat: 10 * 1000 heartbeat: 10 * 1000
}), }),
async (req, res, next) => { (req, res, next) => {
asset_cache = await watcher.ready; watcher.ready.then(cache => {
next(); asset_cache = cache;
next();
});
}, },
set_req_pathname, set_req_pathname,
@@ -125,76 +128,98 @@ function get_asset_handler(opts) {
}; };
} }
function get_route_handler(fn) { const resolved = Promise.resolve();
return async function handle_route(req, res, next) {
const url = req.pathname;
const { client, server } = fn(); function get_route_handler(fn) {
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') {
// 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"`);
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;
// whatever happens, we're going to serve some HTML // whatever happens, we're going to serve some HTML
res.set({ res.set({
'Content-Type': 'text/html' 'Content-Type': 'text/html'
}); });
try { resolved
for (const route of route_manager.routes) { .then(() => {
if (route.test(url)) { for (const route of route_manager.routes) {
req.params = route.exec(url); if (route.test(url)) return handle_route(route, req, res, next, fn());
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"`);
const data = { params: req.params, query: req.query };
if (mod.preload) {
const promise = Promise.resolve(mod.preload(req)).then(preloaded => {
Object.assign(data, preloaded);
return mod.render(data);
});
await 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(); // no matching route — 404
} catch(err) { next();
res.status(500); })
res.end(templates.render(500, { .catch(err => {
title: (err && err.name) || 'Internal server error', res.status(500);
url, res.end(templates.render(500, {
error: escape_html(err && (err.details || err.message || err) || 'Unknown error'), title: (err && err.name) || 'Internal server error',
stack: err && err.stack.split('\n').slice(1).join('\n') url,
})); error: escape_html(err && (err.details || err.message || err) || 'Unknown error'),
} stack: err && err.stack.split('\n').slice(1).join('\n')
}));
});
}; };
} }
@@ -207,7 +232,7 @@ function get_not_found_handler(fn) {
title: 'Not found', title: 'Not found',
status: 404, status: 404,
method: req.method, method: req.method,
main: asset_cache.client.main_file, scripts: `<script src='${asset_cache.client.main_file}'></script>`,
url: req.url url: req.url
})); }));
}; };
@@ -235,4 +260,12 @@ function compose_handlers(handlers) {
function read_json(file) { function read_json(file) {
return JSON.parse(fs.readFileSync(file, 'utf-8')); return JSON.parse(fs.readFileSync(file, 'utf-8'));
}
function try_serialize(data) {
try {
return serialize(data);
} catch (err) {
return null;
}
} }

View File

@@ -1,5 +1,4 @@
const glob = require('glob'); const glob = require('glob');
const chokidar = require('chokidar');
const create_routes = require('./utils/create_routes.js'); const create_routes = require('./utils/create_routes.js');
const { src, dev } = require('./config.js'); const { src, dev } = require('./config.js');
@@ -20,7 +19,7 @@ function update() {
update(); update();
if (dev) { if (dev) {
const watcher = chokidar.watch(`${src}/**/*.+(html|js|mjs)`, { const watcher = require('chokidar').watch(`${src}/**/*.+(html|js|mjs)`, {
ignoreInitial: true, ignoreInitial: true,
persistent: false persistent: false
}); });
@@ -28,4 +27,4 @@ if (dev) {
watcher.on('add', update); watcher.on('add', update);
watcher.on('change', update); watcher.on('change', update);
watcher.on('unlink', update); watcher.on('unlink', update);
} }

View File

@@ -1,10 +1,21 @@
const fs = require('fs'); const fs = require('fs');
const glob = require('glob'); const glob = require('glob');
const chokidar = require('chokidar'); const chalk = require('chalk');
const framer = require('code-frame');
const { locate } = require('locate-character');
const { dev } = require('./config.js'); const { dev } = require('./config.js');
let templates; let 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);
}
function create_templates() { function create_templates() {
templates = glob.sync('*.html', { cwd: 'templates' }) templates = glob.sync('*.html', { cwd: 'templates' })
.map(file => { .map(file => {
@@ -12,7 +23,24 @@ function create_templates() {
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 +59,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,9 +85,14 @@ 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);
} }
}; };
}) })
@@ -65,7 +102,7 @@ function create_templates() {
create_templates(); create_templates();
if (dev) { if (dev) {
const watcher = chokidar.watch('templates/**.html', { const watcher = require('chokidar').watch('templates/**.html', {
ignoreInitial: true, ignoreInitial: true,
persistent: false persistent: false
}); });
@@ -87,4 +124,4 @@ exports.stream = (res, status, data) => {
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}`;
}; };

View File

@@ -1,6 +1,5 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const chokidar = require('chokidar');
const route_manager = require('../route_manager.js'); const route_manager = require('../route_manager.js');
const { src, entry, dev } = require('../config.js'); const { src, entry, dev } = require('../config.js');
@@ -70,7 +69,7 @@ function create_app() {
if (dev) { if (dev) {
route_manager.onchange(create_app); route_manager.onchange(create_app);
const watcher = chokidar.watch(`templates/main.js`, { const watcher = require('chokidar').watch(`templates/main.js`, {
ignoreInitial: true, ignoreInitial: true,
persistent: false persistent: false
}); });
@@ -80,4 +79,4 @@ if (dev) {
watcher.on('unlink', create_app); watcher.on('unlink', create_app);
} }
module.exports = create_app; module.exports = create_app;

View File

@@ -31,7 +31,7 @@ module.exports = function create_matchers(files) {
} }
} }
const pattern = new RegExp(`^${pattern_string || '\\/'}$`); const pattern = new RegExp(`^${pattern_string}\\/?$`);
const test = url => pattern.test(url); const test = url => pattern.test(url);

87
lib/utils/export.js Normal file
View File

@@ -0,0 +1,87 @@
const sander = require('sander');
const app = require('express')();
const cheerio = require('cheerio');
const fetch = require('node-fetch');
const URL = require('url-parse');
const sapper = require('../index.js');
const { PORT = 3000, OUTPUT_DIR = 'dist' } = process.env;
const { dest } = require('../config.js');
const origin = `http://localhost:${PORT}`;
module.exports = function() {
// Prep output directory
sander.rimrafSync(OUTPUT_DIR);
sander.copydirSync('assets').to(OUTPUT_DIR);
sander.copydirSync(`${dest}/client`).to(`${OUTPUT_DIR}/client`);
sander.copyFileSync(`${dest}/service-worker.js`).to(`${OUTPUT_DIR}/service-worker.js`);
// 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.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(sapper());
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());
};

413
package-lock.json generated
View File

@@ -1,14 +1,13 @@
{ {
"name": "sapper", "name": "sapper",
"version": "0.2.10", "version": "0.4.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@types/node": { "@types/node": {
"version": "7.0.51", "version": "7.0.52",
"resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.51.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.52.tgz",
"integrity": "sha512-h5u7FnEnG+Fn44HfknTTvu199FzFWVSo97ToSRWvXl1F11UfN6wGnE7exUy23pFfDn+CeluvEoCoe4l2eCVC3g==", "integrity": "sha512-jjpyQsKGsOF/wUElNjfPULk+d8PKvJOIXk3IUeBYYmNCy5dMWfrI+JiixYNw8ppKOlcRwWTXFl0B+i5oGrf95Q=="
"dev": true
}, },
"accepts": { "accepts": {
"version": "1.3.4", "version": "1.3.4",
@@ -21,9 +20,9 @@
} }
}, },
"acorn": { "acorn": {
"version": "5.2.1", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.2.1.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.3.0.tgz",
"integrity": "sha512-jG0u7c4Ly+3QkkW18V+NRDN+4bWHdln30NL1ZL2AvFZZmQe/BfopYCtghCKKVBUSetZ4QKcyA0pY6/4Gw8Pv8w==" "integrity": "sha512-Yej+zOJ1Dm/IMZzzj78OntP/r3zHEaKcyNoU2lAaxPtrseM6rF0xwqoz5Q5ysAiED9hTjI2hgtvLXitlCN1/Ug=="
}, },
"acorn-dynamic-import": { "acorn-dynamic-import": {
"version": "2.0.2", "version": "2.0.2",
@@ -256,7 +255,7 @@
"dev": true, "dev": true,
"requires": { "requires": {
"browserslist": "1.7.7", "browserslist": "1.7.7",
"caniuse-db": "1.0.30000784", "caniuse-db": "1.0.30000791",
"normalize-range": "0.1.2", "normalize-range": "0.1.2",
"num2fraction": "1.2.2", "num2fraction": "1.2.2",
"postcss": "5.2.18", "postcss": "5.2.18",
@@ -357,7 +356,7 @@
"bytes": "3.0.0", "bytes": "3.0.0",
"content-type": "1.0.4", "content-type": "1.0.4",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "1.1.1", "depd": "1.1.2",
"http-errors": "1.6.2", "http-errors": "1.6.2",
"iconv-lite": "0.4.19", "iconv-lite": "0.4.19",
"on-finished": "2.3.0", "on-finished": "2.3.0",
@@ -377,6 +376,11 @@
} }
} }
}, },
"boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
},
"boom": { "boom": {
"version": "4.3.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz",
@@ -455,7 +459,7 @@
"integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
"requires": { "requires": {
"bn.js": "4.11.8", "bn.js": "4.11.8",
"randombytes": "2.0.5" "randombytes": "2.0.6"
} }
}, },
"browserify-sign": { "browserify-sign": {
@@ -486,7 +490,7 @@
"integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=",
"dev": true, "dev": true,
"requires": { "requires": {
"caniuse-db": "1.0.30000784", "caniuse-db": "1.0.30000791",
"electron-to-chromium": "1.3.30" "electron-to-chromium": "1.3.30"
} }
}, },
@@ -566,15 +570,15 @@
"dev": true, "dev": true,
"requires": { "requires": {
"browserslist": "1.7.7", "browserslist": "1.7.7",
"caniuse-db": "1.0.30000784", "caniuse-db": "1.0.30000791",
"lodash.memoize": "4.1.2", "lodash.memoize": "4.1.2",
"lodash.uniq": "4.5.0" "lodash.uniq": "4.5.0"
} }
}, },
"caniuse-db": { "caniuse-db": {
"version": "1.0.30000784", "version": "1.0.30000791",
"resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000784.tgz", "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000791.tgz",
"integrity": "sha1-G+lQEtlInHcZB0+BruV9vf/mNhs=", "integrity": "sha1-Bnh/VsrvQwChfjXRN0RxI731Nvk=",
"dev": true "dev": true
}, },
"caseless": { "caseless": {
@@ -608,6 +612,19 @@
"integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=",
"dev": true "dev": true
}, },
"cheerio": {
"version": "1.0.0-rc.2",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.2.tgz",
"integrity": "sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs=",
"requires": {
"css-select": "1.2.0",
"dom-serializer": "0.1.0",
"entities": "1.1.1",
"htmlparser2": "3.9.2",
"lodash": "4.17.4",
"parse5": "3.0.3"
}
},
"chokidar": { "chokidar": {
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
@@ -720,6 +737,14 @@
"q": "1.5.1" "q": "1.5.1"
} }
}, },
"code-frame": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/code-frame/-/code-frame-5.0.0.tgz",
"integrity": "sha1-QAMHB+xG4dmdXlLcnzV/ytkNmT0=",
"requires": {
"left-pad": "1.2.0"
}
},
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
@@ -943,7 +968,7 @@
"inherits": "2.0.3", "inherits": "2.0.3",
"pbkdf2": "3.0.14", "pbkdf2": "3.0.14",
"public-encrypt": "4.0.0", "public-encrypt": "4.0.0",
"randombytes": "2.0.5", "randombytes": "2.0.6",
"randomfill": "1.0.3" "randomfill": "1.0.3"
} }
}, },
@@ -954,9 +979,9 @@
"dev": true "dev": true
}, },
"css-loader": { "css-loader": {
"version": "0.28.7", "version": "0.28.8",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-0.28.7.tgz", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-0.28.8.tgz",
"integrity": "sha512-GxMpax8a/VgcfRrVy0gXD6yLd5ePYbXX/5zGgTVYp4wXtJklS8Z2VaUArJgc//f6/Dzil7BaJObdSv8eKKCPgg==", "integrity": "sha512-4jGj7Ag6WUZ5lQyE4te9sJLn0lgkz6HI3WDE4aw98AkW1IAKXPP4blTpPeorlLDpNsYvojo0SYgRJOdz2KbuAw==",
"dev": true, "dev": true,
"requires": { "requires": {
"babel-code-frame": "6.26.0", "babel-code-frame": "6.26.0",
@@ -975,6 +1000,17 @@
"source-list-map": "2.0.0" "source-list-map": "2.0.0"
} }
}, },
"css-select": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
"integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
"requires": {
"boolbase": "1.0.0",
"css-what": "2.1.0",
"domutils": "1.5.1",
"nth-check": "1.0.1"
}
},
"css-selector-tokenizer": { "css-selector-tokenizer": {
"version": "0.7.0", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz", "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz",
@@ -986,6 +1022,11 @@
"regexpu-core": "1.0.0" "regexpu-core": "1.0.0"
} }
}, },
"css-what": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz",
"integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0="
},
"cssesc": { "cssesc": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz",
@@ -1171,9 +1212,9 @@
"dev": true "dev": true
}, },
"depd": { "depd": {
"version": "1.1.1", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=", "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
"dev": true "dev": true
}, },
"des.js": { "des.js": {
@@ -1204,23 +1245,61 @@
"requires": { "requires": {
"bn.js": "4.11.8", "bn.js": "4.11.8",
"miller-rabin": "4.0.1", "miller-rabin": "4.0.1",
"randombytes": "2.0.5" "randombytes": "2.0.6"
} }
}, },
"doctrine": { "doctrine": {
"version": "2.0.2", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.2.tgz", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
"integrity": "sha512-y0tm5Pq6ywp3qSTZ1vPgVdAnbDEoeoc5wlOHXoY1c4Wug/a7JvqHIl7BTvwodaHmejWkK/9dSb3sCYfyo/om8A==", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
"dev": true, "dev": true,
"requires": { "requires": {
"esutils": "2.0.2" "esutils": "2.0.2"
} }
}, },
"dom-serializer": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
"integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=",
"requires": {
"domelementtype": "1.1.3",
"entities": "1.1.1"
},
"dependencies": {
"domelementtype": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz",
"integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs="
}
}
},
"domain-browser": { "domain-browser": {
"version": "1.1.7", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz",
"integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=" "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw="
}, },
"domelementtype": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz",
"integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI="
},
"domhandler": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.1.tgz",
"integrity": "sha1-iS5HAAqZvlW783dP/qBWHYh5wlk=",
"requires": {
"domelementtype": "1.3.0"
}
},
"domutils": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
"integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
"requires": {
"dom-serializer": "0.1.0",
"domelementtype": "1.3.0"
}
},
"duplexer": { "duplexer": {
"version": "0.1.1", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
@@ -1249,7 +1328,7 @@
"integrity": "sha1-Oj6D2WX9f6/kc76N349HJWG2JT0=", "integrity": "sha1-Oj6D2WX9f6/kc76N349HJWG2JT0=",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/node": "7.0.51", "@types/node": "7.0.52",
"electron-download": "3.3.0", "electron-download": "3.3.0",
"extract-zip": "1.6.6" "extract-zip": "1.6.6"
} }
@@ -1266,7 +1345,7 @@
"minimist": "1.2.0", "minimist": "1.2.0",
"nugget": "2.0.1", "nugget": "2.0.1",
"path-exists": "2.1.0", "path-exists": "2.1.0",
"rc": "1.2.2", "rc": "1.2.3",
"semver": "5.4.1", "semver": "5.4.1",
"sumchecker": "1.3.1" "sumchecker": "1.3.1"
}, },
@@ -1374,6 +1453,16 @@
} }
} }
}, },
"ensure-posix-path": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/ensure-posix-path/-/ensure-posix-path-1.0.2.tgz",
"integrity": "sha1-pls+QtC3HPxYXrd0+ZQ8jZuRsMI="
},
"entities": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz",
"integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA="
},
"errno": { "errno": {
"version": "0.1.6", "version": "0.1.6",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.6.tgz", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.6.tgz",
@@ -1506,9 +1595,9 @@
} }
}, },
"eslint": { "eslint": {
"version": "4.13.1", "version": "4.15.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-4.13.1.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.15.0.tgz",
"integrity": "sha512-UCJVV50RtLHYzBp1DZ8CMPtRSg4iVZvjgO9IJHIKyWU/AnJVjtdRikoUPLB29n5pzMB7TnsLQWf0V6VUJfoPfw==", "integrity": "sha512-zEO/Z1ZUxIQ+MhDVKkVTUYpIPDTEJLXGMrkID+5v1NeQHtCz6FZikWuFRgxE1Q/RV2V4zVl1u3xmpPADHhMZ6A==",
"dev": true, "dev": true,
"requires": { "requires": {
"ajv": "5.5.2", "ajv": "5.5.2",
@@ -1517,11 +1606,11 @@
"concat-stream": "1.6.0", "concat-stream": "1.6.0",
"cross-spawn": "5.1.0", "cross-spawn": "5.1.0",
"debug": "3.1.0", "debug": "3.1.0",
"doctrine": "2.0.2", "doctrine": "2.1.0",
"eslint-scope": "3.7.1", "eslint-scope": "3.7.1",
"eslint-visitor-keys": "1.0.0",
"espree": "3.5.2", "espree": "3.5.2",
"esquery": "1.0.0", "esquery": "1.0.0",
"estraverse": "4.2.0",
"esutils": "2.0.2", "esutils": "2.0.2",
"file-entry-cache": "2.0.0", "file-entry-cache": "2.0.0",
"functional-red-black-tree": "1.0.1", "functional-red-black-tree": "1.0.1",
@@ -1584,9 +1673,9 @@
} }
}, },
"eslint-import-resolver-node": { "eslint-import-resolver-node": {
"version": "0.3.1", "version": "0.3.2",
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.1.tgz", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz",
"integrity": "sha512-yUtXS15gIcij68NmXmP9Ni77AQuCN0itXbCc/jWd8C6/yKZaSNXicpC8cgvjnxVdmfsosIXrjpzFq7GcDryb6A==", "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"debug": "2.6.9", "debug": "2.6.9",
@@ -1635,7 +1724,7 @@
"contains-path": "0.1.0", "contains-path": "0.1.0",
"debug": "2.6.9", "debug": "2.6.9",
"doctrine": "1.5.0", "doctrine": "1.5.0",
"eslint-import-resolver-node": "0.3.1", "eslint-import-resolver-node": "0.3.2",
"eslint-module-utils": "2.1.1", "eslint-module-utils": "2.1.1",
"has": "1.0.1", "has": "1.0.1",
"lodash.cond": "4.5.2", "lodash.cond": "4.5.2",
@@ -1674,13 +1763,19 @@
"estraverse": "4.2.0" "estraverse": "4.2.0"
} }
}, },
"eslint-visitor-keys": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
"integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==",
"dev": true
},
"espree": { "espree": {
"version": "3.5.2", "version": "3.5.2",
"resolved": "https://registry.npmjs.org/espree/-/espree-3.5.2.tgz", "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.2.tgz",
"integrity": "sha512-sadKeYwaR/aJ3stC2CdvgXu1T16TdYN+qwCpcWbMnGJ8s0zNWemzrvb2GbD4OhmJ/fwpJjudThAlLobGbWZbCQ==", "integrity": "sha512-sadKeYwaR/aJ3stC2CdvgXu1T16TdYN+qwCpcWbMnGJ8s0zNWemzrvb2GbD4OhmJ/fwpJjudThAlLobGbWZbCQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"acorn": "5.2.1", "acorn": "5.3.0",
"acorn-jsx": "3.0.1" "acorn-jsx": "3.0.1"
} }
}, },
@@ -1813,7 +1908,7 @@
"cookie": "0.3.1", "cookie": "0.3.1",
"cookie-signature": "1.0.6", "cookie-signature": "1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "1.1.1", "depd": "1.1.2",
"encodeurl": "1.0.1", "encodeurl": "1.0.1",
"escape-html": "1.0.3", "escape-html": "1.0.3",
"etag": "1.8.1", "etag": "1.8.1",
@@ -3113,6 +3208,19 @@
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz",
"integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=" "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8="
}, },
"htmlparser2": {
"version": "3.9.2",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz",
"integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=",
"requires": {
"domelementtype": "1.3.0",
"domhandler": "2.4.1",
"domutils": "1.5.1",
"entities": "1.1.1",
"inherits": "2.0.3",
"readable-stream": "2.3.3"
}
},
"http-errors": { "http-errors": {
"version": "1.6.2", "version": "1.6.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
@@ -3125,6 +3233,12 @@
"statuses": "1.3.1" "statuses": "1.3.1"
}, },
"dependencies": { "dependencies": {
"depd": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
"integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=",
"dev": true
},
"setprototypeof": { "setprototypeof": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz",
@@ -3167,18 +3281,18 @@
"integrity": "sha1-g/Cg7DeL8yRheLbCrZE28TWxyWI=", "integrity": "sha1-g/Cg7DeL8yRheLbCrZE28TWxyWI=",
"dev": true, "dev": true,
"requires": { "requires": {
"postcss": "6.0.14" "postcss": "6.0.16"
}, },
"dependencies": { "dependencies": {
"postcss": { "postcss": {
"version": "6.0.14", "version": "6.0.16",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.16.tgz",
"integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==", "integrity": "sha512-m758RWPmSjFH/2MyyG3UOW1fgYbR9rtdzz5UNJnlm7OLtu4B2h9C6gi+bE4qFKghsBRFfZT8NzoQBs6JhLotoA==",
"dev": true, "dev": true,
"requires": { "requires": {
"chalk": "2.3.0", "chalk": "2.3.0",
"source-map": "0.6.1", "source-map": "0.6.1",
"supports-color": "4.5.0" "supports-color": "5.1.0"
} }
}, },
"source-map": { "source-map": {
@@ -3186,6 +3300,15 @@
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true "dev": true
},
"supports-color": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.1.0.tgz",
"integrity": "sha512-Ry0AwkoKjDpVKK4sV4h6o3UJmNRbjYm2uXhwfj3J56lMVdvnUNqzQVRztOOMGQ++w1K/TjNDFvpJk0F/LoeBCQ==",
"dev": true,
"requires": {
"has-flag": "2.0.0"
}
} }
} }
}, },
@@ -3680,6 +3803,11 @@
"invert-kv": "1.0.0" "invert-kv": "1.0.0"
} }
}, },
"left-pad": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.2.0.tgz",
"integrity": "sha1-0wpzxrggHY99jnlWupYWCHpo4O4="
},
"levn": { "levn": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
@@ -3716,6 +3844,11 @@
"json5": "0.5.1" "json5": "0.5.1"
} }
}, },
"locate-character": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-2.0.5.tgz",
"integrity": "sha512-n2GmejDXtOPBAZdIiEFy5dJ5N38xBCXLNOtw2WpB9kGh6pnrEuKlwYI+Tkpofc4wDtVXHtoAOJaMRlYG/oYaxg=="
},
"locate-path": { "locate-path": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
@@ -3796,6 +3929,14 @@
"integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=",
"dev": true "dev": true
}, },
"matcher-collection": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/matcher-collection/-/matcher-collection-1.0.5.tgz",
"integrity": "sha512-nUCmzKipcJEwYsBVAFh5P+d7JBuhJaW1xs85Hara9xuMLqtCVUrW6DSC0JVIkluxEH2W45nPBM/wjHtBXa/tYA==",
"requires": {
"minimatch": "3.0.4"
}
},
"math-expression-evaluator": { "math-expression-evaluator": {
"version": "1.2.17", "version": "1.2.17",
"resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz",
@@ -4069,9 +4210,9 @@
} }
}, },
"mocha": { "mocha": {
"version": "4.0.1", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-4.0.1.tgz", "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz",
"integrity": "sha512-evDmhkoA+cBNiQQQdSKZa2b9+W2mpLoj50367lhy+Klnx9OV8XlCIhigUnn1gaTFLQCa0kdNhEGDr0hCXOQFDw==", "integrity": "sha512-0RVnjg1HJsXY2YFDoTNzcc1NKhYuXKRrBAG2gDygmJJA136Cs2QlRliZG1mA0ap7cuaT30mw16luAeln+4RiNA==",
"dev": true, "dev": true,
"requires": { "requires": {
"browser-stdout": "1.3.0", "browser-stdout": "1.3.0",
@@ -4317,6 +4458,14 @@
"path-key": "2.0.1" "path-key": "2.0.1"
} }
}, },
"nth-check": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz",
"integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=",
"requires": {
"boolbase": "1.0.0"
}
},
"nugget": { "nugget": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/nugget/-/nugget-2.0.1.tgz", "resolved": "https://registry.npmjs.org/nugget/-/nugget-2.0.1.tgz",
@@ -4461,18 +4610,26 @@
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
}, },
"p-limit": { "p-limit": {
"version": "1.1.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.1.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz",
"integrity": "sha1-sH/y2aXYi+yAYDWJWiurZqJ5iLw=" "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==",
"requires": {
"p-try": "1.0.0"
}
}, },
"p-locate": { "p-locate": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
"requires": { "requires": {
"p-limit": "1.1.0" "p-limit": "1.2.0"
} }
}, },
"p-try": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
},
"pako": { "pako": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz",
@@ -4509,6 +4666,14 @@
"error-ex": "1.3.1" "error-ex": "1.3.1"
} }
}, },
"parse5": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz",
"integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==",
"requires": {
"@types/node": "7.0.52"
}
},
"parseurl": { "parseurl": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
@@ -4884,18 +5049,18 @@
"integrity": "sha1-thTJcgvmgW6u41+zpfqh26agXds=", "integrity": "sha1-thTJcgvmgW6u41+zpfqh26agXds=",
"dev": true, "dev": true,
"requires": { "requires": {
"postcss": "6.0.14" "postcss": "6.0.16"
}, },
"dependencies": { "dependencies": {
"postcss": { "postcss": {
"version": "6.0.14", "version": "6.0.16",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.16.tgz",
"integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==", "integrity": "sha512-m758RWPmSjFH/2MyyG3UOW1fgYbR9rtdzz5UNJnlm7OLtu4B2h9C6gi+bE4qFKghsBRFfZT8NzoQBs6JhLotoA==",
"dev": true, "dev": true,
"requires": { "requires": {
"chalk": "2.3.0", "chalk": "2.3.0",
"source-map": "0.6.1", "source-map": "0.6.1",
"supports-color": "4.5.0" "supports-color": "5.1.0"
} }
}, },
"source-map": { "source-map": {
@@ -4903,6 +5068,15 @@
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true "dev": true
},
"supports-color": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.1.0.tgz",
"integrity": "sha512-Ry0AwkoKjDpVKK4sV4h6o3UJmNRbjYm2uXhwfj3J56lMVdvnUNqzQVRztOOMGQ++w1K/TjNDFvpJk0F/LoeBCQ==",
"dev": true,
"requires": {
"has-flag": "2.0.0"
}
} }
} }
}, },
@@ -4913,18 +5087,18 @@
"dev": true, "dev": true,
"requires": { "requires": {
"css-selector-tokenizer": "0.7.0", "css-selector-tokenizer": "0.7.0",
"postcss": "6.0.14" "postcss": "6.0.16"
}, },
"dependencies": { "dependencies": {
"postcss": { "postcss": {
"version": "6.0.14", "version": "6.0.16",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.16.tgz",
"integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==", "integrity": "sha512-m758RWPmSjFH/2MyyG3UOW1fgYbR9rtdzz5UNJnlm7OLtu4B2h9C6gi+bE4qFKghsBRFfZT8NzoQBs6JhLotoA==",
"dev": true, "dev": true,
"requires": { "requires": {
"chalk": "2.3.0", "chalk": "2.3.0",
"source-map": "0.6.1", "source-map": "0.6.1",
"supports-color": "4.5.0" "supports-color": "5.1.0"
} }
}, },
"source-map": { "source-map": {
@@ -4932,6 +5106,15 @@
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true "dev": true
},
"supports-color": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.1.0.tgz",
"integrity": "sha512-Ry0AwkoKjDpVKK4sV4h6o3UJmNRbjYm2uXhwfj3J56lMVdvnUNqzQVRztOOMGQ++w1K/TjNDFvpJk0F/LoeBCQ==",
"dev": true,
"requires": {
"has-flag": "2.0.0"
}
} }
} }
}, },
@@ -4942,18 +5125,18 @@
"dev": true, "dev": true,
"requires": { "requires": {
"css-selector-tokenizer": "0.7.0", "css-selector-tokenizer": "0.7.0",
"postcss": "6.0.14" "postcss": "6.0.16"
}, },
"dependencies": { "dependencies": {
"postcss": { "postcss": {
"version": "6.0.14", "version": "6.0.16",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.16.tgz",
"integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==", "integrity": "sha512-m758RWPmSjFH/2MyyG3UOW1fgYbR9rtdzz5UNJnlm7OLtu4B2h9C6gi+bE4qFKghsBRFfZT8NzoQBs6JhLotoA==",
"dev": true, "dev": true,
"requires": { "requires": {
"chalk": "2.3.0", "chalk": "2.3.0",
"source-map": "0.6.1", "source-map": "0.6.1",
"supports-color": "4.5.0" "supports-color": "5.1.0"
} }
}, },
"source-map": { "source-map": {
@@ -4961,6 +5144,15 @@
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true "dev": true
},
"supports-color": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.1.0.tgz",
"integrity": "sha512-Ry0AwkoKjDpVKK4sV4h6o3UJmNRbjYm2uXhwfj3J56lMVdvnUNqzQVRztOOMGQ++w1K/TjNDFvpJk0F/LoeBCQ==",
"dev": true,
"requires": {
"has-flag": "2.0.0"
}
} }
} }
}, },
@@ -4971,18 +5163,18 @@
"dev": true, "dev": true,
"requires": { "requires": {
"icss-replace-symbols": "1.1.0", "icss-replace-symbols": "1.1.0",
"postcss": "6.0.14" "postcss": "6.0.16"
}, },
"dependencies": { "dependencies": {
"postcss": { "postcss": {
"version": "6.0.14", "version": "6.0.16",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.16.tgz",
"integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==", "integrity": "sha512-m758RWPmSjFH/2MyyG3UOW1fgYbR9rtdzz5UNJnlm7OLtu4B2h9C6gi+bE4qFKghsBRFfZT8NzoQBs6JhLotoA==",
"dev": true, "dev": true,
"requires": { "requires": {
"chalk": "2.3.0", "chalk": "2.3.0",
"source-map": "0.6.1", "source-map": "0.6.1",
"supports-color": "4.5.0" "supports-color": "5.1.0"
} }
}, },
"source-map": { "source-map": {
@@ -4990,6 +5182,15 @@
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true "dev": true
},
"supports-color": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.1.0.tgz",
"integrity": "sha512-Ry0AwkoKjDpVKK4sV4h6o3UJmNRbjYm2uXhwfj3J56lMVdvnUNqzQVRztOOMGQ++w1K/TjNDFvpJk0F/LoeBCQ==",
"dev": true,
"requires": {
"has-flag": "2.0.0"
}
} }
} }
}, },
@@ -5196,7 +5397,7 @@
"browserify-rsa": "4.0.1", "browserify-rsa": "4.0.1",
"create-hash": "1.1.3", "create-hash": "1.1.3",
"parse-asn1": "5.1.0", "parse-asn1": "5.1.0",
"randombytes": "2.0.5" "randombytes": "2.0.6"
} }
}, },
"punycode": { "punycode": {
@@ -5274,9 +5475,9 @@
} }
}, },
"randombytes": { "randombytes": {
"version": "2.0.5", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.5.tgz", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz",
"integrity": "sha512-8T7Zn1AhMsQ/HI1SjcCfT/t4ii3eAqco3yOcSzS4mozsOz69lHLsoMXmF9nZgnFanYscnSlUSgs8uZyKzpE6kg==", "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==",
"requires": { "requires": {
"safe-buffer": "5.1.1" "safe-buffer": "5.1.1"
} }
@@ -5286,7 +5487,7 @@
"resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.3.tgz", "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.3.tgz",
"integrity": "sha512-YL6GrhrWoic0Eq8rXVbMptH7dAxCs0J+mh5Y0euNekPPYaxEmdVGim6GdoxoRzKW2yJoU8tueifS7mYxvcFDEQ==", "integrity": "sha512-YL6GrhrWoic0Eq8rXVbMptH7dAxCs0J+mh5Y0euNekPPYaxEmdVGim6GdoxoRzKW2yJoU8tueifS7mYxvcFDEQ==",
"requires": { "requires": {
"randombytes": "2.0.5", "randombytes": "2.0.6",
"safe-buffer": "5.1.1" "safe-buffer": "5.1.1"
} }
}, },
@@ -5309,9 +5510,9 @@
} }
}, },
"rc": { "rc": {
"version": "1.2.2", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.2.tgz", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.3.tgz",
"integrity": "sha1-2M6ctX6NZNnHut2YdsfDTL48cHc=", "integrity": "sha1-UVdakA+N1oOBxxC0cSwhVMPiA1s=",
"dev": true, "dev": true,
"requires": { "requires": {
"deep-extend": "0.4.2", "deep-extend": "0.4.2",
@@ -5596,9 +5797,9 @@
} }
}, },
"rollup": { "rollup": {
"version": "0.53.0", "version": "0.53.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-0.53.0.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.53.4.tgz",
"integrity": "sha512-bG5RzkF7wcOHmKoVAFtERZ5P9TNJP9/AF+ldwGm/Rx6pejura+Z9BDU0GJtzWu+lYXwjfINmgiCclhLJzP/OXA==", "integrity": "sha512-ErW5cFw5KY/qiyUlPDJ7iBhw51Iro/oyvxETupO85bMg5T7MLlFj3lEDzwjLTOxJAyzWQanUYj/LZHm6aLLm5w==",
"dev": true "dev": true
}, },
"rollup-plugin-typescript": { "rollup-plugin-typescript": {
@@ -5667,6 +5868,16 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
}, },
"sander": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/sander/-/sander-0.6.0.tgz",
"integrity": "sha1-rxYkzX+2362Y6+9WUxn5IAeNqSU=",
"requires": {
"graceful-fs": "4.1.11",
"mkdirp": "0.5.1",
"rimraf": "2.6.2"
}
},
"sax": { "sax": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
@@ -5694,7 +5905,7 @@
"dev": true, "dev": true,
"requires": { "requires": {
"debug": "2.6.9", "debug": "2.6.9",
"depd": "1.1.1", "depd": "1.1.2",
"destroy": "1.0.4", "destroy": "1.0.4",
"encodeurl": "1.0.1", "encodeurl": "1.0.1",
"escape-html": "1.0.3", "escape-html": "1.0.3",
@@ -5719,6 +5930,11 @@
} }
} }
}, },
"serialize-javascript": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.4.0.tgz",
"integrity": "sha1-fJWFFNtqwkQ6irwGLcn3iGp/YAU="
},
"serve-static": { "serve-static": {
"version": "1.13.1", "version": "1.13.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz",
@@ -6109,15 +6325,15 @@
} }
}, },
"svelte": { "svelte": {
"version": "1.49.1", "version": "1.51.1",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-1.49.1.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-1.51.1.tgz",
"integrity": "sha512-oiB4nF19AAQsD+lvJ1ycb58Iqc1NoZjNvyf0dG+i/RjFU3CLQtCO7pdiJN72IwPYtHAqMRFra/Qkayg8VSFH4A==", "integrity": "sha512-Uf9huHmtXLzdmkfuEYQeXMvFE9dt8M9/jzujX3/Q2Ek2dzvVz1ARTLP76hlnGqUf4q28w8wfkQxFEx4LgDV5MQ==",
"dev": true "dev": true
}, },
"svelte-loader": { "svelte-loader": {
"version": "2.3.2", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/svelte-loader/-/svelte-loader-2.3.2.tgz", "resolved": "https://registry.npmjs.org/svelte-loader/-/svelte-loader-2.3.3.tgz",
"integrity": "sha512-0lIt2GGqHtLETRl8rixXW8Vkij/GnukU+3gZ3Yvww1c2XwVjaucVXQgui5J7bStAtdAmOq+DHjGbwLPsgg47cg==", "integrity": "sha512-OdBoANPvNdP1G3DVzHXdjMetv2kFrv7+R5lhKhcp+E6ew3Gna20+Ja+Sq3gVN9O4vwfhTfrWBhWMFtbfJr1kug==",
"dev": true, "dev": true,
"requires": { "requires": {
"loader-utils": "1.1.0", "loader-utils": "1.1.0",
@@ -6497,9 +6713,9 @@
} }
}, },
"wait-on": { "wait-on": {
"version": "2.0.2", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-2.0.2.tgz", "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-2.1.0.tgz",
"integrity": "sha1-CoT9BwJMb8Joyw6r5YW+IXqvK6o=", "integrity": "sha512-hDwJ674+7dfiiK/cxtYCwPxlnjXDjto/pCz1PF02sXUhqCqCWsgvxZln0699PReWqXXgkxqkF6DDo5Rj9sjNvw==",
"dev": true, "dev": true,
"requires": { "requires": {
"core-js": "2.5.3", "core-js": "2.5.3",
@@ -6517,6 +6733,15 @@
} }
} }
}, },
"walk-sync": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-0.3.2.tgz",
"integrity": "sha512-FMB5VqpLqOCcqrzA9okZFc0wq0Qbmdm396qJxvQZhDpyu0W95G9JCmp74tx7iyYnyOcBtUuKJsgIKAqjozvmmQ==",
"requires": {
"ensure-posix-path": "1.0.2",
"matcher-collection": "1.0.5"
}
},
"watchpack": { "watchpack": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.4.0.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.4.0.tgz",
@@ -6532,7 +6757,7 @@
"resolved": "https://registry.npmjs.org/webpack/-/webpack-3.10.0.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-3.10.0.tgz",
"integrity": "sha512-fxxKXoicjdXNUMY7LIdY89tkJJJ0m1Oo8PQutZ5rLgWbV5QVKI15Cn7+/IHnRTd3vfKfiwBx6SBqlorAuNA8LA==", "integrity": "sha512-fxxKXoicjdXNUMY7LIdY89tkJJJ0m1Oo8PQutZ5rLgWbV5QVKI15Cn7+/IHnRTd3vfKfiwBx6SBqlorAuNA8LA==",
"requires": { "requires": {
"acorn": "5.2.1", "acorn": "5.3.0",
"acorn-dynamic-import": "2.0.2", "acorn-dynamic-import": "2.0.2",
"ajv": "5.5.2", "ajv": "5.5.2",
"ajv-keywords": "2.1.1", "ajv-keywords": "2.1.1",

View File

@@ -1,6 +1,6 @@
{ {
"name": "sapper", "name": "sapper",
"version": "0.3.0", "version": "0.5.1",
"description": "Military-grade apps, engineered by Svelte", "description": "Military-grade apps, engineered by Svelte",
"main": "lib/index.js", "main": "lib/index.js",
"bin": { "bin": {
@@ -10,6 +10,7 @@
"cli", "cli",
"lib", "lib",
"runtime", "runtime",
"runtime.js",
"webpack" "webpack"
], ],
"directories": { "directories": {
@@ -17,12 +18,19 @@
}, },
"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",
"locate-character": "^2.0.5",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"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"
}, },

View File

@@ -68,10 +68,16 @@ function render(Component: ComponentConstructor, data: any, scroll: ScrollPositi
} }
} }
function prepare_route(Component, data) { function prepare_route(Component: ComponentConstructor, data: RouteData) {
return Promise.resolve( if (!Component.preload) {
Component.preload ? Component.preload(data) : {} return { Component, data };
).then(preloaded => { }
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) Object.assign(data, preloaded)
return { Component, data }; return { Component, data };
}); });
@@ -164,20 +170,24 @@ let prefetching: {
promise: Promise<{ Component: ComponentConstructor, data: any }>; promise: Promise<{ Component: ComponentConstructor, data: any }>;
} = null; } = null;
function prefetch(event: MouseEvent | TouchEvent) { export function prefetch(href: string) {
const a: HTMLAnchorElement = <HTMLAnchorElement>findAnchor(<Node>event.target); const selected = select_route(new URL(href));
if (!a || a.rel !== 'prefetch') return;
const selected = select_route(new URL(a.href));
if (selected) { if (selected) {
prefetching = { prefetching = {
href: a.href, href,
promise: selected.route.load().then(mod => prepare_route(mod.default, selected.data)) 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; let inited: boolean;
export function init(_target: Node, _routes: Route[]) { export function init(_target: Node, _routes: Route[]) {
@@ -189,8 +199,8 @@ export function init(_target: Node, _routes: Route[]) {
window.addEventListener('popstate', handle_popstate); window.addEventListener('popstate', handle_popstate);
// prefetch // prefetch
window.addEventListener('touchstart', prefetch); window.addEventListener('touchstart', handle_touchstart_mouseover);
window.addEventListener('mouseover', prefetch); window.addEventListener('mouseover', handle_touchstart_mouseover);
inited = true; inited = true;
} }

3
test/app/.gitignore vendored
View File

@@ -3,4 +3,5 @@ node_modules
.sapper .sapper
yarn.lock yarn.lock
cypress/screenshots cypress/screenshots
templates/.* templates/.*
dist

View File

@@ -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",

View File

@@ -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",

View File

@@ -7,12 +7,13 @@
<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 on:click='goto("/blog/what-is-sapper")'>What is Sapper?</button> <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 } from '../../../runtime.js'; import { goto, prefetch } from '../../../runtime.js';
export default { export default {
components: { components: {
@@ -20,7 +21,8 @@
}, },
methods: { methods: {
goto goto,
prefetch
} }
}; };
</script> </script>

View File

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

View File

@@ -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 };
}); });
} }

View File

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

View File

@@ -3,7 +3,7 @@
<script> <script>
export default { export default {
preload({ url }) { preload({ url }) {
return { url }; if (url) return { url };
} }
}; };
</script> </script>

View File

@@ -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>

View File

@@ -5,10 +5,17 @@ const serve = require('serve-static');
const Nightmare = require('nightmare'); const Nightmare = require('nightmare');
const getPort = require('get-port'); const getPort = require('get-port');
const fetch = require('node-fetch'); const fetch = require('node-fetch');
const walkSync = require('walk-sync');
run('production'); run('production');
run('development'); run('development');
Nightmare.action('page', {
title(done) {
this.evaluate_now(() => document.querySelector('h1').textContent, done);
}
});
function run(env) { function run(env) {
describe(`env=${env}`, function () { describe(`env=${env}`, function () {
this.timeout(20000); this.timeout(20000);
@@ -62,52 +69,60 @@ function run(env) {
}); });
} }
before(async () => { before(() => {
process.chdir(path.resolve(__dirname, '../app')); process.chdir(path.resolve(__dirname, '../app'));
process.env.NODE_ENV = env; process.env.NODE_ENV = env;
let exec_promise = Promise.resolve();
let sapper;
if (env === 'production') { if (env === 'production') {
const cli = path.resolve(__dirname, '../../cli/index.js'); const cli = path.resolve(__dirname, '../../cli/index.js');
await exec(`${cli} build`); exec_promise = exec(`${cli} build`).then(() => exec(`${cli} export`));
} }
const resolved = require.resolve('../..'); return exec_promise.then(() => {
delete require.cache[resolved]; const resolved = require.resolve('../..');
const sapper = require(resolved); delete require.cache[resolved];
sapper = require(resolved);
PORT = await getPort(); return getPort();
base = `http://localhost:${PORT}`; }).then(port => {
PORT = port;
base = `http://localhost:${PORT}`;
global.fetch = (url, opts) => { global.fetch = (url, opts) => {
if (url[0] === '/') url = `${base}${url}`; if (url[0] === '/') url = `${base}${url}`;
return fetch(url, opts); return fetch(url, opts);
}; };
let captured; let captured;
capture = async fn => { capture = fn => {
const result = captured = []; const result = captured = [];
await fn(); return fn().then(() => {
captured = null; captured = null;
return result; return result;
}; });
};
const app = express(); const app = express();
app.use(serve('assets')); app.use(serve('assets'));
app.use((req, res, next) => { app.use((req, res, next) => {
if (captured) captured.push(req); if (captured) captured.push(req);
next(); next();
}); });
middleware = sapper(); middleware = sapper();
app.use(middleware); app.use(middleware);
return new Promise((fulfil, reject) => { return new Promise((fulfil, reject) => {
server = app.listen(PORT, err => { server = app.listen(PORT, err => {
if (err) reject(err); if (err) reject(err);
else fulfil(); else fulfil();
});
}); });
}); });
}); });
@@ -137,175 +152,265 @@ function run(env) {
}); });
}); });
afterEach(async () => { afterEach(() => {
await nightmare.end(); return nightmare.end();
}); });
it('serves /', async () => { it('serves /', () => {
const title = await nightmare return nightmare.goto(base).page.title().then(title => {
.goto(base) assert.equal(title, 'Great success!');
.evaluate(() => document.querySelector('h1').textContent);
assert.equal(title, 'Great success!');
});
it('serves static route', async () => {
const title = await nightmare
.goto(`${base}/about`)
.evaluate(() => document.querySelector('h1').textContent);
assert.equal(title, 'About this site');
});
it('serves dynamic route', async () => {
const title = await nightmare
.goto(`${base}/blog/what-is-sapper`)
.evaluate(() => document.querySelector('h1').textContent);
assert.equal(title, 'What is Sapper?');
});
it('navigates to a new page without reloading', async () => {
await nightmare.goto(base).wait(() => window.READY).wait(100);
const requests = await capture(async () => {
await nightmare.click('a[href="/about"]');
}); });
assert.equal(
await nightmare.path(),
'/about'
);
assert.equal(
await nightmare.evaluate(() => document.title),
'About'
);
assert.deepEqual(requests.map(r => r.url), []);
}); });
it('navigates programmatically', async () => { it('serves static route', () => {
await nightmare 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).wait(() => window.READY).wait(200)
.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`) .goto(`${base}/about`)
.wait(() => window.READY) .wait(() => window.READY)
.click('button') .click('.goto')
.wait(() => window.location.pathname === '/blog/what-is-sapper') .wait(() => window.location.pathname === '/blog/what-is-sapper')
.wait(100); .wait(100)
.title()
assert.equal( .then(title => {
await nightmare.evaluate(() => document.title), assert.equal(title, 'What is Sapper?');
'What is Sapper?' });
);
}); });
it('scrolls to active deeplink', async () => { it('prefetches programmatically', () => {
const scrollY = await nightmare return nightmare
.goto(`${base}/about`)
.wait(() => window.READY)
.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`) .goto(`${base}/blog/a-very-long-post#four`)
.wait(() => window.READY) .wait(() => window.READY)
.wait(100) .wait(100)
.evaluate(() => window.scrollY); .evaluate(() => window.scrollY)
.then(scrollY => {
assert.ok(scrollY > 0, scrollY); assert.ok(scrollY > 0, scrollY);
});
}); });
it('reuses prefetch promise', async () => { it('reuses prefetch promise', () => {
await nightmare return nightmare
.goto(`${base}/blog`) .goto(`${base}/blog`)
.wait(() => window.READY) .wait(() => window.READY)
.wait(200); .wait(200)
.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'
]);
const mouseover_requests = (await capture(async () => { return capture(() => {
await nightmare return nightmare
.mouseover('[href="/blog/what-is-sapper"]') .click('[href="/blog/what-is-sapper"]')
.wait(200); .wait(200);
})).map(r => r.url); });
})
assert.deepEqual(mouseover_requests, [ .then(click_requests => {
'/api/blog/what-is-sapper' assert.deepEqual(click_requests.map(r => r.url), []);
]); });
const click_requests = (await capture(async () => {
await nightmare
.click('[href="/blog/what-is-sapper"]')
.wait(200);
})).map(r => r.url);
assert.deepEqual(click_requests, []);
}); });
it('cancels navigation if subsequent navigation occurs during preload', async () => { it('cancels navigation if subsequent navigation occurs during preload', () => {
await nightmare return nightmare
.goto(base) .goto(base)
.wait(() => window.READY) .wait(() => window.READY)
.click('a[href="/slow-preload"]') .click('a[href="/slow-preload"]')
.wait(100) .wait(100)
.click('a[href="/about"]') .click('a[href="/about"]')
.wait(100); .wait(100)
.then(() => nightmare.path())
assert.equal( .then(path => {
await nightmare.path(), assert.equal(path, '/about');
'/about' return nightmare.title();
); })
.then(title => {
assert.equal( assert.equal(title, 'About');
await nightmare.evaluate(() => document.querySelector('h1').textContent), return nightmare.evaluate(() => window.fulfil({})).wait(100);
'About this site' })
); .then(() => nightmare.path())
.then(path => {
await nightmare assert.equal(path, '/about');
.evaluate(() => window.fulfil({})) return nightmare.title();
.wait(100); })
.then(title => {
assert.equal( assert.equal(title, 'About');
await nightmare.path(), });
'/about'
);
assert.equal(
await nightmare.evaluate(() => document.querySelector('h1').textContent),
'About this site'
);
}); });
it('passes entire request object to preload', async () => { it('passes entire request object to preload', () => {
const html = await nightmare return nightmare
.goto(`${base}/show-url`) .goto(`${base}/show-url`)
.evaluate(() => document.querySelector('p').innerHTML); .wait(() => window.READY)
.evaluate(() => document.querySelector('p').innerHTML)
.end().then(html => {
assert.equal(html, `URL is /show-url`);
});
});
assert.equal(html, `URL is /show-url`); it('calls a delete handler', () => {
return nightmare
.goto(`${base}/delete-test`)
.wait(() => window.READY)
.click('.del')
.wait(() => window.deleted)
.evaluate(() => window.deleted.id)
.then(id => {
assert.equal(id, 42);
});
}); });
}); });
describe('headers', () => { describe('headers', () => {
it('sets Content-Type and Link...preload headers', async () => { it('sets Content-Type and Link...preload headers', () => {
const { headers } = await get('/'); return get('/').then(({ headers }) => {
assert.equal(
headers['Content-Type'],
'text/html'
);
assert.equal( assert.ok(
headers['Content-Type'], /<\/client\/main.\w+\.js>;rel="preload";as="script", <\/client\/_.\d+.\w+.js>;rel="preload";as="script"/.test(headers['Link']),
'text/html' headers['Link']
); );
});
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) { function exec(cmd) {
return new Promise((fulfil, reject) => { return new Promise((fulfil, reject) => {
require('child_process').exec(cmd, (err, stdout, stderr) => { const parts = cmd.split(' ');
if (err) { const proc = require('child_process').spawn(parts.shift(), parts);
process.stdout.write(stdout);
process.stderr.write(stderr);
return reject(err); proc.stdout.on('data', data => {
} process.stdout.write(data);
fulfil();
}); });
proc.stderr.on('data', data => {
process.stderr.write(data);
});
proc.on('error', reject);
proc.on('close', () => fulfil());
}); });
} }

View File

@@ -6,7 +6,12 @@ module.exports = {
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'
]
}; };
}, },
@@ -36,4 +41,4 @@ module.exports = {
}; };
} }
} }
}; };