mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-21 23:05:02 +00:00
add dev API
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,6 +8,7 @@ test/app/app/manifest
|
|||||||
test/app/export
|
test/app/export
|
||||||
test/app/build
|
test/app/build
|
||||||
sapper
|
sapper
|
||||||
|
api.js
|
||||||
runtime.js
|
runtime.js
|
||||||
dist
|
dist
|
||||||
!rollup.config.js
|
!rollup.config.js
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"*.js",
|
"*.js",
|
||||||
"*.ts.js",
|
"*.ts.js",
|
||||||
|
"api",
|
||||||
"runtime",
|
"runtime",
|
||||||
"webpack",
|
"webpack",
|
||||||
"sapper",
|
"sapper",
|
||||||
|
|||||||
@@ -25,7 +25,13 @@ export default [
|
|||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
input: [`src/cli.ts`, `src/core.ts`, `src/middleware.ts`, `src/webpack.ts`],
|
input: [
|
||||||
|
`src/api.ts`,
|
||||||
|
`src/cli.ts`,
|
||||||
|
`src/core.ts`,
|
||||||
|
`src/middleware.ts`,
|
||||||
|
`src/webpack.ts`
|
||||||
|
],
|
||||||
output: {
|
output: {
|
||||||
dir: 'dist',
|
dir: 'dist',
|
||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
|
|||||||
5
src/api.ts
Normal file
5
src/api.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { dev } from './api/dev';
|
||||||
|
import { build } from './api/build';
|
||||||
|
import { exporter } from './api/export';
|
||||||
|
|
||||||
|
export { dev, build, exporter };
|
||||||
@@ -6,16 +6,17 @@ import { EventEmitter } from 'events';
|
|||||||
import { minify_html } from './utils/minify_html';
|
import { minify_html } from './utils/minify_html';
|
||||||
import { create_compilers, create_main_manifests, create_routes, create_serviceworker_manifest } from '../core'
|
import { create_compilers, create_main_manifests, create_routes, create_serviceworker_manifest } from '../core'
|
||||||
import { locations } from '../config';
|
import { locations } from '../config';
|
||||||
|
import * as events from './interfaces';
|
||||||
|
|
||||||
export default function build(opts: {}) {
|
export function build(opts: {}) {
|
||||||
const emitter = new EventEmitter();
|
const emitter = new EventEmitter();
|
||||||
|
|
||||||
execute(emitter, opts).then(
|
execute(emitter, opts).then(
|
||||||
() => {
|
() => {
|
||||||
emitter.emit('done', {}); // TODO do we need to pass back any info?
|
emitter.emit('done', <events.DoneEvent>{}); // TODO do we need to pass back any info?
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
emitter.emit('error', {
|
emitter.emit('error', <events.ErrorEvent>{
|
||||||
error
|
error
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -54,7 +55,7 @@ async function execute(emitter: EventEmitter, {
|
|||||||
const { client, server, serviceworker } = create_compilers({ webpack });
|
const { client, server, serviceworker } = create_compilers({ webpack });
|
||||||
|
|
||||||
const client_stats = await compile(client);
|
const client_stats = await compile(client);
|
||||||
emitter.emit('build', {
|
emitter.emit('build', <events.BuildEvent>{
|
||||||
type: 'client',
|
type: 'client',
|
||||||
// TODO duration/warnings
|
// TODO duration/warnings
|
||||||
webpack_stats: client_stats
|
webpack_stats: client_stats
|
||||||
@@ -65,7 +66,7 @@ async function execute(emitter: EventEmitter, {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const server_stats = await compile(server);
|
const server_stats = await compile(server);
|
||||||
emitter.emit('build', {
|
emitter.emit('build', <events.BuildEvent>{
|
||||||
type: 'server',
|
type: 'server',
|
||||||
// TODO duration/warnings
|
// TODO duration/warnings
|
||||||
webpack_stats: server_stats
|
webpack_stats: server_stats
|
||||||
@@ -81,7 +82,7 @@ async function execute(emitter: EventEmitter, {
|
|||||||
|
|
||||||
serviceworker_stats = await compile(serviceworker);
|
serviceworker_stats = await compile(serviceworker);
|
||||||
|
|
||||||
emitter.emit('build', {
|
emitter.emit('build', <events.BuildEvent>{
|
||||||
type: 'serviceworker',
|
type: 'serviceworker',
|
||||||
// TODO duration/warnings
|
// TODO duration/warnings
|
||||||
webpack_stats: serviceworker_stats
|
webpack_stats: serviceworker_stats
|
||||||
|
|||||||
394
src/api/dev.ts
394
src/api/dev.ts
@@ -0,0 +1,394 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as http from 'http';
|
||||||
|
import * as child_process from 'child_process';
|
||||||
|
import * as ports from 'port-authority';
|
||||||
|
import mkdirp from 'mkdirp';
|
||||||
|
import rimraf from 'rimraf';
|
||||||
|
import format_messages from 'webpack-format-messages';
|
||||||
|
import prettyMs from 'pretty-ms';
|
||||||
|
import { locations } from '../config';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { create_routes, create_main_manifests, create_compilers, create_serviceworker_manifest } from '../core';
|
||||||
|
import * as events from '../interfaces';
|
||||||
|
|
||||||
|
export function dev(opts) {
|
||||||
|
return new Watcher(opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Watcher extends EventEmitter {
|
||||||
|
dirs: {
|
||||||
|
app: string;
|
||||||
|
dest: string;
|
||||||
|
routes: string;
|
||||||
|
webpack: string;
|
||||||
|
}
|
||||||
|
port: number;
|
||||||
|
closed: boolean;
|
||||||
|
|
||||||
|
dev_server: DevServer;
|
||||||
|
proc: child_process.ChildProcess;
|
||||||
|
filewatchers: Array<{ close: () => void }>;
|
||||||
|
deferreds: {
|
||||||
|
client: Deferred;
|
||||||
|
server: Deferred;
|
||||||
|
};
|
||||||
|
|
||||||
|
restarting: boolean;
|
||||||
|
current_build: {
|
||||||
|
changed: Set<string>;
|
||||||
|
rebuilding: Set<string>;
|
||||||
|
unique_warnings: Set<string>;
|
||||||
|
unique_errors: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
app = locations.app(),
|
||||||
|
dest = locations.dest(),
|
||||||
|
routes = locations.routes(),
|
||||||
|
webpack = 'webpack',
|
||||||
|
port = +process.env.PORT
|
||||||
|
}: {
|
||||||
|
app: string,
|
||||||
|
dest: string,
|
||||||
|
routes: string,
|
||||||
|
webpack: string,
|
||||||
|
port: number
|
||||||
|
}) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.dirs = { app, dest, routes, webpack };
|
||||||
|
this.port = port;
|
||||||
|
this.closed = false;
|
||||||
|
|
||||||
|
this.filewatchers = [];
|
||||||
|
|
||||||
|
this.current_build = {
|
||||||
|
changed: new Set(),
|
||||||
|
rebuilding: new Set(),
|
||||||
|
unique_errors: new Set(),
|
||||||
|
unique_warnings: new Set()
|
||||||
|
};
|
||||||
|
|
||||||
|
// remove this in a future version
|
||||||
|
const template = fs.readFileSync(path.join(app, 'template.html'), 'utf-8');
|
||||||
|
if (template.indexOf('%sapper.base%') === -1) {
|
||||||
|
const error = new Error(`As of Sapper v0.10, your template.html file must include %sapper.base% in the <head>`);
|
||||||
|
error.code = `missing-sapper-base`;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.env.NODE_ENV = 'development';
|
||||||
|
|
||||||
|
process.on('exit', () => {
|
||||||
|
this.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if (this.port) {
|
||||||
|
if (!await ports.check(this.port)) {
|
||||||
|
this.emit('fatal', <events.FatalEvent>{
|
||||||
|
error: new Error(`Port ${this.port} is unavailable`)
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.port = await ports.find(3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { dest } = this.dirs;
|
||||||
|
rimraf.sync(dest);
|
||||||
|
mkdirp.sync(dest);
|
||||||
|
|
||||||
|
const dev_port = await ports.find(10000);
|
||||||
|
|
||||||
|
const routes = create_routes();
|
||||||
|
create_main_manifests({ routes, dev_port });
|
||||||
|
|
||||||
|
this.dev_server = new DevServer(dev_port);
|
||||||
|
|
||||||
|
this.filewatchers.push(
|
||||||
|
watch_files(locations.routes(), ['add', 'unlink'], () => {
|
||||||
|
const routes = create_routes();
|
||||||
|
create_main_manifests({ routes, dev_port });
|
||||||
|
}),
|
||||||
|
|
||||||
|
watch_files(`${locations.app()}/template.html`, ['change'], () => {
|
||||||
|
this.dev_server.send({
|
||||||
|
action: 'reload'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.deferreds = {
|
||||||
|
server: new Deferred(),
|
||||||
|
client: new Deferred()
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO watch the configs themselves?
|
||||||
|
const compilers = create_compilers({ webpack: this.dirs.webpack });
|
||||||
|
|
||||||
|
this.watch(compilers.server, {
|
||||||
|
name: 'server',
|
||||||
|
|
||||||
|
invalid: filename => {
|
||||||
|
this.restart(filename, 'server');
|
||||||
|
this.deferreds.server = new Deferred();
|
||||||
|
},
|
||||||
|
|
||||||
|
result: info => {
|
||||||
|
fs.writeFileSync(path.join(dest, 'server_info.json'), JSON.stringify(info, null, ' '));
|
||||||
|
|
||||||
|
this.deferreds.client.promise.then(() => {
|
||||||
|
const restart = () => {
|
||||||
|
ports.wait(this.port).then(this.deferreds.server.fulfil);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.proc) {
|
||||||
|
this.proc.kill();
|
||||||
|
this.proc.on('exit', restart);
|
||||||
|
} else {
|
||||||
|
restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.proc = child_process.fork(`${dest}/server.js`, [], {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
env: Object.assign({
|
||||||
|
PORT: this.port
|
||||||
|
}, process.env)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let first = true;
|
||||||
|
|
||||||
|
this.watch(compilers.client, {
|
||||||
|
name: 'client',
|
||||||
|
|
||||||
|
invalid: filename => {
|
||||||
|
this.restart(filename, 'client');
|
||||||
|
this.deferreds.client = new Deferred();
|
||||||
|
|
||||||
|
// TODO we should delete old assets. due to a webpack bug
|
||||||
|
// i don't even begin to comprehend, this is apparently
|
||||||
|
// quite difficult
|
||||||
|
},
|
||||||
|
|
||||||
|
result: info => {
|
||||||
|
fs.writeFileSync(path.join(dest, 'client_info.json'), JSON.stringify({
|
||||||
|
assets: info.assetsByChunkName
|
||||||
|
}, null, ' '));
|
||||||
|
this.deferreds.client.fulfil();
|
||||||
|
|
||||||
|
const client_files = info.assets.map((chunk: { name: string }) => `client/${chunk.name}`);
|
||||||
|
|
||||||
|
this.deferreds.server.promise.then(() => {
|
||||||
|
this.dev_server.send({
|
||||||
|
status: 'completed'
|
||||||
|
});
|
||||||
|
|
||||||
|
this.emit('ready', <events.ReadyEvent>{
|
||||||
|
port: this.port
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
create_serviceworker_manifest({
|
||||||
|
routes: create_routes(),
|
||||||
|
client_files
|
||||||
|
});
|
||||||
|
|
||||||
|
watch_serviceworker();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let watch_serviceworker = compilers.serviceworker
|
||||||
|
? () => {
|
||||||
|
watch_serviceworker = noop;
|
||||||
|
|
||||||
|
this.watch(compilers.serviceworker, {
|
||||||
|
name: 'service worker',
|
||||||
|
|
||||||
|
result: info => {
|
||||||
|
fs.writeFileSync(path.join(dest, 'serviceworker_info.json'), JSON.stringify(info, null, ' '));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: noop;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (this.closed) return;
|
||||||
|
this.closed = true;
|
||||||
|
|
||||||
|
this.dev_server.close();
|
||||||
|
|
||||||
|
if (this.proc) this.proc.kill();
|
||||||
|
this.filewatchers.forEach(watcher => {
|
||||||
|
watcher.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
restart(filename: string, type: string) {
|
||||||
|
if (this.restarting) {
|
||||||
|
this.current_build.changed.add(filename);
|
||||||
|
this.current_build.rebuilding.add(type);
|
||||||
|
} else {
|
||||||
|
this.restarting = true;
|
||||||
|
|
||||||
|
this.current_build = {
|
||||||
|
changed: new Set(),
|
||||||
|
rebuilding: new Set(),
|
||||||
|
unique_warnings: new Set(),
|
||||||
|
unique_errors: new Set()
|
||||||
|
};
|
||||||
|
|
||||||
|
process.nextTick(() => {
|
||||||
|
this.emit('invalid', <events.InvalidEvent>{
|
||||||
|
changed: Array.from(this.current_build.changed),
|
||||||
|
invalid: {
|
||||||
|
server: this.current_build.rebuilding.has('server'),
|
||||||
|
client: this.current_build.rebuilding.has('client'),
|
||||||
|
serviceworker: this.current_build.rebuilding.has('serviceworker'),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.restarting = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(compiler: any, { name, invalid = noop, result }: {
|
||||||
|
name: string,
|
||||||
|
invalid?: (filename: string) => void;
|
||||||
|
result: (stats: any) => void;
|
||||||
|
}) {
|
||||||
|
compiler.hooks.invalid.tap('sapper', (filename: string) => {
|
||||||
|
invalid(filename);
|
||||||
|
});
|
||||||
|
|
||||||
|
compiler.watch({}, (err: Error, stats: any) => {
|
||||||
|
if (err) {
|
||||||
|
this.emit('error', <events.ErrorEvent>{
|
||||||
|
type: name,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const messages = format_messages(stats);
|
||||||
|
const info = stats.toJson();
|
||||||
|
|
||||||
|
this.emit('build', {
|
||||||
|
type: name,
|
||||||
|
|
||||||
|
duration: info.time,
|
||||||
|
|
||||||
|
errors: messages.errors.map((message: string) => {
|
||||||
|
const duplicate = this.current_build.unique_errors.has(message);
|
||||||
|
this.current_build.unique_errors.add(message);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message,
|
||||||
|
duplicate
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
warnings: messages.warnings.map((message: string) => {
|
||||||
|
const duplicate = this.current_build.unique_warnings.has(message);
|
||||||
|
this.current_build.unique_warnings.add(message);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message,
|
||||||
|
duplicate
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
result(info);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Deferred {
|
||||||
|
promise: Promise<any>;
|
||||||
|
fulfil: (value?: any) => void;
|
||||||
|
reject: (error: Error) => void;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.promise = new Promise((fulfil, reject) => {
|
||||||
|
this.fulfil = fulfil;
|
||||||
|
this.reject = reject;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DevServer {
|
||||||
|
clients: Set<http.ServerResponse>;
|
||||||
|
_: http.Server;
|
||||||
|
|
||||||
|
constructor(port: number, interval = 10000) {
|
||||||
|
this.clients = new Set();
|
||||||
|
|
||||||
|
this._ = http.createServer((req, res) => {
|
||||||
|
if (req.url !== '/__sapper__') return;
|
||||||
|
|
||||||
|
req.socket.setKeepAlive(true);
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Headers': 'Cache-Control',
|
||||||
|
'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');
|
||||||
|
|
||||||
|
this.clients.add(res);
|
||||||
|
req.on('close', () => {
|
||||||
|
this.clients.delete(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this._.listen(port);
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
this.send(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this._.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
send(data: any) {
|
||||||
|
this.clients.forEach(client => {
|
||||||
|
client.write(`data: ${JSON.stringify(data)}\n\n`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function noop() {}
|
||||||
|
|
||||||
|
function watch_files(pattern: string, events: string[], callback: () => void) {
|
||||||
|
const chokidar = require('chokidar');
|
||||||
|
|
||||||
|
const watcher = chokidar.watch(pattern, {
|
||||||
|
persistent: true,
|
||||||
|
ignoreInitial: true,
|
||||||
|
disableGlobbing: true
|
||||||
|
});
|
||||||
|
|
||||||
|
events.forEach(event => {
|
||||||
|
watcher.on(event, callback);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
close: () => watcher.close()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,16 +8,17 @@ import * as ports from 'port-authority';
|
|||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { minify_html } from './utils/minify_html';
|
import { minify_html } from './utils/minify_html';
|
||||||
import { locations } from '../config';
|
import { locations } from '../config';
|
||||||
|
import * as events from './interfaces';
|
||||||
|
|
||||||
export default function exporter(opts: {}) {
|
export function exporter(opts: {}) {
|
||||||
const emitter = new EventEmitter();
|
const emitter = new EventEmitter();
|
||||||
|
|
||||||
execute(emitter, opts).then(
|
execute(emitter, opts).then(
|
||||||
() => {
|
() => {
|
||||||
emitter.emit('done', {}); // TODO do we need to pass back any info?
|
emitter.emit('done', <events.DoneEvent>{}); // TODO do we need to pass back any info?
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
emitter.emit('error', {
|
emitter.emit('error', <events.ErrorEvent>{
|
||||||
error
|
error
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -80,7 +81,7 @@ async function execute(emitter: EventEmitter, {
|
|||||||
body = minify_html(body);
|
body = minify_html(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
emitter.emit('file', {
|
emitter.emit('file', <events.FileEvent>{
|
||||||
file,
|
file,
|
||||||
size: body.length
|
size: body.length
|
||||||
});
|
});
|
||||||
@@ -93,7 +94,7 @@ async function execute(emitter: EventEmitter, {
|
|||||||
const range = ~~(r.status / 100);
|
const range = ~~(r.status / 100);
|
||||||
|
|
||||||
if (range >= 4) {
|
if (range >= 4) {
|
||||||
emitter.emit('failure', {
|
emitter.emit('failure', <events.FailureEvent>{
|
||||||
status: r.status,
|
status: r.status,
|
||||||
pathname: url.pathname
|
pathname: url.pathname
|
||||||
});
|
});
|
||||||
|
|||||||
40
src/api/interfaces.ts
Normal file
40
src/api/interfaces.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
export type ReadyEvent = {
|
||||||
|
port: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ErrorEvent = {
|
||||||
|
type: string;
|
||||||
|
error: Error;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FatalEvent = {
|
||||||
|
error: Error;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InvalidEvent = {
|
||||||
|
changed: string[];
|
||||||
|
invalid: {
|
||||||
|
client: boolean;
|
||||||
|
server: boolean;
|
||||||
|
serviceworker: boolean;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BuildEvent = {
|
||||||
|
type: string;
|
||||||
|
errors: Array<{ message: string, duplicate: boolean }>;
|
||||||
|
warnings: Array<{ message: string, duplicate: boolean }>;
|
||||||
|
duration: number;
|
||||||
|
webpack_stats: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FileEvent = {
|
||||||
|
file: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FailureEvent = {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DoneEvent = {}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import _build from '../api/build';
|
import { build as _build } from '../api/build';
|
||||||
import * as colors from 'ansi-colors';
|
import * as colors from 'ansi-colors';
|
||||||
import { locations } from '../config';
|
import { locations } from '../config';
|
||||||
|
|
||||||
|
|||||||
370
src/cli/dev.ts
370
src/cli/dev.ts
@@ -1,323 +1,67 @@
|
|||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as net from 'net';
|
|
||||||
import * as colors from 'ansi-colors';
|
import * as colors from 'ansi-colors';
|
||||||
import * as child_process from 'child_process';
|
import * as child_process from 'child_process';
|
||||||
import * as http from 'http';
|
|
||||||
import mkdirp from 'mkdirp';
|
|
||||||
import rimraf from 'rimraf';
|
|
||||||
import format_messages from 'webpack-format-messages';
|
|
||||||
import prettyMs from 'pretty-ms';
|
import prettyMs from 'pretty-ms';
|
||||||
import * as ports from 'port-authority';
|
import { dev as _dev } from '../api/dev';
|
||||||
import { locations } from '../config';
|
import * as events from '../api/interfaces';
|
||||||
import { create_compilers, create_main_manifests, create_routes, create_serviceworker_manifest } from '../core';
|
|
||||||
|
|
||||||
type Deferred = {
|
export function dev(opts: { port: number, open: boolean }) {
|
||||||
promise?: Promise<any>;
|
try {
|
||||||
fulfil?: (value?: any) => void;
|
const watcher = _dev(opts);
|
||||||
reject?: (err: Error) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function deferred() {
|
let first = true;
|
||||||
const d: Deferred = {};
|
|
||||||
|
|
||||||
d.promise = new Promise((fulfil, reject) => {
|
watcher.on('ready', (event: events.ReadyEvent) => {
|
||||||
d.fulfil = fulfil;
|
if (first) {
|
||||||
d.reject = reject;
|
console.log(`${colors.bold.cyan(`> Listening on http://localhost:${event.port}`)}`);
|
||||||
});
|
if (opts.open) child_process.exec(`open http://localhost:${event.port}`);
|
||||||
|
first = false;
|
||||||
return d;
|
|
||||||
}
|
|
||||||
|
|
||||||
function create_hot_update_server(port: number, interval = 10000) {
|
|
||||||
const clients = new Set();
|
|
||||||
|
|
||||||
const server = http.createServer((req, res) => {
|
|
||||||
if (req.url !== '/__sapper__') return;
|
|
||||||
|
|
||||||
req.socket.setKeepAlive(true);
|
|
||||||
res.writeHead(200, {
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
'Access-Control-Allow-Headers': 'Cache-Control',
|
|
||||||
'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 async function dev(opts: { port: number, open: boolean }) {
|
|
||||||
// remove this in a future version
|
|
||||||
const template = fs.readFileSync(path.join(locations.app(), 'template.html'), 'utf-8');
|
|
||||||
if (template.indexOf('%sapper.base%') === -1) {
|
|
||||||
console.log(`${colors.bold.red(`> As of Sapper v0.10, your template.html file must include %sapper.base% in the <head>`)}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
process.env.NODE_ENV = 'development';
|
|
||||||
|
|
||||||
let port = opts.port || +process.env.PORT;
|
|
||||||
|
|
||||||
if (port) {
|
|
||||||
if (!await ports.check(port)) {
|
|
||||||
console.log(`${colors.bold.red(`> Port ${port} is unavailable`)}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
port = await ports.find(3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dir = locations.dest();
|
|
||||||
rimraf.sync(dir);
|
|
||||||
mkdirp.sync(dir);
|
|
||||||
|
|
||||||
const dev_port = await ports.find(10000);
|
|
||||||
|
|
||||||
const routes = create_routes();
|
|
||||||
create_main_manifests({ routes, dev_port });
|
|
||||||
|
|
||||||
const hot_update_server = create_hot_update_server(dev_port);
|
|
||||||
|
|
||||||
watch_files(locations.routes(), ['add', 'unlink'], () => {
|
|
||||||
const routes = create_routes();
|
|
||||||
create_main_manifests({ routes, dev_port });
|
|
||||||
});
|
|
||||||
|
|
||||||
watch_files(`${locations.app()}/template.html`, ['change'], () => {
|
|
||||||
hot_update_server.send({
|
|
||||||
action: 'reload'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let proc: child_process.ChildProcess;
|
|
||||||
|
|
||||||
process.on('exit', () => {
|
|
||||||
// sometimes webpack crashes, so we need to kill our children
|
|
||||||
if (proc) proc.kill();
|
|
||||||
});
|
|
||||||
|
|
||||||
const deferreds = {
|
|
||||||
server: deferred(),
|
|
||||||
client: deferred()
|
|
||||||
};
|
|
||||||
|
|
||||||
let restarting = false;
|
|
||||||
let build = {
|
|
||||||
unique_warnings: new Set(),
|
|
||||||
unique_errors: new Set()
|
|
||||||
};
|
|
||||||
|
|
||||||
function restart_build(filename: string) {
|
|
||||||
if (restarting) return;
|
|
||||||
|
|
||||||
restarting = true;
|
|
||||||
build = {
|
|
||||||
unique_warnings: new Set(),
|
|
||||||
unique_errors: new Set()
|
|
||||||
};
|
|
||||||
|
|
||||||
process.nextTick(() => {
|
|
||||||
restarting = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`\n${colors.bold.cyan(path.relative(process.cwd(), filename))} changed. rebuilding...`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO watch the configs themselves?
|
|
||||||
const compilers = create_compilers();
|
|
||||||
|
|
||||||
function watch(compiler: any, { name, invalid = noop, error = noop, result }: {
|
|
||||||
name: string,
|
|
||||||
invalid?: (filename: string) => void;
|
|
||||||
error?: (error: Error) => void;
|
|
||||||
result: (stats: any) => void;
|
|
||||||
}) {
|
|
||||||
compiler.hooks.invalid.tap('sapper', (filename: string) => {
|
|
||||||
invalid(filename);
|
|
||||||
});
|
|
||||||
|
|
||||||
compiler.watch({}, (err: Error, stats: any) => {
|
|
||||||
if (err) {
|
|
||||||
console.log(`${colors.red(`✗ ${name}`)}`);
|
|
||||||
console.log(`${colors.red(err.message)}`);
|
|
||||||
error(err);
|
|
||||||
} else {
|
|
||||||
const messages = format_messages(stats);
|
|
||||||
const info = stats.toJson();
|
|
||||||
|
|
||||||
if (messages.errors.length > 0) {
|
|
||||||
console.log(`${colors.bold.red(`✗ ${name}`)}`);
|
|
||||||
|
|
||||||
const filtered = messages.errors.filter((message: string) => {
|
|
||||||
return !build.unique_errors.has(message);
|
|
||||||
});
|
|
||||||
|
|
||||||
filtered.forEach((message: string) => {
|
|
||||||
build.unique_errors.add(message);
|
|
||||||
console.log(message);
|
|
||||||
});
|
|
||||||
|
|
||||||
const hidden = messages.errors.length - filtered.length;
|
|
||||||
if (hidden > 0) {
|
|
||||||
console.log(`${hidden} duplicate ${hidden === 1 ? 'error' : 'errors'} hidden\n`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (messages.warnings.length > 0) {
|
|
||||||
console.log(`${colors.bold.yellow(`• ${name}`)}`);
|
|
||||||
|
|
||||||
const filtered = messages.warnings.filter((message: string) => {
|
|
||||||
return !build.unique_warnings.has(message);
|
|
||||||
});
|
|
||||||
|
|
||||||
filtered.forEach((message: string) => {
|
|
||||||
build.unique_warnings.add(message);
|
|
||||||
console.log(`${message}\n`);
|
|
||||||
});
|
|
||||||
|
|
||||||
const hidden = messages.warnings.length - filtered.length;
|
|
||||||
if (hidden > 0) {
|
|
||||||
console.log(`${hidden} duplicate ${hidden === 1 ? 'warning' : 'warnings'} hidden\n`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`${colors.bold.green(`✔ ${name}`)} ${colors.gray(`(${prettyMs(info.time)})`)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
result(info);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watcher.on('invalid', (event: events.InvalidEvent) => {
|
||||||
|
const changed = event.changed.map(filename => path.relative(process.cwd(), filename)).join(', ');
|
||||||
|
console.log(`\n${colors.bold.cyan(changed)} changed. rebuilding...`);
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.on('error', (event: events.ErrorEvent) => {
|
||||||
|
console.log(`${colors.red(`✗ ${event.type}`)}`);
|
||||||
|
console.log(`${colors.red(event.error.message)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.on('fatal', (event: events.FatalEvent) => {
|
||||||
|
console.log(`${colors.bold.red(`> ${event.error.message}`)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.on('build', (event: events.BuildEvent) => {
|
||||||
|
if (event.errors.length) {
|
||||||
|
console.log(`${colors.bold.red(`✗ ${event.type}`)}`);
|
||||||
|
|
||||||
|
event.errors.filter(e => !e.duplicate).forEach(error => {
|
||||||
|
console.log(error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
const hidden = event.errors.filter(e => e.duplicate).length;
|
||||||
|
if (hidden > 0) {
|
||||||
|
console.log(`${hidden} duplicate ${hidden === 1 ? 'error' : 'errors'} hidden\n`);
|
||||||
|
}
|
||||||
|
} else if (event.warnings.length) {
|
||||||
|
console.log(`${colors.bold.yellow(`• ${event.type}`)}`);
|
||||||
|
|
||||||
|
event.warnings.filter(e => !e.duplicate).forEach(warning => {
|
||||||
|
console.log(warning.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
const hidden = event.warnings.filter(e => e.duplicate).length;
|
||||||
|
if (hidden > 0) {
|
||||||
|
console.log(`${hidden} duplicate ${hidden === 1 ? 'warning' : 'warnings'} hidden\n`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`${colors.bold.green(`✔ ${event.type}`)} ${colors.gray(`(${prettyMs(event.duration)})`)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`${colors.bold.red(`> ${err.message}`)}`);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
watch(compilers.server, {
|
|
||||||
name: 'server',
|
|
||||||
|
|
||||||
invalid: filename => {
|
|
||||||
restart_build(filename);
|
|
||||||
// TODO print message
|
|
||||||
deferreds.server = deferred();
|
|
||||||
},
|
|
||||||
|
|
||||||
result: info => {
|
|
||||||
// TODO log compile errors/warnings
|
|
||||||
|
|
||||||
fs.writeFileSync(path.join(dir, 'server_info.json'), JSON.stringify(info, null, ' '));
|
|
||||||
|
|
||||||
deferreds.client.promise.then(() => {
|
|
||||||
function restart() {
|
|
||||||
ports.wait(port).then(deferreds.server.fulfil);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (proc) {
|
|
||||||
proc.kill();
|
|
||||||
proc.on('exit', restart);
|
|
||||||
} else {
|
|
||||||
restart();
|
|
||||||
}
|
|
||||||
|
|
||||||
proc = child_process.fork(`${dir}/server.js`, [], {
|
|
||||||
cwd: process.cwd(),
|
|
||||||
env: Object.assign({
|
|
||||||
PORT: port
|
|
||||||
}, process.env)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let first = true;
|
|
||||||
|
|
||||||
watch(compilers.client, {
|
|
||||||
name: 'client',
|
|
||||||
|
|
||||||
invalid: filename => {
|
|
||||||
restart_build(filename);
|
|
||||||
deferreds.client = deferred();
|
|
||||||
|
|
||||||
// TODO we should delete old assets. due to a webpack bug
|
|
||||||
// i don't even begin to comprehend, this is apparently
|
|
||||||
// quite difficult
|
|
||||||
},
|
|
||||||
|
|
||||||
result: info => {
|
|
||||||
fs.writeFileSync(path.join(dir, 'client_info.json'), JSON.stringify({
|
|
||||||
assets: info.assetsByChunkName
|
|
||||||
}, null, ' '));
|
|
||||||
deferreds.client.fulfil();
|
|
||||||
|
|
||||||
const client_files = info.assets.map((chunk: { name: string }) => `client/${chunk.name}`);
|
|
||||||
|
|
||||||
deferreds.server.promise.then(() => {
|
|
||||||
hot_update_server.send({
|
|
||||||
status: 'completed'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (first) {
|
|
||||||
first = false;
|
|
||||||
console.log(`${colors.bold.cyan(`> Listening on http://localhost:${port}`)}`);
|
|
||||||
if (opts.open) child_process.exec(`open http://localhost:${port}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
create_serviceworker_manifest({
|
|
||||||
routes: create_routes(),
|
|
||||||
client_files
|
|
||||||
});
|
|
||||||
|
|
||||||
watch_serviceworker();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let watch_serviceworker = compilers.serviceworker
|
|
||||||
? function() {
|
|
||||||
watch_serviceworker = noop;
|
|
||||||
|
|
||||||
watch(compilers.serviceworker, {
|
|
||||||
name: 'service worker',
|
|
||||||
|
|
||||||
result: info => {
|
|
||||||
fs.writeFileSync(path.join(dir, 'serviceworker_info.json'), JSON.stringify(info, null, ' '));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
: noop;
|
|
||||||
}
|
|
||||||
|
|
||||||
function noop() {}
|
|
||||||
|
|
||||||
function watch_files(pattern: string, events: string[], callback: () => void) {
|
|
||||||
const chokidar = require('chokidar');
|
|
||||||
|
|
||||||
const watcher = chokidar.watch(pattern, {
|
|
||||||
persistent: true,
|
|
||||||
ignoreInitial: true,
|
|
||||||
disableGlobbing: true
|
|
||||||
});
|
|
||||||
|
|
||||||
events.forEach(event => {
|
|
||||||
watcher.on(event, callback);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import _exporter from '../api/export';
|
import { exporter as _exporter } from '../api/export';
|
||||||
import * as colors from 'ansi-colors';
|
import * as colors from 'ansi-colors';
|
||||||
import prettyBytes from 'pretty-bytes';
|
import prettyBytes from 'pretty-bytes';
|
||||||
import { locations } from '../config';
|
import { locations } from '../config';
|
||||||
|
|||||||
Reference in New Issue
Block a user