This commit is contained in:
Rich Harris
2018-01-21 11:03:23 -05:00
parent 4590aa313c
commit 39b1fa89ce
27 changed files with 2392 additions and 401 deletions

268
src/middleware/index.js Normal file
View File

@@ -0,0 +1,268 @@
import * as fs from 'fs';
import * as path from 'path';
import serialize from 'serialize-javascript';
import escape_html from 'escape-html';
import * as route_manager from '../shared/route_manager.js';
import * as templates from '../shared/templates.js';
import create_app from '../shared/utils/create_app.js';
import create_watcher from '../shared/utils/create_watcher.js';
import * as compilers from '../shared/utils/compilers.js';
import generate_asset_cache from '../shared/generate_asset_cache.js';
import { dest, dev } from '../config.js';
function connect_dev() {
create_app();
const watcher = create_watcher();
let asset_cache;
const middleware = compose_handlers([
require('webpack-hot-middleware')(compilers.client, {
reload: true,
path: '/__webpack_hmr',
heartbeat: 10 * 1000
}),
(req, res, next) => {
watcher.ready.then(cache => {
asset_cache = cache;
next();
});
},
set_req_pathname,
get_asset_handler({
filter: pathname => pathname === '/index.html',
type: 'text/html',
cache: 'max-age=600',
fn: () => asset_cache.client.index
}),
get_asset_handler({
filter: pathname => pathname === '/service-worker.js',
type: 'application/javascript',
cache: 'max-age=600',
fn: () => asset_cache.client.service_worker
}),
get_asset_handler({
filter: pathname => pathname.startsWith('/client/'),
type: 'application/javascript',
cache: 'max-age=31536000',
fn: pathname => asset_cache.client.chunks[pathname]
}),
get_route_handler(() => asset_cache),
get_not_found_handler(() => asset_cache)
]);
middleware.close = () => {
watcher.close();
// TODO shut down chokidar
};
return middleware;
}
function connect_prod() {
const asset_cache = generate_asset_cache(
read_json(path.join(dest, 'stats.client.json')),
read_json(path.join(dest, 'stats.server.json'))
);
const middleware = compose_handlers([
set_req_pathname,
get_asset_handler({
filter: pathname => pathname === '/index.html',
type: 'text/html',
cache: 'max-age=600',
fn: () => asset_cache.client.index
}),
get_asset_handler({
filter: pathname => pathname === '/service-worker.js',
type: 'application/javascript',
cache: 'max-age=600',
fn: () => asset_cache.client.service_worker
}),
get_asset_handler({
filter: pathname => pathname.startsWith('/client/'),
type: 'application/javascript',
cache: 'max-age=31536000',
fn: pathname => asset_cache.client.chunks[pathname]
}),
get_route_handler(() => asset_cache),
get_not_found_handler(() => asset_cache)
]);
// here for API consistency between dev, and prod, but
// doesn't actually need to do anything
middleware.close = () => {};
return middleware;
}
export default dev ? connect_dev : connect_prod;
function set_req_pathname(req, res, next) {
req.pathname = req.url.replace(/\?.+/, '');
next();
}
function get_asset_handler(opts) {
return (req, res, next) => {
if (!opts.filter(req.pathname)) return next();
res.setHeader('Content-Type', opts.type);
res.setHeader('Cache-Control', opts.cache);
res.end(opts.fn(req.pathname));
};
}
const resolved = Promise.resolve();
function get_route_handler(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.setHeader('Link', `<${client.main_file}>;rel="preload";as="script", <${client.routes[route.id]}>;rel="preload";as="script"`);
const data = { params: req.params, query: req.query };
if (mod.preload) {
const promise = Promise.resolve(mod.preload(req)).then(preloaded => {
const serialized = try_serialize(preloaded);
Object.assign(data, preloaded);
return { rendered: mod.render(data), serialized };
});
return templates.stream(res, 200, {
scripts: promise.then(({ serialized }) => {
const main = `<script src='${client.main_file}'></script>`;
if (serialized) {
return `<script>__SAPPER__ = { preloaded: ${serialized} };</script>${main}`;
}
return main;
}),
html: promise.then(({ rendered }) => rendered.html),
head: promise.then(({ rendered }) => `<noscript id='sapper-head-start'></noscript>${rendered.head}<noscript id='sapper-head-end'></noscript>`),
styles: promise.then(({ rendered }) => (rendered.css && rendered.css.code ? `<style>${rendered.css.code}</style>` : ''))
});
} else {
const { html, head, css } = mod.render(data);
const page = templates.render(200, {
scripts: `<script src='${client.main_file}'></script>`,
html,
head: `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`,
styles: (css && css.code ? `<style>${css.code}</style>` : '')
});
res.end(page);
}
}
else {
const method = req.method.toLowerCase();
// 'delete' cannot be exported from a module because it is a keyword,
// so check for 'del' instead
const method_export = method === 'delete' ? 'del' : method;
const handler = mod[method_export];
if (handler) {
handler(req, res, next);
} else {
// no matching handler for method — 404
next();
}
}
}
return function find_route(req, res, next) {
const url = req.pathname;
// whatever happens, we're going to serve some HTML
res.setHeader('Content-Type', 'text/html');
resolved
.then(() => {
for (const route of route_manager.routes) {
if (route.test(url)) return handle_route(route, req, res, next, fn());
}
// no matching route — 404
next();
})
.catch(err => {
res.statusCode = 500;
res.end(templates.render(500, {
title: (err && err.name) || 'Internal server error',
url,
error: escape_html(err && (err.details || err.message || err) || 'Unknown error'),
stack: err && err.stack.split('\n').slice(1).join('\n')
}));
});
};
}
function get_not_found_handler(fn) {
return function handle_not_found(req, res) {
const asset_cache = fn();
res.statusCode = 404;
res.end(templates.render(404, {
title: 'Not found',
status: 404,
method: req.method,
scripts: `<script src='${asset_cache.client.main_file}'></script>`,
url: req.url
}));
};
}
function compose_handlers(handlers) {
return (req, res, next) => {
let i = 0;
function go() {
const handler = handlers[i];
if (handler) {
handler(req, res, () => {
i += 1;
go();
});
} else {
next();
}
}
go();
};
}
function read_json(file) {
return JSON.parse(fs.readFileSync(file, 'utf-8'));
}
function try_serialize(data) {
try {
return serialize(data);
} catch (err) {
return null;
}
}

