Add goto function (#46)

* WIP

* programmatic navigation

* get tests working

* wait longer
This commit is contained in:
Rich Harris
2017-12-23 13:15:40 -05:00
committed by GitHub
parent e8d510b261
commit db1c1f332a
11 changed files with 1279 additions and 3764 deletions

View File

@@ -187,7 +187,8 @@ function get_route_handler(fn) {
next(); next();
} catch(err) { } catch(err) {
res.status(500).end(templates.render(500, { res.status(500);
res.end(templates.render(500, {
title: (err && err.name) || 'Internal server error', title: (err && err.name) || 'Internal server error',
url, url,
error: escape_html(err && (err.details || err.message || err) || 'Unknown error'), error: escape_html(err && (err.details || err.message || err) || 'Unknown error'),
@@ -201,7 +202,8 @@ function get_not_found_handler(fn) {
return function handle_not_found(req, res) { return function handle_not_found(req, res) {
const asset_cache = fn(); const asset_cache = fn();
res.status(404).end(templates.render(404, { res.status(404);
res.end(templates.render(404, {
title: 'Not found', title: 'Not found',
status: 404, status: 404,
method: req.method, method: req.method,

1223
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@
"webpack-hot-middleware": "^2.21.0" "webpack-hot-middleware": "^2.21.0"
}, },
"devDependencies": { "devDependencies": {
"css-loader": "^0.28.7",
"eslint": "^4.13.1", "eslint": "^4.13.1",
"eslint-plugin-import": "^2.8.0", "eslint-plugin-import": "^2.8.0",
"express": "^4.16.2", "express": "^4.16.2",
@@ -29,6 +30,9 @@
"nightmare": "^2.10.0", "nightmare": "^2.10.0",
"node-fetch": "^1.7.3", "node-fetch": "^1.7.3",
"npm-run-all": "^4.1.2", "npm-run-all": "^4.1.2",
"style-loader": "^0.19.1",
"svelte": "^1.49.1",
"svelte-loader": "^2.3.2",
"wait-on": "^2.0.2" "wait-on": "^2.0.2"
}, },
"scripts": { "scripts": {

View File

@@ -6,12 +6,17 @@ export let component;
let target; let target;
let routes; let routes;
const history = typeof window !== 'undefined' ? window.history : {
pushState: () => {},
replaceState: () => {},
};
const scroll_history = {}; const scroll_history = {};
let uid = 1; let uid = 1;
let cid; let cid;
if ('scrollRestoration' in history) { if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual' history.scrollRestoration = 'manual';
} }
function select_route(url) { function select_route(url) {
@@ -39,7 +44,7 @@ function render(Component, data, scroll) {
} else { } else {
// first load — remove SSR'd <head> contents // first load — remove SSR'd <head> contents
const start = document.querySelector('#sapper-head-start'); const start = document.querySelector('#sapper-head-start');
let end = document.querySelector('#sapper-head-end'); const end = document.querySelector('#sapper-head-end');
if (start && end) { if (start && end) {
while (start.nextSibling !== end) detach(start.nextSibling); while (start.nextSibling !== end) detach(start.nextSibling);
@@ -75,8 +80,6 @@ function navigate(url, id) {
id = cid = ++uid; id = cid = ++uid;
scroll_history[cid] = { x: 0, y: 0 }; scroll_history[cid] = { x: 0, y: 0 };
history.pushState({ id }, '', url.href);
} }
selected.route.load().then(mod => { selected.route.load().then(mod => {
@@ -123,6 +126,7 @@ function handle_click(event) {
if (navigate(url, null)) { if (navigate(url, null)) {
event.preventDefault(); event.preventDefault();
history.pushState({ id: cid }, '', url.href);
} }
} }
@@ -173,7 +177,7 @@ export function init(_target, _routes) {
inited = true; inited = true;
} }
const scroll = scroll_history[uid] = scroll_state(); scroll_history[uid] = scroll_state();
history.replaceState({ id: uid }, '', window.location.href); history.replaceState({ id: uid }, '', window.location.href);
navigate(new URL(window.location), uid); navigate(new URL(window.location), uid);
@@ -189,4 +193,12 @@ function scroll_state() {
x: window.scrollX, x: window.scrollX,
y: window.scrollY y: window.scrollY
}; };
}
export function goto(href, opts = {}) {
if (navigate(new URL(href, window.location.href))) {
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
} else {
window.location.href = href;
}
} }

View File

@@ -6,14 +6,21 @@
<h1>About this site</h1> <h1>About this site</h1>
<p>This is the 'about' page. There's not much here.</p> <p>This is the 'about' page. There's not much here.</p>
<button on:click='goto("/blog/what-is-sapper")'>What is Sapper?</button>
</Layout> </Layout>
<script> <script>
import Layout from './_components/Layout.html'; import Layout from './_components/Layout.html';
import { goto } from '../../../runtime/app.js';
export default { export default {
components: { components: {
Layout Layout
},
methods: {
goto
} }
}; };
</script> </script>

View File

@@ -1,4 +1,4 @@
import { init } from '__app__'; import { init } from '../../../runtime/app.js';
// `routes` is an array of route objects injected by Sapper // `routes` is an array of route objects injected by Sapper
init(document.querySelector('#sapper'), __routes__); init(document.querySelector('#sapper'), __routes__);

View File

@@ -1,7 +1,5 @@
const config = require('../../webpack/config.js'); const config = require('../../webpack/config.js');
const webpack = require('webpack'); const webpack = require('webpack');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
module.exports = { module.exports = {
entry: config.client.entry(), entry: config.client.entry(),
@@ -24,27 +22,18 @@ module.exports = {
} }
} }
}, },
config.dev && { {
test: /\.css$/, test: /\.css$/,
use: [ use: [
{ loader: "style-loader" }, { loader: "style-loader" },
{ loader: "css-loader" } { loader: "css-loader" }
] ]
},
!config.dev && {
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [{ loader: 'css-loader', options: { sourceMap: config.dev } }]
})
} }
].filter(Boolean) ].filter(Boolean)
}, },
plugins: [ plugins: [
config.dev && new webpack.HotModuleReplacementPlugin(), config.dev && new webpack.HotModuleReplacementPlugin(),
!config.dev && new ExtractTextPlugin('main.css'), !config.dev && new webpack.optimize.ModuleConcatenationPlugin()
!config.dev && new webpack.optimize.ModuleConcatenationPlugin(),
!config.dev && new UglifyJSPlugin()
].filter(Boolean), ].filter(Boolean),
devtool: config.dev ? 'inline-source-map' : false devtool: config.dev ? 'inline-source-map' : false
}; };

View File

@@ -1,7 +1,4 @@
const config = require('../../webpack/config.js'); const config = require('../../webpack/config.js');
const webpack = require('webpack');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
module.exports = { module.exports = {
entry: config.server.entry(), entry: config.server.entry(),

View File

@@ -1,4 +1,3 @@
const fs = require('fs');
const path = require('path'); const path = require('path');
const assert = require('assert'); const assert = require('assert');
const express = require('express'); const express = require('express');
@@ -12,7 +11,7 @@ run('development');
function run(env) { function run(env) {
describe(`env=${env}`, function () { describe(`env=${env}`, function () {
this.timeout(30000); this.timeout(5000);
let PORT; let PORT;
let server; let server;
@@ -23,7 +22,7 @@ function run(env) {
let base; let base;
function get(url) { function get(url) {
return new Promise((fulfil, reject) => { return new Promise(fulfil => {
const req = { const req = {
url, url,
method: 'GET' method: 'GET'
@@ -93,7 +92,7 @@ function run(env) {
return result; return result;
}; };
app = express(); const app = express();
app.use(serve('assets')); app.use(serve('assets'));
@@ -128,6 +127,14 @@ function run(env) {
nightmare.on('console', (type, ...args) => { nightmare.on('console', (type, ...args) => {
console[type](...args); console[type](...args);
}); });
nightmare.on('page', (type, ...args) => {
if (type === 'error') {
console.error(args[1]);
} else {
console.warn(type, args);
}
});
}); });
afterEach(async () => { afterEach(async () => {
@@ -177,6 +184,20 @@ function run(env) {
assert.deepEqual(requests.map(r => r.url), []); assert.deepEqual(requests.map(r => r.url), []);
}); });
it('navigates programmatically', async () => {
await nightmare
.goto(`${base}/about`)
.wait(() => window.READY)
.click('button')
.wait(() => window.location.pathname === '/blog/what-is-sapper')
.wait(100);
assert.equal(
await nightmare.evaluate(() => document.title),
'What is Sapper?'
);
});
}); });
describe('headers', () => { describe('headers', () => {
@@ -188,8 +209,8 @@ function run(env) {
'text/html' 'text/html'
); );
assert.ok assert.ok(
(/<\/client\/main.\w+\.js\>;rel="preload";as="script", <\/client\/_.\d+.\w+.js>;rel="preload";as="script"/.test(headers['Link']), /<\/client\/main.\w+\.js>;rel="preload";as="script", <\/client\/_.\d+.\w+.js>;rel="preload";as="script"/.test(headers['Link']),
headers['Link'] headers['Link']
); );
}); });
@@ -200,10 +221,13 @@ function run(env) {
function exec(cmd) { function exec(cmd) {
return new Promise((fulfil, reject) => { return new Promise((fulfil, reject) => {
require('child_process').exec(cmd, (err, stdout, stderr) => { require('child_process').exec(cmd, (err, stdout, stderr) => {
if (err) return reject(err); if (err) {
process.stdout.write(stdout);
process.stderr.write(stderr);
return reject(err);
}
process.stdout.write(stdout);
process.stderr.write(stderr);
fulfil(); fulfil();
}); });
}); });

View File

@@ -1,75 +0,0 @@
const fs = require('fs');
const path = require('path');
const rimraf = require('rimraf');
const child_process = require('child_process');
// ensure sapper doesn't exist in app/node_modules
rimraf.sync(
path.join(__dirname, 'app/node_modules/sapper')
);
rimraf.sync(
path.join(__dirname, 'app/node_modules/.bin/sapper')
);
// create symlinks
fs.symlinkSync(
path.join(__dirname, '..'),
path.join(__dirname, 'app/node_modules/sapper')
);
fs.symlinkSync(
path.join(__dirname, '../cli/index.js'),
path.join(__dirname, 'app/node_modules/.bin/sapper')
);
const app_dir = path.join(__dirname, 'app');
function start_server() {
const server = child_process.spawn(process.execPath, ['server.js'], {
cwd: app_dir,
env: {
NODE_ENV: 'development'
},
stdio: 'pipe'
});
server.stdout.on('data', (data) => {
process.stdout.write(data);
});
server.stderr.on('data', (data) => {
process.stderr.write(data);
});
}
function launch() {
if (process.argv[2] === '--dev') {
start_server();
} else {
child_process.exec(`npm run build`, {
cwd: app_dir
}, (err, stdout, stderr) => {
if (err) throw err;
process.stdout.write(stdout);
process.stderr.write(stderr);
start_server();
});
}
}
// this is a terrible hack
if (process.env.APPVEYOR) {
child_process.exec(`npm install`, {
cwd: app_dir
}, (err, stdout, stderr) => {
if (err) throw err;
process.stdout.write(stdout);
process.stderr.write(stderr);
launch();
});
} else {
launch();
}

3644
yarn.lock

File diff suppressed because it is too large Load Diff