UNPKG

9.92 kBJavaScriptView Raw
1/** @namespace utils */
2// @flow
3
4import fs from 'fs'
5import util from 'util'
6import { typeOf } from 'ne-types'
7import { sync as readPkg } from 'read-pkg-up'
8import { merge } from 'lodash'
9
10export { dedent as joinLines } from 'ne-tag-fns'
11
12const { Stats } = fs;
13
14/**
15 * Deferred is modeled after jQuery's deferred object. It inverts a promise
16 * such that its resolve and reject methods can be invoked without wrapping
17 * all of the related code within a Promise's function.
18 *
19 * @memberof utils
20 * @class Deferred
21 */
22export class Deferred {
23 /**
24 * This property holds a `resolve` function from within the promise this
25 * deferred inverts.
26 *
27 * @type {Function}
28 * @memberof Deferred
29 * @instance
30 */
31 resolve: Function;
32
33 /**
34 * This property holds a `reject` function from within the promise this
35 * deferred inverts
36 *
37 * @type {Function}
38 * @memberof Deferred
39 * @instance
40 */
41 reject: Function;
42
43 /**
44 * This is the promise wrapped by and inverted in this deferred instance
45 *
46 * @type {Promise}
47 * @memberof Deferred
48 * @instance
49 */
50 promise: any;
51
52 /**
53 * An at a glance boolean property that denotes whether or not this
54 * deferred has been resolved or rejected yet.
55 *
56 * @type {boolean}
57 * @memberof Deferred
58 * @instance
59 */
60 complete: boolean;
61
62 /**
63 * Creates an object with four properties of note; promise, resolve, reject
64 * and a flag complete that will be set once either resolve or reject have
65 * been called. A Deferred is considered to be pending while complete is set
66 * to false.
67 *
68 * Once constructed, resolve and reject can be called later, at which point,
69 * the promise is completed. The promise property is the promise resolved
70 * or rejected by the associated properties and can be used with other
71 * async/await or Promise based code.
72 *
73 * @instance
74 * @memberof Deferred
75 * @method ⎆⠀constructor
76 *
77 * @param {any} resolveWith a deferred resolved as Promise.resolve() might do
78 * @param {any} rejectWith a deferred rejected as Promise.reject() might do
79 */
80 constructor(resolveWith: any, rejectWith: any) {
81 this.promise = new Promise((resolve, reject) => {
82 this.complete = false;
83
84 this.resolve = (...args) => {
85 this.complete = true;
86 return resolve(...args);
87 };
88
89 this.reject = (...args) => {
90 this.complete = true;
91 return reject(...args);
92 };
93
94 if (resolveWith && !rejectWith) { this.resolve(resolveWith) }
95 if (rejectWith && !resolveWith) { this.reject(rejectWith) }
96 });
97 }
98
99 /**
100 * Shorthand getter that denotes true if the deferred is not yet complete.
101 *
102 * @instance
103 * @memberof Deferred
104 * @method ⬇︎⠀pending
105 *
106 * @return {boolean} true if the promise is not yet complete; false otherwise
107 */
108 get pending(): boolean { return !this.complete }
109
110 /**
111 * Promises are great but if the code never resolves or rejects a deferred,
112 * then things will become eternal; in a bad way. This makes that less likely
113 * of an event.
114 *
115 * If the number of milliseconds elapses before a resolve or reject occur,
116 * then the deferred is rejected.
117 *
118 * @static
119 * @memberof Deferred
120 * @method ⌾⠀TimedDeferred
121 *
122 * @param {Number} timeOut a number of milliseconds to wait before rejecting
123 * the deferred.
124 * @param {Promise} proxyPromise a promise to proxy then/catch through to the
125 * deferreds resolve/reject.
126 * @return {Deferred} an instance of deferred that will timeout after
127 * `timeOut` milliseconds have elapsed. If `proxyPromise` is a `Promise`
128 * then the deferred's reject and resolve will be tied to the Promise's
129 * catch() and then() methods, respectively.
130 */
131 static TimedDeferred(timeOut: Number, proxyPromise: ?any): Deferred {
132 const deferred = new Deferred();
133
134 if (proxyPromise && typeOf(proxyPromise) === Promise.name) {
135 proxyPromise.then((...args) => deferred.resolve(...args))
136 proxyPromise.catch(reason => deferred.reject(reason))
137 }
138
139 setTimeout(() => deferred.reject(new Error('Deferred timed out'), timeOut))
140
141 return deferred;
142 }
143}
144
145/**
146 * A simply promisify style function that returns an async function wrapped
147 * around a supplied function designed for the standard callback methodology.
148 * If the callback is the last parameter, and that callback is in the form of
149 * (error, ...results) then this wrapper will do the trick for you.
150 *
151 * @method utils~⌾⠀promisify
152 * @since 2.7.0
153 *
154 * @param {Function} method a function to wrap in an asynchronous function
155 * @param {mixed} context an optional `this` object for use with the supplied
156 * function.
157 * @return {Function} an asynchronous function, i.e. one that returns a promise
158 * containing the contents the callback results, that wraps the supplied
159 * function.
160 */
161export function promisify(method: Function, context?: mixed): Function {
162 return async function(...args) {
163 return new Promise((resolve, reject) => {
164 args.push(function(error, ...callbackArgs) {
165 if (error) {
166 reject(error);
167 }
168 else {
169 resolve(...callbackArgs);
170 }
171 });
172
173 method.apply(context, args);
174 })
175 }
176}
177
178/**
179 * It may be necessary to read GraphQL Lattice preferences from the nearest
180 * `package.json` object to the excuting code. `getLatticePrefs()` does this
181 * and merges any subsequently found options in said file on top of the
182 * default values specified here in this file.
183 *
184 * @method utils~⌾⠀getLatticePrefs
185 * @since 2.13.0
186 *
187 * @return {Object} an object containing at least the defaults plus any other
188 * values specified in `package.json`
189 */
190export function getLatticePrefs(readPkgUpOpts: ?Object): Object {
191 let { pkg } = readPkg(readPkgUpOpts)
192 let options = {
193 ModuleParser: {
194 extensions: ['.js', '.jsx', '.ts', '.tsx'],
195 failOnError: false
196 }
197 }
198
199 if (pkg.lattice) {
200 merge(options, pkg.lattice || {})
201 }
202
203 return options;
204}
205
206/**
207 * A small near pass-thru facility for logging within Lattice such that error
208 * objects supplied get mapped to their message unless `LATTICE_ERRORS=STACK`
209 * is set in `process.env`.
210 *
211 * Note the order of log levels for Lattice may be somewhat non-standard. Info
212 * has been taken out of flow and placed above error to solve issues with jest
213 * logging.
214 *
215 * @memberof utils
216 * @type Object
217 * @static
218 */
219export const LatticeLogs = {
220 get LOG(): string { return 'log' },
221
222 get WARN(): string { return 'warn' },
223
224 get ERROR(): string { return 'error' },
225
226 get INFO(): string { return 'info' },
227
228 get TRACE(): string { return 'trace' },
229
230 /**
231 * Ordering of log levels for LatticeLogs. `INFO` is a non error log level
232 * that is non-crucial and appears if LATTICE_LOGLEVEL is set to `INFO` or
233 * `TRACE`
234 */
235 get LEVELS(): Array<string> {
236 const ll = LatticeLogs
237
238 return [ll.LOG, ll.WARN, ll.ERROR, ll.INFO, ll.TRACE]
239 },
240
241 equalOrBelow(testedLevel: string, lessThan: string = 'error') {
242 const ll = LatticeLogs
243
244 return ll.LEVELS.indexOf(testedLevel) <= ll.LEVELS.indexOf(lessThan)
245 },
246
247 atLeast(testedLevel: string, atLeastLevel: string): boolean {
248 const ll = LatticeLogs
249
250 return ll.LEVELS.indexOf(testedLevel) >= ll.LEVELS.indexOf(atLeastLevel)
251 },
252
253 /**
254 * All arguments of any logging function in `LatticeLogs` get passed through
255 * this function first to modify or alter the type of value being logged.
256 *
257 * @param {mixed} arg the argument being passed to the `map()` function
258 * @param {number} index the index in the array of arguments
259 * @param {Array<mixed>} array the array containing this element
260 */
261 argMapper(arg: mixed, index: number, array: Array<mixed>): mixed {
262 let isError = typeOf(arg) === Error.name
263 let showStack = /\bSTACK\b/i.test(process.env.LATTICE_ERRORS || '')
264
265 // $FlowFixMe
266 return !isError ? arg : (showStack ? arg : arg.message)
267 },
268
269 /** A function that, when it returns true, will cause logging to be skipped */
270 failFast(logLevel: ?string, lessThan: ?string) {
271 const ll = LatticeLogs
272
273 if (logLevel) {
274 let compareTo = lessThan || process.env.LATTICE_LOGLEVEL || ll.ERROR
275 if (!ll.equalOrBelow(logLevel, compareTo)) return true
276 }
277
278 return /\b(NONE|OFF|NO|0)\b/i.test(process.env.LATTICE_ERRORS || '')
279 },
280
281 /** Pass-thru to console.log; arguments parsed via `argMapper` */
282 log(...args: Array<mixed>) {
283 if (LatticeLogs.failFast(LatticeLogs.LOG)) return;
284 console.log(...args.map(LatticeLogs.argMapper))
285 },
286
287 /** Pass-thru to console.warn; arguments parsed via `argMapper` */
288 warn(...args: Array<mixed>) {
289 if (LatticeLogs.failFast(LatticeLogs.WARN)) return;
290 console.warn(...args.map(LatticeLogs.argMapper))
291 },
292
293 /** Pass-thru to console.error; arguments parsed via `argMapper` */
294 error(...args: Array<mixed>) {
295 if (LatticeLogs.failFast(LatticeLogs.ERROR)) return;
296 console.error(...args.map(LatticeLogs.argMapper))
297 },
298
299 /** Pass-thru to console.info; arguments parsed via `argMapper` */
300 info(...args: Array<mixed>) {
301 if (LatticeLogs.failFast(LatticeLogs.INFO)) return;
302 console.info(...args.map(LatticeLogs.argMapper))
303 },
304
305 /** Pass-thru to console.trace; arguments parsed via `argMapper` */
306 trace(...args: Array<mixed>) {
307 if (LatticeLogs.failFast(LatticeLogs.TRACE)) return;
308 console.trace(...args.map(LatticeLogs.argMapper))
309 },
310
311 outWrite(
312 chunk: string|Uint8Array|Buffer,
313 encoding: ?string,
314 callback: ?Function
315 ) {
316 if (LatticeLogs.failFast(LatticeLogs.LOG)) return
317 // $FlowFixMe
318 process.stdout.write(chunk, encoding, callback)
319 },
320
321 errWrite(
322 chunk: string|Uint8Array|Buffer,
323 encoding: ?string,
324 callback: ?Function
325 ) {
326 if (LatticeLogs.failFast(LatticeLogs.ERROR)) return
327 // $FlowFixMe
328 process.stderr.write(chunk, encoding, callback)
329 }
330}