154
src/middleware/tmp.json Normal file
View File

@@ -0,0 +1,154 @@
[
{
"x": 1979,
"y": 7.19
},
{
"x": 1980,
"y": 7.83
},
{
"x": 1981,
"y": 7.24
},
{
"x": 1982,
"y": 7.44
},
{
"x": 1983,
"y": 7.51
},
{
"x": 1984,
"y": 7.1
},
{
"x": 1985,
"y": 6.91
},
{
"x": 1986,
"y": 7.53
},
{
"x": 1987,
"y": 7.47
},
{
"x": 1988,
"y": 7.48
},
{
"x": 1989,
"y": 7.03
},
{
"x": 1990,
"y": 6.23
},
{
"x": 1991,
"y": 6.54
},
{
"x": 1992,
"y": 7.54
},
{
"x": 1993,
"y": 6.5
},
{
"x": 1994,
"y": 7.18
},
{
"x": 1995,
"y": 6.12
},
{
"x": 1996,
"y": 7.87
},
{
"x": 1997,
"y": 6.73
},
{
"x": 1998,
"y": 6.55
},
{
"x": 1999,
"y": 6.23
},
{
"x": 2000,
"y": 6.31
},
{
"x": 2001,
"y": 6.74
},
{
"x": 2002,
"y": 5.95
},
{
"x": 2003,
"y": 6.13
},
{
"x": 2004,
"y": 6.04
},
{
"x": 2005,
"y": 5.56
},
{
"x": 2006,
"y": 5.91
},
{
"x": 2007,
"y": 4.29
},
{
"x": 2008,
"y": 4.72
},
{
"x": 2009,
"y": 5.38
},
{
"x": 2010,
"y": 4.92
},
{
"x": 2011,
"y": 4.61
},
{
"x": 2012,
"y": 3.62
},
{
"x": 2013,
"y": 5.35
},
{
"x": 2014,
"y": 5.28
},
{
"x": 2015,
"y": 4.63
},
{
"x": 2016,
"y": 4.72
}
]