/** @namespace utils */
// @flow
import fs from 'fs'
import util from 'util'
import { typeOf } from 'ne-types'
import { sync as readPkg } from 'read-pkg-up'
import { merge } from 'lodash'
export { dedent as joinLines } from 'ne-tag-fns'
const { Stats } = fs;
/**
* Deferred is modeled after jQuery's deferred object. It inverts a promise
* such that its resolve and reject methods can be invoked without wrapping
* all of the related code within a Promise's function.
*
* @memberof utils
* @class Deferred
*/
export class Deferred {
/**
* This property holds a `resolve` function from within the promise this
* deferred inverts.
*
* @type {Function}
* @memberof Deferred
* @instance
*/
resolve: Function;
/**
* This property holds a `reject` function from within the promise this
* deferred inverts
*
* @type {Function}
* @memberof Deferred
* @instance
*/
reject: Function;
/**
* This is the promise wrapped by and inverted in this deferred instance
*
* @type {Promise}
* @memberof Deferred
* @instance
*/
promise: any;
/**
* An at a glance boolean property that denotes whether or not this
* deferred has been resolved or rejected yet.
*
* @type {boolean}
* @memberof Deferred
* @instance
*/
complete: boolean;
/**
* Creates an object with four properties of note; promise, resolve, reject
* and a flag complete that will be set once either resolve or reject have
* been called. A Deferred is considered to be pending while complete is set
* to false.
*
* Once constructed, resolve and reject can be called later, at which point,
* the promise is completed. The promise property is the promise resolved
* or rejected by the associated properties and can be used with other
* async/await or Promise based code.
*
* @instance
* @memberof Deferred
* @method ⎆⠀constructor
*
* @param {any} resolveWith a deferred resolved as Promise.resolve() might do
* @param {any} rejectWith a deferred rejected as Promise.reject() might do
*/
constructor(resolveWith: any, rejectWith: any) {
this.promise = new Promise((resolve, reject) => {
this.complete = false;
this.resolve = (...args) => {
this.complete = true;
return resolve(...args);
};
this.reject = (...args) => {
this.complete = true;
return reject(...args);
};
if (resolveWith && !rejectWith) { this.resolve(resolveWith) }
if (rejectWith && !resolveWith) { this.reject(rejectWith) }
});
}
/**
* Shorthand getter that denotes true if the deferred is not yet complete.
*
* @instance
* @memberof Deferred
* @method ⬇︎⠀pending
*
* @return {boolean} true if the promise is not yet complete; false otherwise
*/
get pending(): boolean { return !this.complete }
/**
* Promises are great but if the code never resolves or rejects a deferred,
* then things will become eternal; in a bad way. This makes that less likely
* of an event.
*
* If the number of milliseconds elapses before a resolve or reject occur,
* then the deferred is rejected.
*
* @static
* @memberof Deferred
* @method ⌾⠀TimedDeferred
*
* @param {Number} timeOut a number of milliseconds to wait before rejecting
* the deferred.
* @param {Promise} proxyPromise a promise to proxy then/catch through to the
* deferreds resolve/reject.
* @return {Deferred} an instance of deferred that will timeout after
* `timeOut` milliseconds have elapsed. If `proxyPromise` is a `Promise`
* then the deferred's reject and resolve will be tied to the Promise's
* catch() and then() methods, respectively.
*/
static TimedDeferred(timeOut: Number, proxyPromise: ?any): Deferred {
const deferred = new Deferred();
if (proxyPromise && typeOf(proxyPromise) === Promise.name) {
proxyPromise.then((...args) => deferred.resolve(...args))
proxyPromise.catch(reason => deferred.reject(reason))
}
setTimeout(() => deferred.reject(new Error('Deferred timed out'), timeOut))
return deferred;
}
}
/**
* A simply promisify style function that returns an async function wrapped
* around a supplied function designed for the standard callback methodology.
* If the callback is the last parameter, and that callback is in the form of
* (error, ...results) then this wrapper will do the trick for you.
*
* @method utils~⌾⠀promisify
* @since 2.7.0
*
* @param {Function} method a function to wrap in an asynchronous function
* @param {mixed} context an optional `this` object for use with the supplied
* function.
* @return {Function} an asynchronous function, i.e. one that returns a promise
* containing the contents the callback results, that wraps the supplied
* function.
*/
export function promisify(method: Function, context?: mixed): Function {
return async function(...args) {
return new Promise((resolve, reject) => {
args.push(function(error, ...callbackArgs) {
if (error) {
reject(error);
}
else {
resolve(...callbackArgs);
}
});
method.apply(context, args);
})
}
}
/**
* It may be necessary to read GraphQL Lattice preferences from the nearest
* `package.json` object to the excuting code. `getLatticePrefs()` does this
* and merges any subsequently found options in said file on top of the
* default values specified here in this file.
*
* @method utils~⌾⠀getLatticePrefs
* @since 2.13.0
*
* @return {Object} an object containing at least the defaults plus any other
* values specified in `package.json`
*/
export function getLatticePrefs(readPkgUpOpts: ?Object): Object {
let { pkg } = readPkg(readPkgUpOpts)
let options = {
ModuleParser: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
failOnError: false
}
}
if (pkg.lattice) {
merge(options, pkg.lattice || {})
}
return options;
}
/**
* A small near pass-thru facility for logging within Lattice such that error
* objects supplied get mapped to their message unless `LATTICE_ERRORS=STACK`
* is set in `process.env`.
*
* Note the order of log levels for Lattice may be somewhat non-standard. Info
* has been taken out of flow and placed above error to solve issues with jest
* logging.
*
* @memberof utils
* @type Object
* @static
*/
export const LatticeLogs = {
get LOG(): string { return 'log' },
get WARN(): string { return 'warn' },
get ERROR(): string { return 'error' },
get INFO(): string { return 'info' },
get TRACE(): string { return 'trace' },
/**
* Ordering of log levels for LatticeLogs. `INFO` is a non error log level
* that is non-crucial and appears if LATTICE_LOGLEVEL is set to `INFO` or
* `TRACE`
*/
get LEVELS(): Array<string> {
const ll = LatticeLogs
return [ll.LOG, ll.WARN, ll.ERROR, ll.INFO, ll.TRACE]
},
equalOrBelow(testedLevel: string, lessThan: string = 'error') {
const ll = LatticeLogs
return ll.LEVELS.indexOf(testedLevel) <= ll.LEVELS.indexOf(lessThan)
},
atLeast(testedLevel: string, atLeastLevel: string): boolean {
const ll = LatticeLogs
return ll.LEVELS.indexOf(testedLevel) >= ll.LEVELS.indexOf(atLeastLevel)
},
/**
* All arguments of any logging function in `LatticeLogs` get passed through
* this function first to modify or alter the type of value being logged.
*
* @param {mixed} arg the argument being passed to the `map()` function
* @param {number} index the index in the array of arguments
* @param {Array<mixed>} array the array containing this element
*/
argMapper(arg: mixed, index: number, array: Array<mixed>): mixed {
let isError = typeOf(arg) === Error.name
let showStack = /\bSTACK\b/i.test(process.env.LATTICE_ERRORS || '')
// $FlowFixMe
return !isError ? arg : (showStack ? arg : arg.message)
},
/** A function that, when it returns true, will cause logging to be skipped */
failFast(logLevel: ?string, lessThan: ?string) {
const ll = LatticeLogs
if (logLevel) {
let compareTo = lessThan || process.env.LATTICE_LOGLEVEL || ll.ERROR
if (!ll.equalOrBelow(logLevel, compareTo)) return true
}
return /\b(NONE|OFF|NO|0)\b/i.test(process.env.LATTICE_ERRORS || '')
},
/** Pass-thru to console.log; arguments parsed via `argMapper` */
log(...args: Array<mixed>) {
if (LatticeLogs.failFast(LatticeLogs.LOG)) return;
console.log(...args.map(LatticeLogs.argMapper))
},
/** Pass-thru to console.warn; arguments parsed via `argMapper` */
warn(...args: Array<mixed>) {
if (LatticeLogs.failFast(LatticeLogs.WARN)) return;
console.warn(...args.map(LatticeLogs.argMapper))
},
/** Pass-thru to console.error; arguments parsed via `argMapper` */
error(...args: Array<mixed>) {
if (LatticeLogs.failFast(LatticeLogs.ERROR)) return;
console.error(...args.map(LatticeLogs.argMapper))
},
/** Pass-thru to console.info; arguments parsed via `argMapper` */
info(...args: Array<mixed>) {
if (LatticeLogs.failFast(LatticeLogs.INFO)) return;
console.info(...args.map(LatticeLogs.argMapper))
},
/** Pass-thru to console.trace; arguments parsed via `argMapper` */
trace(...args: Array<mixed>) {
if (LatticeLogs.failFast(LatticeLogs.TRACE)) return;
console.trace(...args.map(LatticeLogs.argMapper))
},
outWrite(
chunk: string|Uint8Array|Buffer,
encoding: ?string,
callback: ?Function
) {
if (LatticeLogs.failFast(LatticeLogs.LOG)) return
// $FlowFixMe
process.stdout.write(chunk, encoding, callback)
},
errWrite(
chunk: string|Uint8Array|Buffer,
encoding: ?string,
callback: ?Function
) {
if (LatticeLogs.failFast(LatticeLogs.ERROR)) return
// $FlowFixMe
process.stderr.write(chunk, encoding, callback)
}
}