UNPKG

17.7 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.ModuleParser = undefined;
7
8var _toStringTag = require('babel-runtime/core-js/symbol/to-string-tag');
9
10var _toStringTag2 = _interopRequireDefault(_toStringTag);
11
12var _from = require('babel-runtime/core-js/array/from');
13
14var _from2 = _interopRequireDefault(_from);
15
16var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator');
17
18var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2);
19
20var _set = require('babel-runtime/core-js/set');
21
22var _set2 = _interopRequireDefault(_set);
23
24var _map = require('babel-runtime/core-js/map');
25
26var _map2 = _interopRequireDefault(_map);
27
28var _fs = require('fs');
29
30var _fs2 = _interopRequireDefault(_fs);
31
32var _path = require('path');
33
34var _path2 = _interopRequireDefault(_path);
35
36var _neTypes = require('ne-types');
37
38var types = _interopRequireWildcard(_neTypes);
39
40var _GQLBase = require('./GQLBase');
41
42var _GQLJSON = require('./types/GQLJSON');
43
44var _lodash = require('lodash');
45
46var _utils = require('./utils');
47
48function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
49
50function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
51
52// Promisify some bits
53const readdirAsync = (0, _utils.promisify)(_fs2.default.readdir);
54
55const statAsync = (0, _utils.promisify)(_fs2.default.stat);
56
57// Fetch some type checking bits from 'types'
58const {
59 typeOf,
60 isString,
61 isOfType,
62 isPrimitive,
63 isArray,
64 isObject,
65 extendsFrom
66} = types;
67
68/**
69 * The ModuleParser is a utility class designed to loop through and iterate
70 * on a directory and pull out of each .js file found, any classes or exports
71 * that extend from GQLBase or a child of GQLBase.
72 *
73 * @class ModuleParser
74 * @since 2.7.0
75 */
76let ModuleParser = exports.ModuleParser = class ModuleParser {
77
78 /**
79 * The constructor
80 *
81 * @constructor
82 * @method ⎆⠀constructor
83 * @memberof ModuleParser
84 * @inner
85 *
86 * @param {string} directory a string path to a directory containing the
87 * various GQLBase extended classes that should be gathered.
88 */
89
90
91 /**
92 * A boolean value denoting whether or not the `ModuleParser` instance is
93 * valid; i.e. the directory it points to actually exists and is a directory
94 *
95 * @type {boolean}
96 */
97
98
99 /**
100 * A map of skipped items on the last pass and the associated error that
101 * accompanies it.
102 */
103
104 /**
105 * An internal array of `GQLBase` extended classes found during either a
106 * `parse()` or `parseSync()` call.
107 *
108 * @memberof ModuleParser
109 * @type {Array<GQLBase>}
110 */
111 constructor(directory, options = { addLatticeTypes: true }) {
112 Object.defineProperty(this, 'looseGraphQL', {
113 enumerable: true,
114 writable: true,
115 value: []
116 });
117 Object.defineProperty(this, 'options', {
118 enumerable: true,
119 writable: true,
120 value: {}
121 });
122
123 this.directory = _path2.default.resolve(directory);
124 this.classes = [];
125 this.skipped = new _map2.default();
126
127 (0, _lodash.merge)(this.options, options);
128
129 try {
130 this.valid = _fs2.default.statSync(directory).isDirectory();
131 } catch (error) {
132 this.valid = false;
133 }
134 }
135
136 /**
137 * Given a file path, this method will attempt to import/require the
138 * file in question and return the object it exported; whatever that
139 * may be.
140 *
141 * @method ModuleParser#⌾⠀importClass
142 * @since 2.7.0
143 *
144 * @param {string} filePath a path to pass to `require()`
145 *
146 * @return {Object} the object, or undefined, that was returned when
147 * it was `require()`'ed.
148 */
149
150
151 /**
152 * An object, optionally added during construction, that specifies some
153 * configuration about the ModuleParser and how it should do its job.
154 *
155 * Initially, the
156 *
157 * @type {Object}
158 */
159
160
161 /**
162 * A string denoting the directory on disk where `ModuleParser` should be
163 * searching for its classes.
164 *
165 * @memberof ModuleParser
166 * @type {string}
167 */
168
169
170 /**
171 * An array of strings holding loose GraphQL schema documents.
172 *
173 * @memberof ModuleParser
174 * @type {Array<string>}
175 */
176 importClass(filePath) {
177 let moduleContents = {};
178 let yellow = '\x1b[33m';
179 let clear = '\x1b[0m';
180
181 try {
182 moduleContents = require(filePath);
183 } catch (ignore) {
184 if (/\.graphql/i.test(_path2.default.extname(filePath))) {
185 _utils.LatticeLogs.log(`Ingesting .graphql file ${filePath}`);
186 let buffer = _fs2.default.readFileSync(filePath);
187 this.looseGraphQL.push(_fs2.default.readFileSync(filePath).toString());
188 } else {
189 _utils.LatticeLogs.log(`${yellow}Skipping${clear} ${filePath}`);
190 _utils.LatticeLogs.trace(ignore);
191 this.skipped.set(filePath, ignore);
192 }
193 }
194
195 return moduleContents;
196 }
197
198 /**
199 * Given an object, typically the result of a `require()` or `import`
200 * command, iterate over its contents and find any `GQLBase` derived
201 * exports. Continually, and recursively, build this list of classes out
202 * so that we can add them to a `GQLExpressMiddleware`.
203 *
204 * @method ModuleParser#⌾⠀findGQLBaseClasses
205 * @since 2.7.0
206 *
207 * @param {Object} contents the object to parse for properties extending
208 * from `GQLBase`
209 * @param {Array<GQLBase>} gqlDefinitions the results, allowed as a second
210 * parameter during recursion as a means to save state between calls
211 * @return {Set<mixed>} a unique set of values that are currently being
212 * iterated over. Passed in as a third parameter to save state between calls
213 * during recursion.
214 */
215 findGQLBaseClasses(contents, gqlDefinitions = [], stack = new _set2.default()) {
216 // In order to prevent infinite object recursion, we should add the
217 // object being iterated over to our Set. At each new recursive level
218 // add the item being iterated over to the set and only recurse into
219 // if the item does not already exist in the stack itself.
220 stack.add(contents);
221
222 for (let key in contents) {
223 let value = contents[key];
224
225 if (isPrimitive(value)) {
226 continue;
227 }
228
229 if (extendsFrom(value, _GQLBase.GQLBase)) {
230 gqlDefinitions.push(value);
231 }
232
233 if ((isObject(value) || isArray(value)) && !stack.has(value)) {
234 gqlDefinitions = this.findGQLBaseClasses(value, gqlDefinitions, stack);
235 }
236 }
237
238 // We remove the current iterable from our set as we leave this current
239 // recursive iteration.
240 stack.delete(contents);
241
242 return gqlDefinitions;
243 }
244
245 /**
246 * This method takes a instance of ModuleParser, initialized with a directory,
247 * and walks its contents, importing files as they are found, and sorting
248 * any exports that extend from GQLBase into an array of such classes
249 * in a resolved promise.
250 *
251 * @method ModuleParser#⌾⠀parse
252 * @async
253 * @since 2.7.0
254 *
255 * @return {Promise<Array<GQLBase>>} an array GQLBase classes, or an empty
256 * array if none could be identified.
257 */
258 parse() {
259 var _this = this;
260
261 return (0, _asyncToGenerator3.default)(function* () {
262 let modules;
263 let files;
264 let set = new _set2.default();
265 let opts = (0, _utils.getLatticePrefs)();
266
267 if (!_this.valid) {
268 throw new Error(`
269 ModuleParser instance is invalid for use with ${_this.directory}.
270 The path is either a non-existent path or it does not represent a
271 directory.
272 `);
273 }
274
275 _this.skipped.clear();
276
277 // @ComputedType
278 files = yield _this.constructor.walk(_this.directory);
279 modules = files.map(function (file) {
280 return _this.importClass(file);
281 })
282
283 // @ComputedType
284 (modules.map(function (mod) {
285 return _this.findGQLBaseClasses(mod);
286 }).reduce(function (last, cur) {
287 return (last || []).concat(cur || []);
288 }, []).forEach(function (Class) {
289 return set.add(Class);
290 }));
291
292 // Convert the set back into an array
293 _this.classes = (0, _from2.default)(set);
294
295 // We can ignore equality since we came from a set; @ComputedType
296 _this.classes.sort(function (l, r) {
297 return l.name < r.name ? -1 : 1;
298 });
299
300 // Add in any GraphQL Lattice types requested
301 if (_this.options.addLatticeTypes) {
302 _this.classes.push(_GQLJSON.GQLJSON);
303 }
304
305 // Stop flow and throw an error if some files failed to load and settings
306 // declare we should do so. After Lattice 3.x we should expect this to be
307 // the new default
308 if (opts.ModuleParser.failOnError && _this.skipped.size) {
309 _this.printSkipped();
310 throw new Error('Some files skipped due to errors');
311 }
312
313 return _this.classes;
314 })();
315 }
316
317 /**
318 * This method takes a instance of ModuleParser, initialized with a directory,
319 * and walks its contents, importing files as they are found, and sorting
320 * any exports that extend from GQLBase into an array of such classes
321 *
322 * @method ModuleParser#⌾⠀parseSync
323 * @async
324 * @since 2.7.0
325 *
326 * @return {Array<GQLBase>} an array GQLBase classes, or an empty
327 * array if none could be identified.
328 */
329 parseSync() {
330 let modules;
331 let files;
332 let set = new _set2.default();
333 let opts = (0, _utils.getLatticePrefs)();
334
335 if (!this.valid) {
336 throw new Error(`
337 ModuleParser instance is invalid for use with ${this.directory}.
338 The path is either a non-existent path or it does not represent a
339 directory.
340 `);
341 }
342
343 this.skipped.clear();
344
345 files = this.constructor.walkSync(this.directory);
346 modules = files.map(file => {
347 return this.importClass(file);
348 });
349
350 modules.map(mod => this.findGQLBaseClasses(mod)).reduce((last, cur) => (last || []).concat(cur || []), []).forEach(Class => set.add(Class));
351
352 // Convert the set back into an array
353 this.classes = (0, _from2.default)(set);
354
355 // We can ignore equality since we came from a set; @ComputedType
356 this.classes.sort((l, r) => l.name < r.name ? -1 : 1);
357
358 // Add in any GraphQL Lattice types requested
359 if (this.options.addLatticeTypes) {
360 this.classes.push(_GQLJSON.GQLJSON);
361 }
362
363 // Stop flow and throw an error if some files failed to load and settings
364 // declare we should do so. After Lattice 3.x we should expect this to be
365 // the new default
366 if (opts.ModuleParser.failOnError && this.skipped.size) {
367 this.printSkipped();
368 throw new Error('Some files skipped due to errors');
369 }
370
371 return this.classes;
372 }
373
374 /**
375 * Prints the list of skipped files, their stack traces, and the errors
376 * denoting the reasons the files were skipped.
377 */
378 printSkipped() {
379 if (this.skipped.size) {
380 _utils.LatticeLogs.outWrite('\x1b[1;91m');
381 _utils.LatticeLogs.outWrite('Skipped\x1b[0;31m the following files\n');
382
383 for (let [key, value] of this.skipped) {
384 _utils.LatticeLogs.log(`${_path2.default.basename(key)}: ${value.message}`);
385 if (value.stack) _utils.LatticeLogs.log(value.stack.replace(/(^)/m, '$1 '));
386 }
387
388 _utils.LatticeLogs.outWrite('\x1b[0m');
389 } else {
390 _utils.LatticeLogs.log('\x1b[1;32mNo files skipped\x1b[0m');
391 }
392 }
393
394 /**
395 * Returns the `constructor` name. If invoked as the context, or `this`,
396 * object of the `toString` method of `Object`'s `prototype`, the resulting
397 * value will be `[object MyClass]`, given an instance of `MyClass`
398 *
399 * @method ⌾⠀[Symbol.toStringTag]
400 * @memberof ModuleParser
401 *
402 * @return {string} the name of the class this is an instance of
403 * @ComputedType
404 */
405 get [_toStringTag2.default]() {
406 return this.constructor.name;
407 }
408
409 /**
410 * Applies the same logic as {@link #[Symbol.toStringTag]} but on a static
411 * scale. So, if you perform `Object.prototype.toString.call(MyClass)`
412 * the result would be `[object MyClass]`.
413 *
414 * @method ⌾⠀[Symbol.toStringTag]
415 * @memberof ModuleParser
416 * @static
417 *
418 * @return {string} the name of this class
419 * @ComputedType
420 */
421 static get [_toStringTag2.default]() {
422 return this.name;
423 }
424
425 /**
426 * Recursively walks a directory and returns an array of asbolute file paths
427 * to the files under the specified directory.
428 *
429 * @method ModuleParser~⌾⠀walk
430 * @async
431 * @since 2.7.0
432 *
433 * @param {string} dir string path to the top level directory to parse
434 * @param {Array<string>} filelist an array of existing absolute file paths,
435 * or if not parameter is supplied a default empty array will be used.
436 * @return {Promise<Array<string>>} an array of existing absolute file paths
437 * found under the supplied `dir` directory.
438 */
439 static walk(dir, filelist = [], extensions = ['.js', '.jsx', '.ts', '.tsx']) {
440 var _this2 = this;
441
442 return (0, _asyncToGenerator3.default)(function* () {
443 let files = yield readdirAsync(dir);
444 let exts = ModuleParser.checkForPackageExtensions() || extensions;
445 let pattern = ModuleParser.arrayToPattern(exts);
446 let stats;
447
448 files = files.map(function (file) {
449 return _path2.default.resolve(_path2.default.join(dir, file));
450 });
451
452 for (let file of files) {
453 stats = yield statAsync(file);
454 if (stats.isDirectory()) {
455 filelist = yield _this2.walk(file, filelist);
456 } else {
457 if (pattern.test(_path2.default.extname(file))) filelist = filelist.concat(file);
458 }
459 }
460
461 return filelist;
462 })();
463 }
464
465 /**
466 * Recursively walks a directory and returns an array of asbolute file paths
467 * to the files under the specified directory. This version does this in a
468 * synchronous fashion.
469 *
470 * @method ModuleParser~⌾⠀walkSync
471 * @async
472 * @since 2.7.0
473 *
474 * @param {string} dir string path to the top level directory to parse
475 * @param {Array<string>} filelist an array of existing absolute file paths,
476 * or if not parameter is supplied a default empty array will be used.
477 * @return {Array<string>} an array of existing absolute file paths found
478 * under the supplied `dir` directory.
479 */
480 static walkSync(dir, filelist = [], extensions = ['.js', '.jsx', '.ts', '.tsx']) {
481 let files = (0, _fs.readdirSync)(dir);
482 let exts = ModuleParser.checkForPackageExtensions() || extensions;
483 let pattern = ModuleParser.arrayToPattern(exts);
484 let stats;
485
486 files = files.map(file => _path2.default.resolve(_path2.default.join(dir, file)));
487
488 for (let file of files) {
489 stats = (0, _fs.statSync)(file);
490 if (stats.isDirectory()) {
491 filelist = this.walkSync(file, filelist);
492 } else {
493 if (pattern.test(_path2.default.extname(file))) filelist = filelist.concat(file);
494 }
495 }
496
497 return filelist;
498 }
499
500 /**
501 * The ModuleParser should only parse files that match the default or
502 * supplied file extensions. The default list contains .js, .jsx, .ts
503 * and .tsx; so JavaScript or TypeScript files and their JSX React
504 * counterparts
505 *
506 * Since the list is customizable for a usage, however, it makes sense
507 * to have a function that will match what is supplied rather than
508 * creating a constant expression to use instead.
509 *
510 * @static
511 * @memberof ModuleParser
512 * @function ⌾⠀arrayToPattern
513 * @since 2.13.0
514 *
515 * @param {Array<string>} extensions an array of extensions to
516 * convert to a regular expression that would pass for each
517 * @param {string} flags the value passed to a new RegExp denoting the
518 * flags used in the pattern; defaults to 'i' for case insensitivity
519 * @return {RegExp} a regular expression object matching the contents
520 * of the array of extensions or the default extensions and that will
521 * also match those values in a case insensitive manner
522 */
523 static arrayToPattern(extensions = ['.js', '.jsx', '.ts', '.tsx'], flags = 'i') {
524 return new RegExp(extensions.join('|').replace(/\./g, '\\.').replace(/([\|$])/g, '\\b$1'), flags);
525 }
526
527 /**
528 * Using the module `read-pkg-up`, finds the nearest package.json file
529 * and checks to see if it has a `.lattice.moduleParser.extensions'
530 * preference. If so, if the value is an array, that value is used,
531 * otherwise the value is wrapped in an array. If the optional parameter
532 * `toString` is `true` then `.toString()` will be invoked on any non
533 * Array values found; this behavior is the default
534 *
535 * @static
536 * @memberof ModuleParser
537 * @method ⌾⠀checkForPackageExtensions
538 * @since 2.13.0
539 *
540 * @param {boolean} toString true if any non-array values should have
541 * their `.toString()` method invoked before being wrapped in an Array;
542 * defaults to true
543 * @return {?Array<string>} null if no value is set for the property
544 * `lattice.ModuleParser.extensions` in `package.json` or the value
545 * of the setting if it is an array. Finally if the value is set but is
546 * not an array, the specified value wrapped in an array is returned
547 */
548 static checkForPackageExtensions(toString = true) {
549 let pkg = (0, _utils.getLatticePrefs)();
550 let extensions = null;
551
552 if (pkg.ModuleParser && pkg.ModuleParser.extensions) {
553 let packageExts = pkg.ModuleParser.extensions;
554
555 if (Array.isArray(packageExts)) {
556 extensions = packageExts;
557 } else {
558 extensions = [toString ? packageExts.toString() : packageExts];
559 }
560 }
561
562 return extensions;
563 }
564};
565exports.default = ModuleParser;
566//# sourceMappingURL=ModuleParser.js.map
\No newline at end of file