prevent unnecessary promise chains, and hoist handler function

This commit is contained in:
Rich Harris
2018-01-14 00:01:10 -05:00
3 changed files with 258 additions and 219 deletions

View File

@@ -23,9 +23,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,81 +127,88 @@ 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 => {
Object.assign(data, preloaded);
return mod.render(data);
});
return 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 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);
}
return;
}
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);
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')
}));
});
}; };
} }

View File

@@ -31,10 +31,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 +57,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);
} }
}; };
}) })

View File

@@ -62,52 +62,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,157 +145,170 @@ 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) .goto(base)
.evaluate(() => document.querySelector('h1').textContent); .evaluate(() => document.querySelector('h1').textContent)
.then(title => {
assert.equal(title, 'Great success!'); assert.equal(title, 'Great success!');
});
}); });
it('serves static route', async () => { it('serves static route', () => {
const title = await nightmare return nightmare
.goto(`${base}/about`) .goto(`${base}/about`)
.evaluate(() => document.querySelector('h1').textContent); .evaluate(() => document.querySelector('h1').textContent)
.then(title => {
assert.equal(title, 'About this site'); assert.equal(title, 'About this site');
});
}); });
it('serves dynamic route', async () => { it('serves dynamic route', () => {
const title = await nightmare return nightmare
.goto(`${base}/blog/what-is-sapper`) .goto(`${base}/blog/what-is-sapper`)
.evaluate(() => document.querySelector('h1').textContent); .evaluate(() => document.querySelector('h1').textContent)
.then(title => {
assert.equal(title, 'What is Sapper?'); assert.equal(title, 'What is Sapper?');
});
}); });
it('navigates to a new page without reloading', async () => { it('navigates to a new page without reloading', () => {
await nightmare.goto(base).wait(() => window.READY).wait(200); let requests;
return nightmare
.goto(base).wait(() => window.READY).wait(200)
.then(() => {
return capture(() => {
return nightmare.click('a[href="/about"]');
});
})
.then(reqs => {
requests = reqs;
const requests = await capture(async () => { return nightmare.path();
await nightmare.click('a[href="/about"]'); })
}); .then(path => {
assert.equal(path, '/about');
assert.equal( return nightmare.evaluate(() => document.title);
await nightmare.path(), })
'/about' .then(title => {
); assert.equal(title, 'About');
assert.equal( assert.deepEqual(requests.map(r => r.url), []);
await nightmare.evaluate(() => document.title), });
'About'
);
assert.deepEqual(requests.map(r => r.url), []);
}); });
it('navigates programmatically', async () => { it('navigates programmatically', () => {
await nightmare 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)
.then(() => nightmare.evaluate(() => document.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())
.then(path => {
assert.equal(path, '/about');
assert.equal( return nightmare.evaluate(() => document.querySelector('h1').textContent);
await nightmare.path(), })
'/about' .then(header_text => {
); assert.equal(header_text, 'About this site');
assert.equal( return nightmare.evaluate(() => window.fulfil({})).wait(100);
await nightmare.evaluate(() => document.querySelector('h1').textContent), })
'About this site' .then(() => nightmare.path())
); .then(path => {
assert.equal(path, '/about');
await nightmare return nightmare.evaluate(() => document.querySelector('h1').textContent);
.evaluate(() => window.fulfil({})) })
.wait(100); .then(header_text => {
assert.equal(header_text, 'About this site');
assert.equal( return nightmare.evaluate(() => window.fulfil({})).wait(100);
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); .evaluate(() => document.querySelector('p').innerHTML)
.then(html => {
assert.equal(html, `URL is /show-url`); assert.equal(html, `URL is /show-url`);
});
}); });
it('calls a delete handler', async () => { it('calls a delete handler', async () => {
@@ -305,18 +326,18 @@ function run(env) {
}); });
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']
);
}); });
}); });
}); });