mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-13 19:45:26 +00:00
252 lines
7.0 KiB
TypeScript
252 lines
7.0 KiB
TypeScript
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import mkdirp from 'mkdirp';
|
|
import rimraf from 'rimraf';
|
|
import serialize from 'serialize-javascript';
|
|
import escape_html from 'escape-html';
|
|
import { create_routes, templates, create_compilers, create_template } from 'sapper/core.js';
|
|
import { dest, entry, isDev, src } from '../config';
|
|
import { Route, Template } from '../interfaces';
|
|
|
|
const dev = isDev();
|
|
|
|
type Assets = {
|
|
index: string;
|
|
service_worker: string;
|
|
client: Record<string, string>;
|
|
}
|
|
|
|
export default function middleware({ routes }: {
|
|
routes: Route[]
|
|
}) {
|
|
const client_info = JSON.parse(fs.readFileSync(path.join(dest, 'client_info.json'), 'utf-8'));
|
|
|
|
const assets: Assets = {
|
|
index: try_read(path.join(dest, 'index.html')),
|
|
service_worker: try_read(path.join(dest, 'service-worker.js')),
|
|
client: fs.readdirSync(path.join(dest, 'client')).reduce((lookup: Record<string, string>, file: string) => {
|
|
lookup[file] = try_read(path.join(dest, 'client', file));
|
|
return lookup;
|
|
}, {})
|
|
};
|
|
|
|
const template = create_template();
|
|
|
|
const middleware = compose_handlers([
|
|
set_req_pathname,
|
|
|
|
get_asset_handler({
|
|
filter: (pathname: string) => pathname === '/index.html',
|
|
type: 'text/html',
|
|
cache: 'max-age=600',
|
|
fn: () => assets.index
|
|
}),
|
|
|
|
get_asset_handler({
|
|
filter: (pathname: string) => pathname === '/service-worker.js',
|
|
type: 'application/javascript',
|
|
cache: 'max-age=600',
|
|
fn: () => assets.service_worker
|
|
}),
|
|
|
|
get_asset_handler({
|
|
filter: (pathname: string) => pathname.startsWith('/client/'),
|
|
type: 'application/javascript',
|
|
cache: 'max-age=31536000',
|
|
fn: (pathname: string) => assets.client[pathname.replace('/client/', '')]
|
|
}),
|
|
|
|
get_route_handler(client_info.assetsByChunkName, () => assets, () => routes, () => template),
|
|
|
|
get_not_found_handler(client_info.assetsByChunkName, () => routes, () => template)
|
|
]);
|
|
|
|
// here for API consistency between dev, and prod, but
|
|
// doesn't actually need to do anything
|
|
middleware.close = () => {};
|
|
|
|
return middleware;
|
|
}
|
|
|
|
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(chunks: Record<string, string>, get_assets: () => Assets, get_routes: () => Route[], get_template: () => Template) {
|
|
function handle_route(route, req, res, next, { client }) {
|
|
req.params = route.params(route.pattern.exec(req.pathname));
|
|
|
|
const mod = route.module;
|
|
|
|
if (route.type === 'page') {
|
|
// for page routes, we're going to serve some HTML
|
|
res.setHeader('Content-Type', 'text/html');
|
|
|
|
// preload main.js and current route
|
|
// TODO detect other stuff we can preload? images, CSS, fonts?
|
|
res.setHeader('Link', `</client/${chunks.main}>;rel="preload";as="script", </client/${chunks[route.id]}>;rel="preload";as="script"`);
|
|
|
|
const data = { params: req.params, query: req.query };
|
|
|
|
const template = get_template();
|
|
|
|
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 template.stream(res, {
|
|
scripts: promise.then(({ serialized }) => {
|
|
const main = `<script src='/client/${chunks.main}'></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 = template.render({
|
|
scripts: `<script src='/client/${chunks.main}'></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;
|
|
|
|
const routes = get_routes();
|
|
|
|
try {
|
|
for (const route of routes) {
|
|
if (route.pattern.test(url)) return handle_route(route, req, res, next, get_assets());
|
|
}
|
|
|
|
// no matching route — 404
|
|
next();
|
|
} catch (error) {
|
|
res.statusCode = 500;
|
|
res.setHeader('Content-Type', 'text/html');
|
|
|
|
const route = get_routes().find((route: Route) => route.pattern.test('/5xx'));
|
|
const rendered = route ? route.module.render({
|
|
status: 500,
|
|
error
|
|
}) : { head: '', css: '', html: 'Not found' };
|
|
|
|
const { head, css, html } = rendered;
|
|
|
|
res.end(get_template().render({
|
|
scripts: `<script src='/client/${chunks.main}'></script>`,
|
|
html,
|
|
head: `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`,
|
|
styles: (css && css.code ? `<style>${css.code}</style>` : '')
|
|
}));
|
|
}
|
|
};
|
|
}
|
|
|
|
function get_not_found_handler(chunks: Record<string, string>, get_routes: () => Route[], get_template: () => Template) {
|
|
return function handle_not_found(req, res) {
|
|
res.statusCode = 404;
|
|
res.setHeader('Content-Type', 'text/html');
|
|
|
|
const route = get_routes().find((route: Route) => route.pattern.test('/4xx')); // TODO separate 4xx and 5xx out
|
|
const rendered = route ? route.module.render({
|
|
status: 404,
|
|
message: 'Not found'
|
|
}) : { head: '', css: '', html: 'Not found' };
|
|
|
|
const { head, css, html } = rendered;
|
|
|
|
res.end(get_template().render({
|
|
scripts: `<script src='/client/${chunks.main}'></script>`,
|
|
html,
|
|
head: `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`,
|
|
styles: (css && css.code ? `<style>${css.code}</style>` : '')
|
|
}));
|
|
};
|
|
}
|
|
|
|
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: string) {
|
|
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
}
|
|
|
|
function try_serialize(data: any) {
|
|
try {
|
|
return serialize(data);
|
|
} catch (err) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function try_read(file: string) {
|
|
try {
|
|
return fs.readFileSync(file, 'utf-8');
|
|
} catch (err) {
|
|
return null;
|
|
}
|
|
} |