1 | ;
|
2 |
|
3 | Object.defineProperty(exports, "__esModule", {
|
4 | value: true
|
5 | });
|
6 | exports.ModuleParser = undefined;
|
7 |
|
8 | var _toStringTag = require('babel-runtime/core-js/symbol/to-string-tag');
|
9 |
|
10 | var _toStringTag2 = _interopRequireDefault(_toStringTag);
|
11 |
|
12 | var _from = require('babel-runtime/core-js/array/from');
|
13 |
|
14 | var _from2 = _interopRequireDefault(_from);
|
15 |
|
16 | var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator');
|
17 |
|
18 | var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2);
|
19 |
|
20 | var _set = require('babel-runtime/core-js/set');
|
21 |
|
22 | var _set2 = _interopRequireDefault(_set);
|
23 |
|
24 | var _map = require('babel-runtime/core-js/map');
|
25 |
|
26 | var _map2 = _interopRequireDefault(_map);
|
27 |
|
28 | var _fs = require('fs');
|
29 |
|
30 | var _fs2 = _interopRequireDefault(_fs);
|
31 |
|
32 | var _path = require('path');
|
33 |
|
34 | var _path2 = _interopRequireDefault(_path);
|
35 |
|
36 | var _neTypes = require('ne-types');
|
37 |
|
38 | var types = _interopRequireWildcard(_neTypes);
|
39 |
|
40 | var _GQLBase = require('./GQLBase');
|
41 |
|
42 | var _GQLJSON = require('./types/GQLJSON');
|
43 |
|
44 | var _lodash = require('lodash');
|
45 |
|
46 | var _utils = require('./utils');
|
47 |
|
48 | function _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 |
|
50 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
51 |
|
52 | // Promisify some bits
|
53 | const readdirAsync = (0, _utils.promisify)(_fs2.default.readdir);
|
54 |
|
55 | const statAsync = (0, _utils.promisify)(_fs2.default.stat);
|
56 |
|
57 | // Fetch some type checking bits from 'types'
|
58 | const {
|
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 | */
|
76 | let 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 | };
|
565 | exports.default = ModuleParser;
|
566 | //# sourceMappingURL=ModuleParser.js.map |
\ | No newline at end of file |