mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-15 20:34:44 +00:00
Merge branch 'master' into lazy-chokidar
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,4 +2,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
cypress/screenshots
|
cypress/screenshots
|
||||||
test/app/.sapper
|
test/app/.sapper
|
||||||
runtime.js
|
runtime.js
|
||||||
|
yarn.lock
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ sudo: false
|
|||||||
language: node_js
|
language: node_js
|
||||||
|
|
||||||
node_js:
|
node_js:
|
||||||
|
- "6"
|
||||||
- "stable"
|
- "stable"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# sapper changelog
|
# sapper changelog
|
||||||
|
|
||||||
|
## 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
|
## 0.3.2
|
||||||
|
|
||||||
* Expose `prefetch` function ([#61](https://github.com/sveltejs/sapper/pull/61))
|
* Expose `prefetch` function ([#61](https://github.com/sveltejs/sapper/pull/61))
|
||||||
|
|||||||
163
lib/index.js
163
lib/index.js
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,21 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const glob = require('glob');
|
const glob = require('glob');
|
||||||
|
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 => {
|
||||||
@@ -11,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 = (
|
||||||
@@ -30,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) {
|
||||||
@@ -52,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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
616
package-lock.json
generated
616
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sapper",
|
"name": "sapper",
|
||||||
"version": "0.3.2",
|
"version": "0.4.0",
|
||||||
"description": "Military-grade apps, engineered by Svelte",
|
"description": "Military-grade apps, engineered by Svelte",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -19,11 +19,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^2.3.0",
|
"chalk": "^2.3.0",
|
||||||
"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",
|
||||||
|
"serialize-javascript": "^1.4.0",
|
||||||
"webpack": "^3.10.0",
|
"webpack": "^3.10.0",
|
||||||
"webpack-hot-middleware": "^2.21.0"
|
"webpack-hot-middleware": "^2.21.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 };
|
||||||
});
|
});
|
||||||
@@ -176,10 +182,10 @@ export function prefetch(href: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handle_touchstart_mouseover(event: MouseEvent | TouchEvent) {
|
function handle_touchstart_mouseover(event: MouseEvent | TouchEvent) {
|
||||||
const a: HTMLAnchorElement = <HTMLAnchorElement>findAnchor(<Node>event.target);
|
const a: HTMLAnchorElement = <HTMLAnchorElement>findAnchor(<Node>event.target);
|
||||||
if (!a || a.rel !== 'prefetch') return;
|
if (!a || a.rel !== 'prefetch') return;
|
||||||
|
|
||||||
prefetch(a.href);
|
prefetch(a.href);
|
||||||
}
|
}
|
||||||
|
|
||||||
let inited: boolean;
|
let inited: boolean;
|
||||||
|
|||||||
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
|
||||||
|
}));
|
||||||
|
}
|
||||||
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>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
preload({ url }) {
|
preload({ url }) {
|
||||||
return { url };
|
if (url) return { url };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -9,6 +9,12 @@ const fetch = require('node-fetch');
|
|||||||
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 +68,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`);
|
||||||
}
|
}
|
||||||
|
|
||||||
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,173 +151,177 @@ 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(200);
|
|
||||||
|
|
||||||
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('.goto')
|
.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('prefetches programmatically', async () => {
|
it('prefetches programmatically', () => {
|
||||||
await nightmare
|
return nightmare
|
||||||
.goto(`${base}/about`)
|
.goto(`${base}/about`)
|
||||||
.wait(() => window.READY);
|
.wait(() => window.READY)
|
||||||
|
.then(() => {
|
||||||
const requests = await capture(async () => {
|
return capture(() => {
|
||||||
return await nightmare
|
return nightmare
|
||||||
.click('.prefetch')
|
.click('.prefetch')
|
||||||
.wait(100);
|
.wait(100);
|
||||||
});
|
});
|
||||||
|
})
|
||||||
assert.ok(!!requests.find(r => r.url === '/api/blog/why-the-name'));
|
.then(requests => {
|
||||||
|
assert.ok(!!requests.find(r => r.url === '/api/blog/why-the-name'));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('scrolls to active deeplink', async () => {
|
it('scrolls to active deeplink', () => {
|
||||||
const scrollY = await nightmare
|
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']
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -322,4 +340,4 @@ function exec(cmd) {
|
|||||||
fulfil();
|
fulfil();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user