Replace server with own implementation, along with a new server-side rendering API
This commit is contained in:
parent
27d0916da8
commit
2947e4f008
|
@ -22,9 +22,6 @@
|
||||||
<FileRef
|
<FileRef
|
||||||
location = "container:.gitignore">
|
location = "container:.gitignore">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
<FileRef
|
|
||||||
location = "container:editor-dev.htm">
|
|
||||||
</FileRef>
|
|
||||||
<FileRef
|
<FileRef
|
||||||
location = "container:index.html">
|
location = "container:index.html">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {VirtualSequenceDiagram} = require('../lib/sequence-diagram');
|
||||||
|
const {HttpError, Server} = require('./server/Server');
|
||||||
|
|
||||||
|
const DEV = process.argv.includes('dev');
|
||||||
|
const HOSTNAME = '127.0.0.1';
|
||||||
|
const PORT = 8080;
|
||||||
|
|
||||||
|
function beginTimer() {
|
||||||
|
return process.hrtime();
|
||||||
|
}
|
||||||
|
|
||||||
|
function endTimer(timer) {
|
||||||
|
const delay = process.hrtime(timer);
|
||||||
|
return delay[0] * 1e9 + delay[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const NUM_MATCH = '[0-9]+(?:\\.[0-9]+)?';
|
||||||
|
const MATCH_RENDER = new RegExp(
|
||||||
|
'^/render/' +
|
||||||
|
`(?:(?:w(${NUM_MATCH}))?(?:h(${NUM_MATCH}))?/|z(${NUM_MATCH})/)?` +
|
||||||
|
'(?:(uri|b64)/)?' +
|
||||||
|
'(.*?)' +
|
||||||
|
'(?:\\.(svg))?$',
|
||||||
|
'i'
|
||||||
|
);
|
||||||
|
|
||||||
|
function getNumeric(v, name) {
|
||||||
|
if(!v) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const n = Number.parseFloat(v);
|
||||||
|
if(Number.isNaN(n)) {
|
||||||
|
throw new HttpError(400, 'Invalid value for ' + name);
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readEncoded(str, encoding) {
|
||||||
|
switch(encoding) {
|
||||||
|
case 'b64':
|
||||||
|
return Buffer
|
||||||
|
.from(decodeURIComponent(str), 'base64')
|
||||||
|
.toString('utf8');
|
||||||
|
case 'uri':
|
||||||
|
return str.split('/').map((ln) => decodeURIComponent(ln)).join('\n');
|
||||||
|
default:
|
||||||
|
throw new HttpError(400, 'Unknown encoding');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRender(req, res, {match, pickEncoding, log, writeEncoded}) {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
|
||||||
|
const encoding = pickEncoding();
|
||||||
|
const size = {
|
||||||
|
height: getNumeric(match[2], 'height'),
|
||||||
|
width: getNumeric(match[1], 'width'),
|
||||||
|
zoom: getNumeric(match[3], 'zoom'),
|
||||||
|
};
|
||||||
|
const code = readEncoded(match[5], (match[4] || 'uri').toLowerCase());
|
||||||
|
const format = (match[6] || 'svg').toLowerCase();
|
||||||
|
|
||||||
|
const timer = beginTimer();
|
||||||
|
const svg = VirtualSequenceDiagram.render(code, {size});
|
||||||
|
const delay = endTimer(timer);
|
||||||
|
log('RENDER (' + (delay / 1e6).toFixed(3) + 'ms)');
|
||||||
|
|
||||||
|
switch(format) {
|
||||||
|
case 'svg':
|
||||||
|
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||||
|
writeEncoded(encoding, svg);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new HttpError(400, 'Unsupported image format');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = new Server()
|
||||||
|
.addMimeType('htm', 'text/html; charset=utf-8')
|
||||||
|
.addMimeType('html', 'text/html; charset=utf-8')
|
||||||
|
.addMimeType('js', 'application/javascript; charset=utf-8')
|
||||||
|
.addMimeType('mjs', 'application/javascript; charset=utf-8')
|
||||||
|
.addMimeType('css', 'text/css; charset=utf-8')
|
||||||
|
.addMimeType('png', 'image/png')
|
||||||
|
.addMimeType('svg', 'image/svg+xml; charset=utf-8')
|
||||||
|
.addHandler('GET', MATCH_RENDER, handleRender)
|
||||||
|
.addShutdownHook(() => process.stdout.write('\nShutdown.\n'));
|
||||||
|
|
||||||
|
function devMapper(file, type, data) {
|
||||||
|
if(!type.includes('text/html')) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = data.toString('utf8');
|
||||||
|
if(DEV) {
|
||||||
|
return code
|
||||||
|
.replace(/<!--* *DEV *-*>?([^]*?)(?:<!)?-* *\/DEV *-->/g, '$1')
|
||||||
|
.replace(/<!--* *LIVE[^]*? *\/LIVE *-->/g, '');
|
||||||
|
} else {
|
||||||
|
return code
|
||||||
|
.replace(/<!--* *LIVE *-*>?([^]*?)(?:<!)?-* *\/LIVE *-->/g, '$1')
|
||||||
|
.replace(/<!--* *DEV[^]*? *\/DEV *-->/g, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server.addStaticResources('/', '', [
|
||||||
|
'index.html',
|
||||||
|
'library.htm',
|
||||||
|
'styles',
|
||||||
|
'lib',
|
||||||
|
'weblib',
|
||||||
|
'favicon.png',
|
||||||
|
'apple-touch-icon.png',
|
||||||
|
], devMapper);
|
||||||
|
|
||||||
|
if(DEV) {
|
||||||
|
server.addStaticResources('/', '', [
|
||||||
|
'node_modules/requirejs/require.js',
|
||||||
|
'scripts',
|
||||||
|
'web',
|
||||||
|
]);
|
||||||
|
server.setFileWatch(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
server
|
||||||
|
.listen(PORT, HOSTNAME)
|
||||||
|
.then(() => server.printListeningInfo(process.stdout));
|
|
@ -0,0 +1,459 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const http = require('http');
|
||||||
|
const stream = require('stream');
|
||||||
|
const util = require('util');
|
||||||
|
const zlib = require('zlib');
|
||||||
|
|
||||||
|
const PREF_ENCODINGS = ['gzip', 'deflate', 'identity'];
|
||||||
|
|
||||||
|
const PRE_COMPRESS_OPTS = {
|
||||||
|
level: 9,
|
||||||
|
memLevel: 8,
|
||||||
|
windowBits: 15,
|
||||||
|
};
|
||||||
|
|
||||||
|
const LIVE_COMPRESS_OPTS = {
|
||||||
|
level: 5,
|
||||||
|
memLevel: 9,
|
||||||
|
windowBits: 15,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MATCH_ACCEPT = new RegExp('^ *([^;]+)(?:;q=([0-9]+(?:\\.[0-9]+)?))? *$');
|
||||||
|
const MATCH_INDEX = new RegExp('^(.*/)index\\.[^./]+$', 'i');
|
||||||
|
|
||||||
|
function passthroughMapper(file, type, data) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
class HttpError extends Error {
|
||||||
|
constructor(status, message) {
|
||||||
|
super(message);
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAcceptEncoding(accept) {
|
||||||
|
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3
|
||||||
|
|
||||||
|
const opts = (accept || 'identity;q=1, *;q=0.5').split(',');
|
||||||
|
const types = new Map();
|
||||||
|
opts.forEach((opt) => {
|
||||||
|
const match = MATCH_ACCEPT.exec(opt);
|
||||||
|
if(match) {
|
||||||
|
let q = Number.parseFloat(match[2] || '1');
|
||||||
|
if(q === 0) {
|
||||||
|
q = -1;
|
||||||
|
}
|
||||||
|
types.set(match[1], q);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if(!types.has('*')) {
|
||||||
|
if(!types.has('identity')) {
|
||||||
|
types.set('identity', -0.5);
|
||||||
|
}
|
||||||
|
types.set('*', -1);
|
||||||
|
}
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickAcceptEncoding(types, preferred) {
|
||||||
|
let best = null;
|
||||||
|
let bestQ = -1;
|
||||||
|
const wildcard = types.get('*');
|
||||||
|
|
||||||
|
preferred.forEach((opt) => {
|
||||||
|
const q = types.get(opt) || wildcard;
|
||||||
|
if(q > bestQ) {
|
||||||
|
best = opt;
|
||||||
|
bestQ = q;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if(best === null) {
|
||||||
|
throw new HttpError(406, 'Not Acceptable');
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RequestHandler {
|
||||||
|
constructor(method, matcher, handler) {
|
||||||
|
this.method = method;
|
||||||
|
this.matcher = matcher;
|
||||||
|
this.handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(req, res, info) {
|
||||||
|
if(req.method !== this.method) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const match = this.matcher.exec(req.url);
|
||||||
|
if(!match) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(this.handler(req, res, Object.assign({match}, info)) === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bufferIfBetter(buffer, check) {
|
||||||
|
if(buffer.byteLength < check.byteLength) {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class StaticRequestHandler extends RequestHandler {
|
||||||
|
constructor(baseUrlPattern) {
|
||||||
|
super(
|
||||||
|
'GET',
|
||||||
|
new RegExp('^' + baseUrlPattern + '([^?]*)(\\?.*)?$'),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
this.handler = this.handle.bind(this);
|
||||||
|
this.resources = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
add(path, type, content) {
|
||||||
|
let data = content;
|
||||||
|
if(typeof content === 'string') {
|
||||||
|
data = Buffer.from(content, 'utf8');
|
||||||
|
}
|
||||||
|
const existing = this.resources.get(path);
|
||||||
|
if(existing && data.equals(existing.encodings.identity)) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
return Promise.all([
|
||||||
|
data,
|
||||||
|
util.promisify(zlib.deflate)(data, PRE_COMPRESS_OPTS),
|
||||||
|
util.promisify(zlib.gzip)(data, PRE_COMPRESS_OPTS),
|
||||||
|
])
|
||||||
|
.then(([identity, deflate, gzip]) => {
|
||||||
|
const resource = {
|
||||||
|
encodings: {
|
||||||
|
deflate: bufferIfBetter(deflate, identity),
|
||||||
|
gzip: bufferIfBetter(gzip, identity),
|
||||||
|
identity,
|
||||||
|
},
|
||||||
|
path,
|
||||||
|
type,
|
||||||
|
};
|
||||||
|
|
||||||
|
const match = MATCH_INDEX.exec(path);
|
||||||
|
if(match) {
|
||||||
|
this.resources.set(match[1], resource);
|
||||||
|
}
|
||||||
|
this.resources.set(path, resource);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleResource(req, res, resource, {pickEncoding, log}) {
|
||||||
|
log('SERVE ' + resource.path);
|
||||||
|
|
||||||
|
const encoding = pickEncoding(
|
||||||
|
PREF_ENCODINGS.filter((enc) => (resource.encodings[enc] !== null))
|
||||||
|
);
|
||||||
|
if(encoding !== 'identity') {
|
||||||
|
res.setHeader('Content-Encoding', encoding);
|
||||||
|
}
|
||||||
|
res.setHeader('Content-Type', resource.type);
|
||||||
|
res.end(resource.encodings[encoding]);
|
||||||
|
}
|
||||||
|
|
||||||
|
handle(req, res, info) {
|
||||||
|
const resource = this.resources.get(info.match[1]);
|
||||||
|
if(!resource) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.handleResource(req, res, resource, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
write(target) {
|
||||||
|
let maxLen = 0;
|
||||||
|
for(const [path] of this.resources) {
|
||||||
|
maxLen = Math.max(maxLen, path.length);
|
||||||
|
}
|
||||||
|
let indent = '';
|
||||||
|
for(let i = 0; i < maxLen; ++ i) {
|
||||||
|
indent += ' ';
|
||||||
|
}
|
||||||
|
for(const [path, res] of this.resources) {
|
||||||
|
const {encodings} = res;
|
||||||
|
target.write(path + indent.substr(path.length));
|
||||||
|
target.write(' ' + encodings.identity.byteLength + 'b');
|
||||||
|
for(const enc of ['gzip', 'deflate']) {
|
||||||
|
const buf = encodings[enc];
|
||||||
|
if(buf !== null) {
|
||||||
|
target.write(', ' + enc + ': ' + buf.byteLength + 'b');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target.write('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeEncoded(res, encoding, compressionOpts, data) {
|
||||||
|
if(encoding === 'identity') {
|
||||||
|
res.end(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setHeader('Content-Encoding', encoding);
|
||||||
|
|
||||||
|
const raw = new stream.Readable();
|
||||||
|
raw.push(data, 'utf8');
|
||||||
|
raw.push(null);
|
||||||
|
|
||||||
|
switch(encoding) {
|
||||||
|
case 'gzip':
|
||||||
|
raw.pipe(zlib.createGzip(compressionOpts)).pipe(res);
|
||||||
|
break;
|
||||||
|
case 'deflate':
|
||||||
|
raw.pipe(zlib.createDeflate(compressionOpts)).pipe(res);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new HttpError(500, 'Failed to encode');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Server {
|
||||||
|
constructor() {
|
||||||
|
this.awaiting = [];
|
||||||
|
this.mimes = new Map();
|
||||||
|
this.defaultMime = 'text/plain; charset=utf-8';
|
||||||
|
this.handlers = [];
|
||||||
|
this.watchFiles = false;
|
||||||
|
this.watchers = [];
|
||||||
|
this.staticHandler = new StaticRequestHandler('');
|
||||||
|
this.urlMaxLength = 65536;
|
||||||
|
this.server = http.createServer(this._handleRequest.bind(this));
|
||||||
|
this.log = this.log.bind(this);
|
||||||
|
this.logTarget = process.stdout;
|
||||||
|
this.running = false;
|
||||||
|
this.registerShutdown = false;
|
||||||
|
this.shutdownCallbacks = [];
|
||||||
|
|
||||||
|
this.close = this.close.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFileWatch(enabled) {
|
||||||
|
this.watchFiles = enabled;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addMimeType(extension, type) {
|
||||||
|
this.mimes.set(extension, type);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
_addStaticFile(path, file, mapper) {
|
||||||
|
const ext = file.substr(file.lastIndexOf('.') + 1);
|
||||||
|
const type = this.mimes.get(ext) || this.defaultMime;
|
||||||
|
const map = mapper || passthroughMapper;
|
||||||
|
const fn = () => util.promisify(fs.readFile)(file)
|
||||||
|
.then((data) => map(file, type, data))
|
||||||
|
.then((data) => this.staticHandler.add(path, type, data));
|
||||||
|
this.watchers.push({file, fn, watcher: null});
|
||||||
|
return fn();
|
||||||
|
}
|
||||||
|
|
||||||
|
_addStaticDir(path, file, mapper) {
|
||||||
|
return util.promisify(fs.readdir)(file)
|
||||||
|
.then((subFiles) => this._addStaticResources(
|
||||||
|
path + '/',
|
||||||
|
file + '/',
|
||||||
|
subFiles.filter((sub) => !sub.startsWith('.')),
|
||||||
|
mapper
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
_addStaticResource(path, file, mapper) {
|
||||||
|
return util.promisify(fs.stat)(file)
|
||||||
|
.then((stats) => {
|
||||||
|
if(stats.isDirectory()) {
|
||||||
|
return this._addStaticDir(path, file, mapper);
|
||||||
|
} else {
|
||||||
|
return this._addStaticFile(path, file, mapper);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_addStaticResources(basePath, baseFs, files, mapper) {
|
||||||
|
return Promise.all(files.map((file) => this._addStaticResource(
|
||||||
|
basePath + file,
|
||||||
|
baseFs + file,
|
||||||
|
mapper
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
addStaticFile(path, file, mapper = null) {
|
||||||
|
this.awaiting.push(this._addStaticFile(path, file, mapper));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addStaticDir(basePath, baseFile, mapper = null) {
|
||||||
|
this.awaiting.push(this._addStaticDir(basePath, baseFile, mapper));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addStaticResource(path, file, mapper = null) {
|
||||||
|
this.awaiting.push(this._addStaticResource(path, file, mapper));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addStaticResources(basePath, baseFs, files, mapper = null) {
|
||||||
|
this.awaiting.push(
|
||||||
|
this._addStaticResources(basePath, baseFs, files, mapper)
|
||||||
|
);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addHandler(method, matcher, handler) {
|
||||||
|
this.handlers.push(new RequestHandler(method, matcher, handler));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addShutdownHook(callback) {
|
||||||
|
if(callback) {
|
||||||
|
this.shutdownCallbacks.push(callback);
|
||||||
|
}
|
||||||
|
this.registerShutdown = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(message) {
|
||||||
|
this.logTarget.write(new Date().toISOString() + ' ' + message + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleError(req, res, e) {
|
||||||
|
let status = 500;
|
||||||
|
let message = 'An internal error occurred';
|
||||||
|
if(typeof e === 'object' && e.message) {
|
||||||
|
status = e.status || 400;
|
||||||
|
message = e.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.statusCode = status;
|
||||||
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||||
|
res.end(message + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
_makeInfo(req, res) {
|
||||||
|
const acceptEncoding = parseAcceptEncoding(
|
||||||
|
req.headers['accept-encoding']
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
log: this.log,
|
||||||
|
pickEncoding: (opts) => pickAcceptEncoding(
|
||||||
|
acceptEncoding,
|
||||||
|
opts || PREF_ENCODINGS
|
||||||
|
),
|
||||||
|
writeEncoded: (encoding, data) => writeEncoded(
|
||||||
|
res,
|
||||||
|
encoding,
|
||||||
|
LIVE_COMPRESS_OPTS,
|
||||||
|
data
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleRequest(req, res) {
|
||||||
|
try {
|
||||||
|
if(req.url.length > this.urlMaxLength) {
|
||||||
|
throw new HttpError(400, 'Request too long');
|
||||||
|
}
|
||||||
|
const info = this._makeInfo(req, res);
|
||||||
|
for(const handler of this.handlers) {
|
||||||
|
if(handler.apply(req, res, info)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!this.staticHandler.apply(req, res, info)) {
|
||||||
|
throw new HttpError(404, 'Not Found');
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
this._handleError(req, res, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
baseurl() {
|
||||||
|
return 'http://' + this.hostname + ':' + this.port + '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
_addWatchers() {
|
||||||
|
if(this.watchFiles) {
|
||||||
|
this.watchers.forEach((entry) => {
|
||||||
|
if(entry.watcher) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const makeWatcher = () => fs.watch(entry.file, () => {
|
||||||
|
/*
|
||||||
|
* If editor changed file by moving it, we are now
|
||||||
|
* watching the wrong file, so re-create watcher:
|
||||||
|
*/
|
||||||
|
entry.watcher.close();
|
||||||
|
entry.watcher = makeWatcher();
|
||||||
|
|
||||||
|
entry.fn().then((changed) => {
|
||||||
|
if(changed) {
|
||||||
|
this.log('RELOADED ' + entry.file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
entry.watcher = makeWatcher();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_removeWatchers() {
|
||||||
|
this.watchers.forEach((entry) => {
|
||||||
|
if(entry.watcher) {
|
||||||
|
entry.watcher.close();
|
||||||
|
entry.watcher = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
listen(port, hostname) {
|
||||||
|
if(this.running) {
|
||||||
|
throw new Error('Already listening');
|
||||||
|
}
|
||||||
|
this.port = port;
|
||||||
|
this.hostname = hostname;
|
||||||
|
return Promise.all(this.awaiting).then(() => new Promise((resolve) => {
|
||||||
|
this._addWatchers();
|
||||||
|
this.server.listen(port, hostname, () => {
|
||||||
|
this.running = true;
|
||||||
|
if(this.registerShutdown) {
|
||||||
|
process.on('SIGINT', this.close);
|
||||||
|
}
|
||||||
|
resolve(this);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if(!this.running) {
|
||||||
|
return Promise.resolve(this);
|
||||||
|
}
|
||||||
|
this.running = false;
|
||||||
|
this._removeWatchers();
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.server.close(() => {
|
||||||
|
this.shutdownCallbacks.forEach((fn) => fn(this));
|
||||||
|
resolve(this);
|
||||||
|
process.removeListener('SIGINT', this.close);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
printListeningInfo(target) {
|
||||||
|
target.write('Serving static resources:\n');
|
||||||
|
this.staticHandler.write(target);
|
||||||
|
target.write('\n');
|
||||||
|
target.write('Available at ' + this.baseurl() + '\n\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {HttpError, Server};
|
|
@ -16,25 +16,22 @@ To get started, you can clone this repository and run:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
npm install;
|
npm install;
|
||||||
npm start;
|
npm start -- dev;
|
||||||
```
|
```
|
||||||
|
|
||||||
This will launch a server in the project directory. You can now open
|
This will launch a server in the project directory. You can now open
|
||||||
several pages:
|
several pages:
|
||||||
|
|
||||||
* [http://localhost:8080/](http://localhost:8080/):
|
* [http://localhost:8080/](http://localhost:8080/):
|
||||||
the main editor (uses minified sources, so you won't see your changes
|
the main editor
|
||||||
immediately)
|
|
||||||
* [http://localhost:8080/editor-dev.htm](http://localhost:8080/editor-dev.htm):
|
|
||||||
the main editor, using non-minified sources (good for development)
|
|
||||||
* [http://localhost:8080/library.htm](http://localhost:8080/library.htm):
|
* [http://localhost:8080/library.htm](http://localhost:8080/library.htm):
|
||||||
the library sample page (uses minified sources)
|
the library sample page
|
||||||
|
|
||||||
**NOTE**: This project uses web modules, which are only supported by
|
**NOTE**: When running in `dev` mode, this project uses web modules,
|
||||||
recent browsers. In particular, note that FireFox 59 does not support
|
which are only supported by recent browsers. In particular, note that
|
||||||
web modules unless a flag is set (FireFox 60 will support them fully).
|
FireFox 59 does not support web modules unless a flag is set (FireFox
|
||||||
The editor and library page do not require web modules, so should have
|
60 will support them fully). Production mode does not require web
|
||||||
wider support.
|
modules, so should have wider support.
|
||||||
|
|
||||||
To run the tests and linter, run the command:
|
To run the tests and linter, run the command:
|
||||||
|
|
||||||
|
@ -48,6 +45,16 @@ And to rebuild the minified sources, run:
|
||||||
npm run minify;
|
npm run minify;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To check that the code works with minified sources, run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm start;
|
||||||
|
```
|
||||||
|
|
||||||
|
(index.htm and library.htm will now run with minified sources, but note
|
||||||
|
that it will not perform hot-reloading; you will need to restart the
|
||||||
|
server if you make changes)
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
The available commands are:
|
The available commands are:
|
||||||
|
@ -86,9 +93,8 @@ The high-level structure is:
|
||||||
Useful helpers can also be found in `/scripts/core/*` and
|
Useful helpers can also be found in `/scripts/core/*` and
|
||||||
`/scripts/svg/*`.
|
`/scripts/svg/*`.
|
||||||
|
|
||||||
The live editor (index.htm & editor-dev.htm) uses the source in
|
The live editor (index.htm) uses the source in `/web/editor.mjs` and
|
||||||
`/web/editor.mjs` and `/web/interface/*`. Other pages use sources in
|
`/web/interface/*`.
|
||||||
the root of `/web` as their entry-points.
|
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
|
|
@ -1,86 +0,0 @@
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="content-security-policy" content="
|
|
||||||
base-uri 'self';
|
|
||||||
default-src 'none';
|
|
||||||
script-src 'self' https://cdnjs.cloudflare.com https://unpkg.com;
|
|
||||||
style-src 'self'
|
|
||||||
https://cdnjs.cloudflare.com
|
|
||||||
'sha256-s7UPtBgvov5WNF9C1DlTZDpqwLgEmfiWha5a5p/Zn7E='
|
|
||||||
;
|
|
||||||
font-src 'self' data:;
|
|
||||||
img-src 'self' blob:;
|
|
||||||
form-action 'none';
|
|
||||||
">
|
|
||||||
|
|
||||||
<title>Sequence Diagram</title>
|
|
||||||
<link rel="icon" href="favicon.png">
|
|
||||||
<link rel="apple-touch-icon" href="apple-touch-icon.png">
|
|
||||||
|
|
||||||
<link rel="stylesheet"
|
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.37.0/codemirror.min.css"
|
|
||||||
integrity="sha256-I8NyGs4wjbMuBSUE40o55W6k6P7tu/7G28/JGUUYCIs="
|
|
||||||
crossorigin="anonymous"
|
|
||||||
>
|
|
||||||
|
|
||||||
<link rel="stylesheet"
|
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.37.0/addon/hint/show-hint.min.css"
|
|
||||||
integrity="sha256-Ng5EdzHS/CC37tR7tE75e4Th9+fBvOB4eYITOkXS22Q="
|
|
||||||
crossorigin="anonymous"
|
|
||||||
>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="styles/editor.css">
|
|
||||||
|
|
||||||
<script
|
|
||||||
src="node_modules/requirejs/require.js"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
></script>
|
|
||||||
|
|
||||||
<meta
|
|
||||||
name="cdn-split"
|
|
||||||
content="https://unpkg.com/split.js@1.3.5/split.min.js"
|
|
||||||
data-integrity="sha256-vBu0/2gpeLlfmY2yFkr8eHTMuYKBw2M+/Id/qZx27F4="
|
|
||||||
>
|
|
||||||
|
|
||||||
<meta
|
|
||||||
name="cdn-cm/lib/codemirror"
|
|
||||||
content="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.37.0/codemirror.min.js"
|
|
||||||
data-integrity="sha256-U/4XQwZXDFDdAHjIZt1Lm7sFfmMiFDZzFYprq6XJ0gk="
|
|
||||||
>
|
|
||||||
|
|
||||||
<meta
|
|
||||||
name="cdn-cm/addon/hint/show-hint"
|
|
||||||
content="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.37.0/addon/hint/show-hint.min.js"
|
|
||||||
data-integrity="sha256-/Cxd7R7oycnq3vuRycj68ToCzdZ73tux8nSULZiWWK0="
|
|
||||||
>
|
|
||||||
|
|
||||||
<meta
|
|
||||||
name="cdn-cm/addon/edit/trailingspace"
|
|
||||||
content="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.37.0/addon/edit/trailingspace.min.js"
|
|
||||||
data-integrity="sha256-IazJOM2ma6yJ/doWlcIy+Ica+UAgYJDDyYf/qgkLUNM="
|
|
||||||
>
|
|
||||||
|
|
||||||
<meta
|
|
||||||
name="cdn-cm/addon/comment/comment"
|
|
||||||
content="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.37.0/addon/comment/comment.min.js"
|
|
||||||
data-integrity="sha256-xRNygSqAYMT9wcso0FgZEY3ROGoD6JdvYd8M9IjSuNg="
|
|
||||||
>
|
|
||||||
|
|
||||||
<script src="web/editor.mjs" type="module"></script>
|
|
||||||
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div id="loader">
|
|
||||||
<h1>Sequence Diagram Online Editor</h1>
|
|
||||||
<p class="loadmsg">Loading…</p>
|
|
||||||
<noscript><p class="noscript">This tool requires Javascript!<p></noscript>
|
|
||||||
<nav>
|
|
||||||
<a href="library.htm" target="_blank">Library</a>
|
|
||||||
<a href="https://github.com/davidje13/SequenceDiagram" target="_blank">GitHub</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
24
index.html
24
index.html
|
@ -32,14 +32,6 @@
|
||||||
|
|
||||||
<link rel="stylesheet" href="styles/editor.css">
|
<link rel="stylesheet" href="styles/editor.css">
|
||||||
|
|
||||||
<script src="lib/sequence-diagram-web.min.js"></script>
|
|
||||||
|
|
||||||
<script
|
|
||||||
src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.5/require.min.js"
|
|
||||||
integrity="sha256-0SGl1PJNDyJwcV5T+weg2zpEMrh7xvlwO4oXgvZCeZk="
|
|
||||||
crossorigin="anonymous"
|
|
||||||
></script>
|
|
||||||
|
|
||||||
<meta
|
<meta
|
||||||
name="cdn-split"
|
name="cdn-split"
|
||||||
content="https://unpkg.com/split.js@1.3.5/split.min.js"
|
content="https://unpkg.com/split.js@1.3.5/split.min.js"
|
||||||
|
@ -70,7 +62,23 @@
|
||||||
data-integrity="sha256-xRNygSqAYMT9wcso0FgZEY3ROGoD6JdvYd8M9IjSuNg="
|
data-integrity="sha256-xRNygSqAYMT9wcso0FgZEY3ROGoD6JdvYd8M9IjSuNg="
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<!-- DEV
|
||||||
|
<script
|
||||||
|
src="node_modules/requirejs/require.js"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
></script>
|
||||||
|
<script src="web/editor.mjs" type="module"></script>
|
||||||
|
/DEV -->
|
||||||
|
|
||||||
|
<!-- LIVE -->
|
||||||
|
<script src="lib/sequence-diagram-web.min.js"></script>
|
||||||
|
<script
|
||||||
|
src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.5/require.min.js"
|
||||||
|
integrity="sha256-0SGl1PJNDyJwcV5T+weg2zpEMrh7xvlwO4oXgvZCeZk="
|
||||||
|
crossorigin="anonymous"
|
||||||
|
></script>
|
||||||
<script src="weblib/editor.min.js"></script>
|
<script src="weblib/editor.min.js"></script>
|
||||||
|
<!-- /LIVE -->
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -57,7 +57,13 @@
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
></script>
|
></script>
|
||||||
|
|
||||||
|
<!-- DEV
|
||||||
|
<script src="scripts/standalone-web.mjs" type="module"></script>
|
||||||
|
/DEV -->
|
||||||
|
|
||||||
|
<!-- LIVE -->
|
||||||
<script src="lib/sequence-diagram-web.min.js"></script>
|
<script src="lib/sequence-diagram-web.min.js"></script>
|
||||||
|
<!-- /LIVE -->
|
||||||
|
|
||||||
<script>document.addEventListener('DOMContentLoaded', () => {
|
<script>document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -30,7 +30,7 @@
|
||||||
"minify-web": "rollup --config web/rollup.config.js && uglifyjs --compress --mangle --warn --output weblib/editor.min.js -- weblib/editor.js",
|
"minify-web": "rollup --config web/rollup.config.js && uglifyjs --compress --mangle --warn --output weblib/editor.min.js -- weblib/editor.js",
|
||||||
"minify": "npm run minify-lib && npm run minify-web",
|
"minify": "npm run minify-lib && npm run minify-web",
|
||||||
"prepublishOnly": "npm run minify-lib && npm run generate-screenshots && npm test",
|
"prepublishOnly": "npm run minify-lib && npm run generate-screenshots && npm test",
|
||||||
"start": "http-server",
|
"start": "bin/server.js",
|
||||||
"test": "npm run unit-test && npm run web-test && npm run lint && echo 'PASSED :)'",
|
"test": "npm run unit-test && npm run web-test && npm run lint && echo 'PASSED :)'",
|
||||||
"unit-test": "rollup --config spec/support/rollup.config.js && node -r source-map-support/register node_modules/.bin/jasmine --config=spec/support/jasmine.json",
|
"unit-test": "rollup --config spec/support/rollup.config.js && node -r source-map-support/register node_modules/.bin/jasmine --config=spec/support/jasmine.json",
|
||||||
"web-test": "karma start spec/support/karma.conf.js --single-run",
|
"web-test": "karma start spec/support/karma.conf.js --single-run",
|
||||||
|
@ -41,7 +41,6 @@
|
||||||
"codemirror": "^5.37.0",
|
"codemirror": "^5.37.0",
|
||||||
"eslint": "^4.19.1",
|
"eslint": "^4.19.1",
|
||||||
"eslint-plugin-jasmine": "^2.9.3",
|
"eslint-plugin-jasmine": "^2.9.3",
|
||||||
"http-server": "^0.10.0",
|
|
||||||
"jasmine": "^3.1.0",
|
"jasmine": "^3.1.0",
|
||||||
"karma": "^2.0.2",
|
"karma": "^2.0.2",
|
||||||
"karma-chrome-launcher": "^2.2.0",
|
"karma-chrome-launcher": "^2.2.0",
|
||||||
|
|
Loading…
Reference in New Issue