basic sapper dev task, with HMR

This commit is contained in:
Rich Harris
2018-02-17 17:44:57 -05:00
parent d95f52f8e9
commit ab1ca60363
9 changed files with 248 additions and 179 deletions

View File

@@ -1,12 +1,17 @@
import * as fs from 'fs';
import * as path from 'path';
import * as net from 'net';
import * as chalk from 'chalk';
import * as child_process from 'child_process';
import * as http from 'http';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import { wait_for_port } from './utils';
import { create_compilers, create_app, create_routes, create_serviceworker, create_template } from 'sapper/core.js';
type Deferred = {
promise?: Promise<any>;
fulfil?: (value: any) => void;
fulfil?: (value?: any) => void;
reject?: (err: Error) => void;
}
@@ -21,61 +26,61 @@ function deferred() {
return d;
}
function create_hot_update_server(port: number, interval = 10000) {
const clients = new Set();
const server = http.createServer((req, res) => {
if (req.url !== '/hmr') return;
req.socket.setKeepAlive(true);
res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/event-stream;charset=utf-8',
'Cache-Control': 'no-cache, no-transform',
'Connection': 'keep-alive',
// While behind nginx, event stream should not be buffered:
// http://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffering
'X-Accel-Buffering': 'no'
});
res.write('\n');
clients.add(res);
req.on('close', () => {
clients.delete(res);
});
});
server.listen(port);
function send(data: any) {
clients.forEach(client => {
client.write(`data: ${JSON.stringify(data)}\n\n`);
});
}
setInterval(() => {
send(null)
}, interval);
return { send };
}
export default function create_watcher(src: string, dir: string) {
rimraf.sync(dir);
mkdirp.sync(dir);
const chokidar = require('chokidar');
// initial build
const routes = create_routes({ src });
create_app({ routes, src, dev: true });
const hot_update_server = create_hot_update_server(23456); // TODO robustify port selection
// TODO watch the configs themselves?
const compilers = create_compilers();
const deferreds = {
client: deferred(),
server: deferred()
};
const invalidate = () => Promise.all([
deferreds.client.promise,
deferreds.server.promise
]).then(([client_stats, server_stats]) => {
const client_info = client_stats.toJson();
fs.writeFileSync(path.join(dir, 'stats.client.json'), JSON.stringify(client_info, null, ' '));
const server_info = server_stats.toJson();
fs.writeFileSync(path.join(dir, 'stats.server.json'), JSON.stringify(server_info, null, ' '));
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: 'client' | 'server', callback: (err: Error) => void) {
const compiler = compilers[type];
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: Error) => {
deferreds[type].reject(err);
});
return compiler.watch({}, (err: Error, stats: any) => {
if (stats.hasErrors()) {
deferreds[type].reject(stats.toJson().errors[0]);
} else {
deferreds[type].fulfil(stats);
}
});
}
const chokidar = require('chokidar');
function watch_files(pattern: string, callback: () => void) {
const watcher = chokidar.watch(pattern, {
persistent: false
@@ -96,11 +101,114 @@ export default function create_watcher(src: string, dir: string) {
// TODO reload current page?
});
watch_compiler('client', () => {
let proc: child_process.ChildProcess;
const deferreds = {
server: deferred(),
client: deferred()
};
const times = {
client_start: Date.now(),
server_start: Date.now(),
serviceworker_start: Date.now()
};
compilers.server.plugin('invalid', () => {
times.server_start = Date.now();
// TODO print message
deferreds.server = deferred();
});
watch_compiler('server', () => {
compilers.server.watch({}, (err: Error, stats: any) => {
if (err) {
console.error(chalk.red(err.message));
} else if (stats.hasErrors()) {
// print errors. TODO notify client
stats.toJson().errors.forEach((error: Error) => {
console.error(error); // TODO make this look nice
});
} else {
console.log(`built server in ${Date.now() - times.server_start}ms`); // TODO prettify
const server_info = stats.toJson();
fs.writeFileSync(path.join(dir, 'server_info.json'), JSON.stringify(server_info, null, ' '));
deferreds.client.promise.then(() => {
if (proc) proc.kill();
proc = child_process.fork(`${dir}/server.js`, [], {
cwd: process.cwd(),
env: Object.assign({}, process.env)
});
wait_for_port(3000, deferreds.server.fulfil); // TODO control port
});
}
});
compilers.client.plugin('invalid', (filename: string) => {
times.client_start = Date.now();
deferreds.client = deferred();
// TODO print message
fs.readdirSync(path.join(dir, 'client')).forEach(file => {
fs.unlinkSync(path.join(dir, 'client', file));
});
});
compilers.client.watch({}, (err: Error, stats: any) => {
if (err) {
console.error(chalk.red(err.message));
} else if (stats.hasErrors()) {
// print errors. TODO notify client
stats.toJson().errors.forEach((error: Error) => {
console.error(error); // TODO make this look nice
});
} else {
console.log(`built client in ${Date.now() - times.client_start}ms`); // TODO prettify
const client_info = stats.toJson();
fs.writeFileSync(path.join(dir, 'client_info.json'), JSON.stringify(client_info, null, ' '));
deferreds.client.fulfil();
const client_files = client_info.assets.map((chunk: { name: string }) => `/client/${chunk.name}`);
deferreds.server.promise.then(() => {
hot_update_server.send({
status: 'completed'
});
});
return create_serviceworker({
routes: create_routes({ src }),
client_files,
src
});
}
});
if (compilers.serviceworker) {
compilers.serviceworker.plugin('invalid', (filename: string) => {
times.serviceworker_start = Date.now();
});
compilers.client.watch({}, (err: Error, stats: any) => {
if (err) {
// TODO notify client
} else if (stats.hasErrors()) {
// print errors. TODO notify client
stats.toJson().errors.forEach((error: Error) => {
console.error(error); // TODO make this look nice
});
} else {
console.log(`built service worker in ${Date.now() - times.serviceworker_start}ms`); // TODO prettify
const serviceworker_info = stats.toJson();
fs.writeFileSync(path.join(dir, 'serviceworker_info.json'), JSON.stringify(serviceworker_info, null, ' '));
// TODO trigger reload?
}
});
}
}