/**
* @copyright Copyright 2016 Kevin Locke <kevin@kevinlocke.name>
* @license MIT
*/
'use strict';
const fs = require('fs');
const AggregateError = require('./lib/aggregate-error');
/** Combines one or more errors into a single error.
*
* @param {AggregateError|Error} errPrev Previous errors, if any.
* @param {!Error} errNew New error.
* @return {!AggregateError|!Error} Error which represents all errors that have
* occurred. If only one error has occurred, it will be returned. Otherwise
* an {@link AggregateError} including all previous errors will be returned.
* @private
*/
function combineErrors(errPrev, errNew) {
if (!errPrev) {
return errNew;
}
let errCombined;
if (errPrev instanceof AggregateError) {
errCombined = errPrev;
} else {
errCombined = new AggregateError();
errCombined.push(errPrev);
}
errCombined.push(errNew);
return errCombined;
}
/** Options for {@link nodecat}.
*
* @typedef {{
* fileStreams: (Object<string,!stream.Readable>|undefined),
* outStream: (stream.Writable|undefined),
* errStream: (stream.Writable|undefined)
* }} CommandOptions
* @property {Object<string,!stream.Readable>=} fileStreams Mapping from file
* names to readable streams which will be read for the named file. If the
* file appears multiple times, the stream is only read once.
* @property {stream.Writable=} outStream Stream to which concatenated output
* is written. (default: <code>process.stdout</code>)
* @property {stream.Writable=} errStream Stream to which errors (and
* non-output status messages) are written.
* (default: <code>process.stderr</code>)
*/
// var NodecatOptions;
/** Concatenate named files.
*
* @param {!Array<string>} fileNames Names of files to be concatenated, in the
* order in which their content will appear. Files may appear multiple times.
* If the Array is empty, no output will be written.
* @param {NodecatOptions=} options Options.
* @param {?function(Error)=} callback Callback with the first
* <code>Error</code> which occurred, if any. Note that concatenation
* continues after errors. Required if <code>global.Promise</code> is not
* defined.
* @return {Promise|undefined} If <code>callback</code> is not given and
* <code>global.Promise</code> is defined, a <code>Promise</code> which
* resolves once all output has been written.
*/
function nodecat(fileNames, options, callback) {
if (!callback && typeof options === 'function') {
callback = options;
options = null;
}
if (!callback) {
return new Promise((resolve, reject) => {
nodecat(fileNames, options, (err, result) => {
if (err) { reject(err); } else { resolve(result); }
});
});
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function');
}
const callerStreamEnded = {};
const callerStreams = (options && options.fileStreams) || {};
const errStream = (options && options.errStream) || process.stderr;
const outStream = (options && options.outStream) || process.stdout;
try {
if (!fileNames
|| typeof fileNames !== 'object'
|| fileNames.length !== Math.floor(fileNames.length)) {
throw new TypeError('fileNames must be an Array-like object');
}
if (options && typeof options !== 'object') {
throw new TypeError('options must be an object');
}
if (typeof callerStreams !== 'object') {
throw new TypeError('options.fileStreams must be an object');
}
if (typeof outStream.write !== 'function') {
throw new TypeError('options.outStream must be a stream.Writable');
}
if (typeof errStream.write !== 'function') {
throw new TypeError('options.errStream must be a stream.Writable');
}
} catch (err) {
process.nextTick(() => {
callback(err);
});
return undefined;
}
// Error which will be returned from nodecat
let errNodecat = null;
// Cleanup function for the currently piping input stream
let inCleanup;
function allDone() {
outStream.removeListener('error', onOutError);
callback(errNodecat);
}
// Note: src.unpipe is called by stream.Readable internals on dest 'error'
function onOutError(err) {
errNodecat = combineErrors(errNodecat, err);
errStream.write(`nodecat: ${err}\n`);
if (inCleanup) {
inCleanup();
}
allDone();
}
outStream.once('error', onOutError);
let i = 0;
function catNext() {
if (i >= fileNames.length) {
allDone();
return;
}
const fileName = fileNames[i];
i += 1;
const callerStream = callerStreams[fileName];
if (callerStream && callerStreamEnded[fileName]) {
catNext();
return;
}
const inStream = callerStream || fs.createReadStream(fileName);
inCleanup = function cleanup() {
inStream.removeListener('error', onInError);
inStream.removeListener('end', done);
};
function done() {
if (callerStream) {
callerStreamEnded[fileName] = true;
}
inCleanup();
catNext();
}
function onInError(err) {
// Mark error with the name of the file which caused it
err.fileName = fileName;
errNodecat = combineErrors(errNodecat, err);
errStream.write(`nodecat: ${fileName}: ${err.message}\n`);
// There is no way to know whether more data may be emitted.
// To be safe, unpipe to prevent interleaving data after starting next.
if (typeof inStream.unpipe === 'function') {
inStream.unpipe(outStream);
}
done();
}
inStream.once('error', onInError);
inStream.once('end', done);
inStream.pipe(outStream, {end: false});
}
catNext();
return undefined;
}
module.exports = nodecat;