mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-15 04:14:46 +00:00
work in progress
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import { create_app, create_assets, create_routes, templates } from 'sapper/core.js';
|
||||
import { create_app, create_serviceworker, create_routes, create_template } from 'sapper/core.js';
|
||||
import { dest } from '../config.js';
|
||||
|
||||
type Deferred = {
|
||||
promise?: Promise<any>;
|
||||
fulfil?: (value: any) => void;
|
||||
reject?: (err: Error) => void;
|
||||
}
|
||||
|
||||
function deferred() {
|
||||
const d = {};
|
||||
const d: Deferred = {};
|
||||
|
||||
d.promise = new Promise((fulfil, reject) => {
|
||||
d.fulfil = fulfil;
|
||||
@@ -15,7 +21,7 @@ function deferred() {
|
||||
return d;
|
||||
}
|
||||
|
||||
export default function create_watcher({ compilers, dev, entry, src, onroutes }) {
|
||||
export default function create_watcher({ compilers, dev, entry, src, onroutes, ontemplate }) {
|
||||
const deferreds = {
|
||||
client: deferred(),
|
||||
server: deferred()
|
||||
@@ -31,27 +37,29 @@ export default function create_watcher({ compilers, dev, entry, src, onroutes })
|
||||
const server_info = server_stats.toJson();
|
||||
fs.writeFileSync(path.join(dest, 'stats.server.json'), JSON.stringify(server_info, null, ' '));
|
||||
|
||||
return create_assets({
|
||||
src, dest, dev,
|
||||
client_info: client_stats.toJson(),
|
||||
server_info: server_stats.toJson()
|
||||
const client_files = client_info.assets.map((chunk: { name: string }) => `/client/${chunk.name}`);
|
||||
|
||||
return create_serviceworker({
|
||||
routes: create_routes({ src }),
|
||||
client_files,
|
||||
src
|
||||
});
|
||||
});
|
||||
|
||||
function watch_compiler(type) {
|
||||
function watch_compiler(type: 'client' | 'server') {
|
||||
const compiler = compilers[type];
|
||||
|
||||
compiler.plugin('invalid', filename => {
|
||||
compiler.plugin('invalid', (filename: string) => {
|
||||
console.log(chalk.cyan(`${type} bundle invalidated, file changed: ${chalk.bold(filename)}`));
|
||||
deferreds[type] = deferred();
|
||||
watcher.ready = invalidate();
|
||||
});
|
||||
|
||||
compiler.plugin('failed', err => {
|
||||
compiler.plugin('failed', (err: Error) => {
|
||||
deferreds[type].reject(err);
|
||||
});
|
||||
|
||||
return compiler.watch({}, (err, stats) => {
|
||||
return compiler.watch({}, (err: Error, stats: any) => {
|
||||
if (stats.hasErrors()) {
|
||||
deferreds[type].reject(stats.toJson().errors[0]);
|
||||
} else {
|
||||
@@ -62,7 +70,7 @@ export default function create_watcher({ compilers, dev, entry, src, onroutes })
|
||||
|
||||
const chokidar = require('chokidar');
|
||||
|
||||
function watch_files(pattern, callback) {
|
||||
function watch_files(pattern: string, callback: () => void) {
|
||||
const watcher = chokidar.watch(pattern, {
|
||||
persistent: false
|
||||
});
|
||||
@@ -76,15 +84,13 @@ export default function create_watcher({ compilers, dev, entry, src, onroutes })
|
||||
const routes = create_routes({ src });
|
||||
onroutes(routes);
|
||||
|
||||
create_app({ dev, entry, src }); // TODO this calls `create_routes` again, we should pass `routes` to `create_app` instead
|
||||
create_app({ routes, src, dev });
|
||||
});
|
||||
|
||||
watch_files('templates/main.js', () => {
|
||||
create_app({ dev, entry, src });
|
||||
});
|
||||
watch_files('app/template.html', () => {
|
||||
const template = create_template();
|
||||
ontemplate(template);
|
||||
|
||||
watch_files('templates/**.html', () => {
|
||||
templates.create_templates();
|
||||
// TODO reload current page?
|
||||
});
|
||||
|
||||
@@ -4,117 +4,61 @@ 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_assets } from 'sapper/core.js';
|
||||
import { create_routes, templates, create_compilers, create_template } from 'sapper/core.js';
|
||||
import { dest, entry, isDev, src } from '../config';
|
||||
import create_watcher from './create_watcher';
|
||||
import { Route, Template } from '../interfaces';
|
||||
|
||||
const dev = isDev();
|
||||
|
||||
function connect_dev() {
|
||||
mkdirp.sync(dest);
|
||||
rimraf.sync(path.join(dest, '**/*'));
|
||||
|
||||
const compilers = create_compilers();
|
||||
|
||||
let routes;
|
||||
|
||||
const watcher = create_watcher({
|
||||
dev, entry, src,
|
||||
compilers,
|
||||
onroutes: _ => {
|
||||
routes = _;
|
||||
}
|
||||
});
|
||||
|
||||
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, () => routes),
|
||||
|
||||
get_not_found_handler(() => asset_cache)
|
||||
]);
|
||||
|
||||
middleware.close = () => {
|
||||
watcher.close();
|
||||
// TODO shut down chokidar
|
||||
};
|
||||
|
||||
return middleware;
|
||||
type Assets = {
|
||||
index: string;
|
||||
service_worker: string;
|
||||
client: Record<string, string>;
|
||||
}
|
||||
|
||||
function connect_prod() {
|
||||
const asset_cache = create_assets({
|
||||
src, dest,
|
||||
dev: false,
|
||||
client_info: read_json(path.join(dest, 'stats.client.json')),
|
||||
server_info: read_json(path.join(dest, 'stats.server.json'))
|
||||
});
|
||||
export default function middleware({ routes }: {
|
||||
routes: Route[]
|
||||
}) {
|
||||
const client_info = JSON.parse(fs.readFileSync(path.join(dest, 'client_info.json'), 'utf-8'));
|
||||
|
||||
const routes = create_routes({ src }); // TODO rename update
|
||||
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 => pathname === '/index.html',
|
||||
filter: (pathname: string) => pathname === '/index.html',
|
||||
type: 'text/html',
|
||||
cache: 'max-age=600',
|
||||
fn: () => asset_cache.client.index
|
||||
fn: () => assets.index
|
||||
}),
|
||||
|
||||
get_asset_handler({
|
||||
filter: pathname => pathname === '/service-worker.js',
|
||||
filter: (pathname: string) => pathname === '/service-worker.js',
|
||||
type: 'application/javascript',
|
||||
cache: 'max-age=600',
|
||||
fn: () => asset_cache.client.service_worker
|
||||
fn: () => assets.service_worker
|
||||
}),
|
||||
|
||||
get_asset_handler({
|
||||
filter: pathname => pathname.startsWith('/client/'),
|
||||
filter: (pathname: string) => pathname.startsWith('/client/'),
|
||||
type: 'application/javascript',
|
||||
cache: 'max-age=31536000',
|
||||
fn: pathname => asset_cache.client.chunks[pathname]
|
||||
fn: (pathname: string) => assets.client[pathname.replace('/client/', '')]
|
||||
}),
|
||||
|
||||
get_route_handler(() => asset_cache, () => routes),
|
||||
get_route_handler(client_info.assetsByChunkName, () => assets, () => routes, () => template),
|
||||
|
||||
get_not_found_handler(() => asset_cache)
|
||||
get_not_found_handler(client_info.assetsByChunkName, () => routes, () => template)
|
||||
]);
|
||||
|
||||
// here for API consistency between dev, and prod, but
|
||||
@@ -124,10 +68,6 @@ function connect_prod() {
|
||||
return middleware;
|
||||
}
|
||||
|
||||
export default function connect({ dev: _dev = dev } = {}) {
|
||||
return _dev ? connect_dev() : connect_prod();
|
||||
}
|
||||
|
||||
function set_req_pathname(req, res, next) {
|
||||
req.pathname = req.url.replace(/\?.*/, '');
|
||||
next();
|
||||
@@ -146,11 +86,11 @@ function get_asset_handler(opts) {
|
||||
|
||||
const resolved = Promise.resolve();
|
||||
|
||||
function get_route_handler(get_assets, get_routes) {
|
||||
function handle_route(route, req, res, next, { client, server }) {
|
||||
req.params = route.exec(req.pathname);
|
||||
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 = require(server.entry)[route.id];
|
||||
const mod = route.module;
|
||||
|
||||
if (route.type === 'page') {
|
||||
// for page routes, we're going to serve some HTML
|
||||
@@ -158,10 +98,12 @@ function get_route_handler(get_assets, get_routes) {
|
||||
|
||||
// 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"`);
|
||||
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);
|
||||
@@ -170,9 +112,9 @@ function get_route_handler(get_assets, get_routes) {
|
||||
return { rendered: mod.render(data), serialized };
|
||||
});
|
||||
|
||||
return templates.stream(res, 200, {
|
||||
return template.stream(res, {
|
||||
scripts: promise.then(({ serialized }) => {
|
||||
const main = `<script src='${client.main_file}'></script>`;
|
||||
const main = `<script src='/client/${chunks.main}'></script>`;
|
||||
|
||||
if (serialized) {
|
||||
return `<script>__SAPPER__ = { preloaded: ${serialized} };</script>${main}`;
|
||||
@@ -187,8 +129,8 @@ function get_route_handler(get_assets, get_routes) {
|
||||
} else {
|
||||
const { html, head, css } = mod.render(data);
|
||||
|
||||
const page = templates.render(200, {
|
||||
scripts: `<script src='${client.main_file}'></script>`,
|
||||
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>` : '')
|
||||
@@ -216,39 +158,55 @@ function get_route_handler(get_assets, get_routes) {
|
||||
return function find_route(req, res, next) {
|
||||
const url = req.pathname;
|
||||
|
||||
resolved
|
||||
.then(() => {
|
||||
const routes = get_routes();
|
||||
for (const route of routes) {
|
||||
if (route.test(url)) return handle_route(route, req, res, next, get_assets());
|
||||
}
|
||||
const routes = get_routes();
|
||||
|
||||
// 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')
|
||||
}));
|
||||
});
|
||||
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(fn) {
|
||||
function get_not_found_handler(chunks: Record<string, string>, get_routes: () => Route[], get_template: () => Template) {
|
||||
return function handle_not_found(req, res) {
|
||||
const asset_cache = fn();
|
||||
|
||||
res.statusCode = 404;
|
||||
res.end(templates.render(404, {
|
||||
title: 'Not found',
|
||||
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,
|
||||
method: req.method,
|
||||
scripts: `<script src='${asset_cache.client.main_file}'></script>`,
|
||||
url: req.url
|
||||
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>` : '')
|
||||
}));
|
||||
};
|
||||
}
|
||||
@@ -273,14 +231,22 @@ function compose_handlers(handlers) {
|
||||
};
|
||||
}
|
||||
|
||||
function read_json(file) {
|
||||
function read_json(file: string) {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
}
|
||||
|
||||
function try_serialize(data) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user