@discoveryjs/cli
Version:
CLI tools to serve & build projects based on Discovery.js
468 lines (397 loc) • 12.1 kB
JavaScript
const fs = require('fs');
const path = require('path');
const { fork } = require('child_process');
const { Readable } = require('stream');
const { EventEmitter } = require('events');
const mime = require('mime');
const chalk = require('chalk');
const prettyMs = require('pretty-ms');
const discoveryCliPath = path.resolve(__dirname, '../..');
const ANSI_REGEXP = /([\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><])/g;
let printIdent = 0;
let silent = false;
const processName = require.main
? require('path').basename(require.main.filename)
: '<unknown process>';
function stdoutWrite(str) {
if (!silent) {
process.stdout.write(str);
}
}
function print(...args) {
stdoutWrite(' '.repeat(printIdent) + args.join(' '));
}
function println(...args) {
stdoutWrite(' '.repeat(printIdent) + args.join(' ') + '\n');
}
function sectionStart(...args) {
println(...args);
printIdent++;
}
function sectionEnd(...args) {
if (args.length) {
println(...args);
}
printIdent = Math.max(printIdent - 1, 0);
}
function section(name, fn) {
sectionStart(name);
const res = fn();
if (res && typeof res.then === 'function') {
return res.finally(sectionEnd);
}
sectionEnd();
return res;
}
function processStep(name, fn) {
print(name + ' ... ');
const startTime = Date.now();
const res = fn();
if (res && typeof res.then === 'function') {
return res.then(res => {
const time = Date.now() - startTime;
stdoutWrite(`OK${time > 2 ? chalk.gray(` (${time}ms)`) : ''}\n`);
return res;
});
}
const time = Date.now() - startTime;
stdoutWrite(`OK${time > 2 ? chalk.gray(` (${time}ms)`) : ''}\n`);
return res;
}
async function silentFn(fn) {
try {
silent = true;
return await fn();
} finally {
silent = false;
}
}
function time() {
const pad2 = value => String(value).padStart(2, 0);
const now = new Date();
return `${pad2(now.getHours())}:${pad2(now.getMinutes())}:${pad2(now.getSeconds())}`;
}
function prettyDuration(duration, options) {
if (typeof options === 'boolean') {
options = {
spaces: options
};
} else if (!options) {
options = {};
}
const result = prettyMs(duration, options);
return options.spaces === false
? result.replace(/\s+/g, '')
: result;
}
function createBufferingStream(inputReadable, maxBufferSize = 1024 * 1024) {
if (maxBufferSize === 0) {
return inputReadable;
}
const bufferingReadableStream = new ReadableStream({
start() {
this.reader = ReadableStream.from(inputReadable).getReader();
},
async pull(controller) {
const chunks = [];
let bufferSize = 0;
while (bufferSize < maxBufferSize) {
const { done, value } = await this.reader.read();
if (done) {
break;
}
chunks.push(value);
bufferSize += value.byteLength;
}
if (bufferSize > 0) {
controller.enqueue(Buffer.concat(chunks));
} else {
controller.close();
this.cancel();
}
},
cancel() {
if (this.reader) {
this.reader.release();
this.reader = null;
}
}
});
return Readable.fromWeb(bufferingReadableStream);
}
function isReadableStream(value) {
return (
value instanceof EventEmitter &&
(typeof value.read === 'function' || typeof value._read === 'function') &&
typeof value.pipe === 'function' &&
value.readable !== false &&
value.readableObjectMode === false
);
}
function prepareStdStreamChunkForMessage(chunk) {
return chunk
// convert to string
.toString('utf8')
// strip ansi escapes
.replace(ANSI_REGEXP, '');
}
function createEventsSource() {
let listeners = [];
let messages = [];
let closed = false;
const emit = (message) => {
if (closed) {
return;
}
messages.push(message);
if (message === null) {
closed = true;
}
for (const { fn } of listeners) {
fn(message);
}
};
return {
emit,
listen(fn) {
let listener = { fn };
Promise.resolve().then(() => {
if (closed) {
listener = null;
}
if (listener === null) {
return;
}
for (let message of messages) {
fn(message);
}
listeners.push(listener);
});
return () => {
listeners = listeners.filter(item => item !== listener);
listener = null;
};
},
dispose() {
emit(null);
listeners = [];
messages = null;
}
};
}
function runScript(command, args) {
const eventsSource = createEventsSource();
const runScriptResult = new Promise((resolve_, reject_) => {
let resolved = false;
const stderr = [];
const stderrBuffer = [];
const child = fork(command, args, {
execArgv: ['--unhandled-rejections=strict'],
stdio: ['inherit', 'pipe', 'pipe', 'ipc', 'pipe'],
env: {
...process.env,
FORCE_COLOR: chalk.supportsColor ? chalk.supportsColor.level : 0
}
});
eventsSource.emit({
type: 'start',
timestamp: Date.now()
});
child.stderr
.on('data', chunk => {
stderr.push(chunk);
stderrBuffer.push(chunk);
eventsSource.emit({
type: 'stderr',
chunk: serializeErrorForClient(chunk)
});
});
child.stdout
.on('data', chunk => {
if (stderrBuffer) {
// flush error buffer
stderrBuffer.splice(0, Infinity).forEach(chunk =>
process.stderr.write(chalk.yellow(chunk))
);
}
process.stdout.write(chunk);
eventsSource.emit({
type: 'stdout',
chunk: prepareStdStreamChunkForMessage(chunk)
});
});
child
.on('message', async message => {
if (message.type) {
eventsSource.emit(message);
return;
}
if (message.payload === 'stream') {
try {
resolve({
...message,
stream: createBufferingStream(child.stdio[4], 1024 * 1024)
});
} catch (e) {
reject(e);
}
} else {
resolve(message);
}
})
.on('close', code => {
const stderrOutput = stderr.join('');
if (stderrOutput || code) {
const error = new Error(stderrOutput || 'Child process script exited with code ' + code);
if (resolved) {
process.stderr.write([
chalk.white.bgRed('CHILD SCRIPT ERROR') + chalk.gray(' (after a result promise is resolved)'),
chalk.red('| ') + chalk.yellow([command].concat(args).join(' ')),
chalk.red('|'),
chalk.red('| ') + chalk.red(error.message.replace(/\r\n?|\n/g, '\n| '))
].join('\n') + '\n');
}
reject(error);
}
// make sure promise is fulfilled on script finish
reject(new Error('Unresolved runScript promise'));
});
function resolve(...args) {
resolved = true;
resolve_(...args);
}
function reject(...args) {
resolved = true;
reject_(...args);
}
}).catch((error) => {
eventsSource.emit({
type: 'crash',
timestamp: Date.now()
});
throw error;
}).finally(() => {
eventsSource.emit({
type: 'finish',
timestamp: Date.now()
});
eventsSource.dispose();
});
return Object.assign(runScriptResult, {
listen: eventsSource.listen
});
}
function logMsg(...args) {
console.log(
chalk.grey(time()),
chalk.grey(processName),
...args
);
}
function logError(...args) {
console.error(
chalk.grey(time()),
chalk.grey(processName),
chalk.bgRed.white('ERROR'),
...args.map(val => val instanceof Error ? '\n' + chalk.redBright(val) : val)
);
}
function logWarning(...args) {
console.warn(
chalk.grey(time()),
chalk.grey(processName),
chalk.bgYellow.black('WARNING'),
...args.map(val => val instanceof Error ? '\n' + chalk.yellow(val) : val)
);
}
function logSlugMsg(slug, ...args) {
logMsg(chalk.cyan(slug), ...args);
}
function logSlugError(slug, ...args) {
logError(chalk.cyan(slug), ...args);
}
function logSlugWarning(slug, ...args) {
logWarning(chalk.cyan(slug), ...args);
}
function toRegExp(str) {
return new RegExp(str.replace(/\[\]\(\)\{\}\.\+\*\?/g, '\\$1'), 'g');
}
function serializeErrorForClient(error) {
const cwd = process.cwd();
const text = String(error.stack || error).replace(ANSI_REGEXP, '');
if (cwd) {
const cwdRx = toRegExp(cwd);
const discoveryCliRx = toRegExp(discoveryCliPath);
return text
.replace(cwdRx, '.')
.replace(discoveryCliRx, path.relative(cwd, discoveryCliPath));
}
return text;
}
function sortModels(models) {
return models.slice().sort(({ slug: a }, { slug: b }) =>
a > b ? 1 : a < b ? -1 : 0
);
}
function nameExt(pathname) {
const ext = path.extname(pathname);
return {
name: pathname.slice(0, -ext.length),
ext: ext.slice(1)
};
}
function buildEntryNameByPattern(pattern = '[slug]/index', values) {
return path.posix.resolve('/', pattern.replace(
/\[([a-z]+)\]/g,
(m, name) => values[name] ?? m)
).slice(1) + '.html';
}
function buildScriptNameByPattern(pattern = '[slug]/[name]', entryName, values) {
return path.posix.relative(
path.posix.dirname(entryName),
path.posix.resolve('/', pattern.replace(
/\[([a-z]+)\]/g,
(m, name) => values[name] ?? m)
).slice(1) + '.' + values.ext
);
}
function buildAssetNameByPattern(pattern = '[slug]/[name]', entryName, values) {
return path.posix.relative(
path.posix.dirname(entryName),
path.posix.resolve('/', pattern.replace(
/\[([a-z]+)\]/g,
(m, name) => values[name] ?? m)
).slice(1) + '.' + values.ext
);
}
function dataUriForPath(filepath, content) {
return `data:${mime.getType(path.extname(filepath))};base64,${
content || fs.readFileSync(filepath, 'base64')
}`;
}
module.exports = {
print,
println,
section,
sectionStart,
sectionEnd,
process: processStep,
silent: silentFn,
time,
prettyDuration,
runScript,
logMsg,
logError,
logWarning,
logSlugMsg,
logSlugError,
logSlugWarning,
serializeErrorForClient,
sortModels,
nameExt,
buildEntryNameByPattern,
buildScriptNameByPattern,
buildAssetNameByPattern,
dataUriForPath,
isReadableStream
};