@stackbit/cms-core 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,997 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const fse = require('fs-extra');
4
+ const glob = require('glob');
5
+ const _ = require('lodash');
6
+ const yaml = require('js-yaml');
7
+ const toml = require('@iarna/toml');
8
+ const chalk = require('chalk');
9
+
10
+ module.exports = {
11
+ forEachPromise,
12
+ mapPromise,
13
+ reducePromise,
14
+ findPromise,
15
+ promiseAllMap,
16
+ copyFilesRecursively,
17
+ copy,
18
+ copyDefault,
19
+ mergeAtPath,
20
+ omitByNil,
21
+ rename,
22
+ append,
23
+ concat,
24
+ indent,
25
+ pascalCase,
26
+ readDirRec,
27
+ readDirRecSync,
28
+ readDirGlob,
29
+ fieldPathToString,
30
+ hrtimeAndPrint,
31
+ printHRTime,
32
+ forEachDeep,
33
+ mapDeep,
34
+ getFirst,
35
+ getFirstExistingFile,
36
+ parseFirstExistingFile,
37
+ getFirstExistingFileSync,
38
+ parseFirstExistingFileSync,
39
+ parseFile,
40
+ parseFileSync,
41
+ parseDataByFilePath,
42
+ outputData,
43
+ outputDataSync,
44
+ outputDataIfNeeded,
45
+ stringifyDataByFilePath,
46
+ parseMarkdownWithFrontMatter,
47
+ deepFreeze,
48
+ failFunctionWithTag,
49
+ assertFunctionWithFail,
50
+ createLogger,
51
+ logObject,
52
+ joinPathAndGlob,
53
+ globToArray,
54
+ fromPath,
55
+ omitDeep,
56
+ dataReducerSync,
57
+ dataReducer,
58
+ encodeJsx,
59
+ decodeJsx,
60
+ replaceInRange,
61
+ isRelevantReactData
62
+ };
63
+
64
+ const INDENT = _.repeat(' ', 4);
65
+
66
+ /**
67
+ * Iterates over array items and invokes callback function for each of them.
68
+ * The callback must return a promise and is called with three parameters: array item,
69
+ * item index, array itself. Callbacks are invoked serially, such that callback for the
70
+ * following item will not be called until the promise returned from the previous callback
71
+ * is not fulfilled.
72
+ *
73
+ * @param {array} array
74
+ * @param {function} callback
75
+ * @param {object} [thisArg]
76
+ * @return {Promise<any>}
77
+ */
78
+ function forEachPromise(array, callback, thisArg) {
79
+ return new Promise((resolve, reject) => {
80
+ function next(index) {
81
+ if (index < array.length) {
82
+ callback
83
+ .call(thisArg, array[index], index, array)
84
+ .then(() => {
85
+ next(index + 1);
86
+ })
87
+ .catch((error) => {
88
+ reject(error);
89
+ });
90
+ } else {
91
+ resolve();
92
+ }
93
+ }
94
+ next(0);
95
+ });
96
+ }
97
+
98
+ function mapPromise(array, callback, thisArg) {
99
+ return new Promise((resolve, reject) => {
100
+ let results = [];
101
+
102
+ function next(index) {
103
+ if (index < array.length) {
104
+ callback
105
+ .call(thisArg, array[index], index, array)
106
+ .then((result) => {
107
+ results[index] = result;
108
+ next(index + 1);
109
+ })
110
+ .catch((error) => {
111
+ reject(error);
112
+ });
113
+ } else {
114
+ resolve(results);
115
+ }
116
+ }
117
+
118
+ next(0);
119
+ });
120
+ }
121
+
122
+ function reducePromise(array, callback, initValue, thisArg) {
123
+ return new Promise((resolve, reject) => {
124
+ function next(index, accumulator) {
125
+ if (index < array.length) {
126
+ callback
127
+ .call(thisArg, accumulator, array[index], index, array)
128
+ .then((accumulator) => {
129
+ next(index + 1, accumulator);
130
+ })
131
+ .catch((error) => {
132
+ reject(error);
133
+ });
134
+ } else {
135
+ resolve(accumulator);
136
+ }
137
+ }
138
+
139
+ next(0, initValue);
140
+ });
141
+ }
142
+
143
+ function reduceObjectPromise(object, callback, initValue, thisArg) {
144
+ return new Promise((resolve, reject) => {
145
+ const keys = _.keys(object);
146
+ function next(index, accumulator) {
147
+ if (index < keys.length) {
148
+ const key = keys[index];
149
+ callback
150
+ .call(thisArg, accumulator, object[key], key, object)
151
+ .then((accumulator) => {
152
+ next(index + 1, accumulator);
153
+ })
154
+ .catch((error) => {
155
+ reject(error);
156
+ });
157
+ } else {
158
+ resolve(accumulator);
159
+ }
160
+ }
161
+
162
+ next(0, initValue);
163
+ });
164
+ }
165
+
166
+ function findPromise(array, callback, thisArg) {
167
+ return new Promise((resolve, reject) => {
168
+ function next(index) {
169
+ if (index < array.length) {
170
+ const item = array[index];
171
+ callback
172
+ .call(thisArg, item, index, array)
173
+ .then((result) => {
174
+ if (result) {
175
+ resolve(item);
176
+ } else {
177
+ next(index + 1);
178
+ }
179
+ })
180
+ .catch((error) => {
181
+ reject(error);
182
+ });
183
+ } else {
184
+ resolve();
185
+ }
186
+ }
187
+
188
+ next(0);
189
+ });
190
+ }
191
+
192
+ function promiseAllMap(array, limit, interval, callback, thisArg) {
193
+ return new Promise((resolve, reject) => {
194
+ const arrayCopy = array.slice();
195
+ const results = [];
196
+ let index = 0;
197
+ let runCount = 0;
198
+ let doneCount = 0;
199
+ let lastRunTime = null;
200
+ let timeout = null;
201
+ limit = limit || null;
202
+ interval = interval || null;
203
+
204
+ function run() {
205
+ let idx = index;
206
+ if (timeout) {
207
+ clearTimeout(timeout);
208
+ timeout = null;
209
+ }
210
+ index += 1;
211
+ runCount += 1;
212
+ lastRunTime = process.hrtime();
213
+ Promise.resolve(callback.call(thisArg, arrayCopy[idx], idx, arrayCopy))
214
+ .then((result) => {
215
+ runCount -= 1;
216
+ doneCount += 1;
217
+ results.push(result);
218
+ next();
219
+ })
220
+ .catch((error) => {
221
+ reject(error);
222
+ });
223
+ next();
224
+ }
225
+
226
+ if (interval) {
227
+ let origRun = run;
228
+ run = function () {
229
+ if (!lastRunTime) {
230
+ origRun();
231
+ } else if (!timeout) {
232
+ let diff = process.hrtime(lastRunTime);
233
+ let diffMs = diff[0] * 1000 + diff[1] / 1000000;
234
+ if (diffMs >= interval) {
235
+ origRun();
236
+ } else {
237
+ timeout = setTimeout(origRun, interval - diffMs);
238
+ }
239
+ }
240
+ };
241
+ }
242
+
243
+ if (limit) {
244
+ let origRun = run;
245
+ run = function () {
246
+ if (runCount < limit) {
247
+ origRun();
248
+ }
249
+ };
250
+ }
251
+
252
+ function next() {
253
+ if (index < arrayCopy.length) {
254
+ run();
255
+ } else if (doneCount === arrayCopy.length) {
256
+ resolve(results);
257
+ }
258
+ }
259
+
260
+ next();
261
+ });
262
+ }
263
+
264
+ /**
265
+ * Recursively copies files from source to target directories.
266
+ * The optional "options" argument is an object with an optional "processNunjucksFile"
267
+ * and "filePathMap" fields.
268
+ *
269
+ * If "processNunjucksFile" function is passed, it will be invoked for every file with ".njk"
270
+ * extension with a filepath relative to the sourceDir as its single argument.
271
+ * This function should return the result of processing Nunjucks template.
272
+ *
273
+ * Files named _gitignore will be copied as .gitignore
274
+ *
275
+ * @param {string} sourceDir
276
+ * @param {string} targetDir
277
+ * @param {object} [options]
278
+ * @param {Function} options.processNunjucksFile Function that receives filePath
279
+ * relative to sourceDir and returns processed file data to be stored inside targetDir
280
+ * @param {object} options.filePathMap Map between source and target file paths.
281
+ * If mapped value is null, the file will not be copied.
282
+ */
283
+ function copyFilesRecursively(sourceDir, targetDir, options, _internalOptions) {
284
+ if (!_internalOptions) {
285
+ _internalOptions = {
286
+ origSourceDir: sourceDir,
287
+ origTargetDir: targetDir
288
+ };
289
+ }
290
+
291
+ fs.readdirSync(sourceDir).forEach((fileName) => {
292
+ let sourceFilePath = path.join(sourceDir, fileName);
293
+ let targetFilePath = path.join(targetDir, fileName);
294
+ let fileStat = fs.statSync(sourceFilePath);
295
+
296
+ if (fileStat.isDirectory()) {
297
+ copyFilesRecursively(sourceFilePath, targetFilePath, options, _internalOptions);
298
+ } else if (fileStat.isFile()) {
299
+ let outputPathObject = path.parse(targetFilePath);
300
+ let data = null;
301
+ if (outputPathObject.ext === '.njk') {
302
+ if (!_.has(options, 'processNunjucksFile') || !_.isFunction(options.processNunjucksFile)) {
303
+ throw new Error(
304
+ `utils.copyFilesRecursively(): file (${sourceFilePath}) has '.njk' extension but processNunjucksFile function was not passed`
305
+ );
306
+ }
307
+ let relativeSourceFilePath = path.relative(_internalOptions.origSourceDir, sourceFilePath);
308
+ data = options.processNunjucksFile(relativeSourceFilePath);
309
+ targetFilePath = path.resolve(outputPathObject.dir, outputPathObject.name);
310
+ }
311
+ let relativeTargetFilePath = path.relative(_internalOptions.origTargetDir, targetFilePath);
312
+ if (_.has(options, ['filePathMap', relativeTargetFilePath])) {
313
+ let mappedFilePath = _.get(options, ['filePathMap', relativeTargetFilePath]);
314
+ if (mappedFilePath === null) {
315
+ return;
316
+ }
317
+ targetFilePath = path.join(_internalOptions.origTargetDir, mappedFilePath);
318
+ }
319
+ if (fileName === '_gitignore') {
320
+ targetFilePath = path.resolve(outputPathObject.dir, '.gitignore');
321
+ }
322
+ if (!data) {
323
+ fse.copySync(sourceFilePath, targetFilePath);
324
+ } else {
325
+ fse.outputFileSync(targetFilePath, data, { mode: fileStat.mode });
326
+ }
327
+ } else {
328
+ throw new Error(`utils.copyFilesRecursively(): file type is not supported: ${sourceFilePath}`);
329
+ }
330
+ });
331
+ }
332
+
333
+ /**
334
+ * Copies the value at a sourcePath of the sourceObject to a targetPath of the targetObject.
335
+ *
336
+ * @param {Object} sourceObject
337
+ * @param {String} sourcePath
338
+ * @param {Object} targetObject
339
+ * @param {String} targetPath
340
+ * @param {Function} [transform]
341
+ */
342
+ function copy(sourceObject, sourcePath, targetObject, targetPath, transform) {
343
+ if (_.has(sourceObject, sourcePath)) {
344
+ let value = _.get(sourceObject, sourcePath);
345
+ if (transform) {
346
+ value = transform(value);
347
+ }
348
+ _.set(targetObject, targetPath, value);
349
+ }
350
+ }
351
+
352
+ function copyDefault(sourceObject, sourcePath, targetObject, targetPath, transform) {
353
+ if (!_.has(targetObject, targetPath)) {
354
+ copy(sourceObject, sourcePath, targetObject, targetPath, transform);
355
+ }
356
+ }
357
+
358
+ function mergeAtPath(object, path, source) {
359
+ // First get the existing object at path, merge it with source and then set
360
+ // the merged object back. This ensures that if path has number like field
361
+ // names while the original object has objects at this path, it will not
362
+ // override the object with array
363
+ const nodeAtPath = _.get(object, path);
364
+ const merged = _.merge(nodeAtPath, source);
365
+ return _.set(object, path, merged);
366
+ }
367
+
368
+ function omitByNil(object) {
369
+ return _.omitBy(object, _.isNil);
370
+ }
371
+
372
+ function rename(object, oldPath, newPath) {
373
+ if (_.has(object, oldPath)) {
374
+ _.set(object, newPath, _.get(object, oldPath));
375
+ oldPath = _.toPath(oldPath);
376
+ if (oldPath.length > 1) {
377
+ object = _.get(object, _.initial(oldPath));
378
+ }
379
+ delete object[_.last(oldPath)];
380
+ }
381
+ }
382
+
383
+ function append(object, path, value) {
384
+ if (!_.has(object, path)) {
385
+ _.set(object, path, []);
386
+ }
387
+ _.get(object, path).push(value);
388
+ }
389
+
390
+ function concat(object, path, value) {
391
+ if (!_.has(object, path)) {
392
+ _.set(object, path, []);
393
+ }
394
+ _.set(object, path, _.get(object, path).concat(value));
395
+ }
396
+
397
+ function indent(str, indent, indentFirst = false) {
398
+ if (_.isNumber(indent)) {
399
+ indent = _.repeat(INDENT, indent);
400
+ }
401
+ return (indentFirst ? indent : '') + str.split('\n').join(`\n${indent}`);
402
+ }
403
+
404
+ function pascalCase(str) {
405
+ return _.upperFirst(_.camelCase(str));
406
+ }
407
+
408
+ async function readDirRec(dir, options) {
409
+ const dirExists = await fse.pathExists(dir);
410
+ if (!dirExists) {
411
+ return [];
412
+ }
413
+ const rootDir = _.get(options, 'rootDir', dir);
414
+ const files = await fse.readdir(dir);
415
+ const result = await mapPromise(files, async (file) => {
416
+ const filePath = path.join(dir, file);
417
+ const relFilePath = path.relative(rootDir, filePath);
418
+ const stats = await fse.stat(filePath);
419
+ if (_.has(options, 'filter') && !options.filter(relFilePath, stats)) {
420
+ return Promise.resolve();
421
+ }
422
+ if (stats.isDirectory()) {
423
+ return readDirRec(filePath, { ...options, rootDir });
424
+ } else if (stats.isFile()) {
425
+ return relFilePath;
426
+ } else {
427
+ return null;
428
+ }
429
+ });
430
+ return _.chain(result).compact().flatten().value();
431
+ }
432
+
433
+ function readDirGlob(dir, options) {
434
+ let dirPattern = dir;
435
+ // default to listing all files recursively
436
+ if (!glob.hasMagic(dirPattern)) {
437
+ dirPattern = path.join(dir, '**/*');
438
+ }
439
+ return new Promise((resolve, reject) => {
440
+ glob(dirPattern, _.get(options, 'globOptions', {}), (err, files) => {
441
+ if (!_.isEmpty(err)) {
442
+ return reject(err);
443
+ }
444
+ resolve(
445
+ _.compact(
446
+ files.map((file) => {
447
+ if (_.has(options, 'filter') && !options.filter(file)) {
448
+ return null;
449
+ }
450
+ return file;
451
+ })
452
+ )
453
+ );
454
+ });
455
+ });
456
+ }
457
+
458
+ function readDirRecSync(dir, options) {
459
+ let list = [];
460
+ const dirExists = fse.pathExistsSync(dir);
461
+ if (!dirExists) {
462
+ return list;
463
+ }
464
+ const files = fs.readdirSync(dir);
465
+ _.forEach(files, (file) => {
466
+ const filePath = path.join(dir, file);
467
+ if (_.has(options, 'filter') && !options.filter(filePath)) {
468
+ return;
469
+ }
470
+ const stats = fs.statSync(filePath);
471
+ if (stats.isDirectory()) {
472
+ list = list.concat(readDirRecSync(filePath, options));
473
+ } else if (stats.isFile()) {
474
+ list.push(filePath);
475
+ }
476
+ });
477
+ return list;
478
+ }
479
+
480
+ function fieldPathToString(fieldPath) {
481
+ return _.reduce(
482
+ fieldPath,
483
+ (accumulator, fieldName, index) => {
484
+ if (_.isString(fieldName) && /\W/.test(fieldName)) {
485
+ // field name is a string with non alphanumeric character
486
+ accumulator += `['${fieldName}']`;
487
+ } else if (_.isNumber(fieldName)) {
488
+ accumulator += `[${fieldName}]`;
489
+ } else {
490
+ if (index > 0) {
491
+ accumulator += '.';
492
+ }
493
+ accumulator += fieldName;
494
+ }
495
+ return accumulator;
496
+ },
497
+ ''
498
+ );
499
+ }
500
+
501
+ function hrtimeAndPrint(time) {
502
+ const res = process.hrtime(time);
503
+ return printHRTime(res);
504
+ }
505
+
506
+ function printHRTime(time) {
507
+ const precision = 3;
508
+ if (time[0] > 0) {
509
+ return _.round(time[0] + time[1] / 1e9, precision) + 'sec';
510
+ } else if (time[1] >= 1e6) {
511
+ return _.round(time[1] / 1e6, precision) + 'ms';
512
+ } else if (time[1] >= 1e3) {
513
+ return _.round(time[1] / 1e3, precision) + 'µs';
514
+ } else if (time[1] >= 1e3) {
515
+ return _.round(time[1], precision) + 'ns';
516
+ }
517
+ }
518
+
519
+ /**
520
+ * Recursively iterates over elements of a collection and invokes iteratee for each element.
521
+ *
522
+ * @param {*} value The value to iterate
523
+ * @param {Function} iteratee The iteratee function
524
+ * @param {string|number} key The key of the `value` if the `object` is an Object, or the index of the `value` if the `object` is an Array
525
+ * @param {Object} object The parent object of `value`.
526
+ */
527
+ function forEachDeep(value, iteratee, key, object) {
528
+ iteratee(value, key, object);
529
+ if (_.isPlainObject(value)) {
530
+ _.forEach(value, (v, k) => {
531
+ forEachDeep(v, iteratee, k, value);
532
+ });
533
+ } else if (_.isArray(value)) {
534
+ _.forEach(value, (v, k) => {
535
+ forEachDeep(v, iteratee, k, value);
536
+ });
537
+ }
538
+ }
539
+
540
+ /**
541
+ * Gets the value at the first path of object having non undefined value.
542
+ * If all paths resolve to undefined values, the defaultValue is returned.
543
+ *
544
+ * @param {Object} object The object to query.
545
+ * @param {Array<String | Array<String>>} paths The property paths to search for.
546
+ * @param {*} [defaultValue] The value returned if all paths resolve to undefined values
547
+ * @returns {*}
548
+ */
549
+ function getFirst(object, paths, defaultValue) {
550
+ let result = _(object).at(paths).reject(_.isUndefined).first();
551
+ return _.isUndefined(result) ? defaultValue : result;
552
+ }
553
+
554
+ /**
555
+ *
556
+ * @param {*} value
557
+ * @param {Function} iteratee Function (value: any, fieldPath: Array, stack: Array)
558
+ * @param {object} [options]
559
+ * @param {boolean} options.iterateCollections. Default: true
560
+ * @param {boolean} options.iterateScalars. Default: true
561
+ * @returns {*}
562
+ */
563
+ function mapDeep(value, iteratee, options, _keyPath, _objectStack) {
564
+ let iterate;
565
+ if (_.isPlainObject(value) || _.isArray(value)) {
566
+ iterate = _.get(options, 'iterateCollections', true);
567
+ } else {
568
+ iterate = _.get(options, 'iterateScalars', true);
569
+ }
570
+ if (iterate) {
571
+ value = iteratee(value, _keyPath, _objectStack);
572
+ }
573
+ _keyPath = _keyPath || [];
574
+ _objectStack = _objectStack || [];
575
+ if (_.isPlainObject(value)) {
576
+ value = _.mapValues(value, (val, key) => {
577
+ return mapDeep(val, iteratee, options, _.concat(_keyPath, key), _.concat(_objectStack, value));
578
+ });
579
+ } else if (_.isArray(value)) {
580
+ value = _.map(value, (val, key) => {
581
+ return mapDeep(val, iteratee, options, _.concat(_keyPath, key), _.concat(_objectStack, [value]));
582
+ });
583
+ }
584
+ return value;
585
+ }
586
+
587
+ function getFirstExistingFile(fileNames, inputDir) {
588
+ return findPromise(fileNames, (fileName) => {
589
+ const absPath = path.resolve(inputDir, fileName);
590
+ return fse.exists(absPath);
591
+ });
592
+ }
593
+
594
+ async function parseFirstExistingFile(fileNames, inputDir) {
595
+ const filePath = await getFirstExistingFile(fileNames, inputDir);
596
+ if (filePath) {
597
+ return await parseFile(filePath);
598
+ } else {
599
+ return null;
600
+ }
601
+ }
602
+
603
+ function getFirstExistingFileSync(fileNames, inputDir) {
604
+ return _.chain(fileNames)
605
+ .map((fileName) => path.resolve(inputDir, fileName))
606
+ .find((filePath) => fs.existsSync(filePath))
607
+ .value();
608
+ }
609
+
610
+ function parseFirstExistingFileSync(fileNames, inputDir) {
611
+ let filePath = getFirstExistingFileSync(fileNames, inputDir);
612
+ if (filePath) {
613
+ return parseFileSync(filePath);
614
+ } else {
615
+ return null;
616
+ }
617
+ }
618
+
619
+ function parseFile(filePath) {
620
+ return fse.readFile(filePath, 'utf8').then((data) => {
621
+ return parseDataByFilePath(data, filePath);
622
+ });
623
+ }
624
+
625
+ function parseFileSync(filePath) {
626
+ let data = fs.readFileSync(filePath, 'utf8');
627
+ return parseDataByFilePath(data, filePath);
628
+ }
629
+
630
+ function parseDataByFilePath(string, filePath) {
631
+ const extension = path.extname(filePath).substring(1);
632
+ let data;
633
+ switch (extension) {
634
+ case 'yml':
635
+ case 'yaml':
636
+ string = string.replace(/\n---\s*$/, '');
637
+ data = yaml.safeLoad(string, { schema: yaml.JSON_SCHEMA });
638
+ break;
639
+ case 'json':
640
+ data = JSON.parse(string);
641
+ break;
642
+ case 'toml':
643
+ data = toml.parse(string);
644
+ break;
645
+ case 'markdown':
646
+ case 'mdx':
647
+ case 'md':
648
+ data = parseMarkdownWithFrontMatter(string);
649
+ break;
650
+ case 'js':
651
+ case 'jsx':
652
+ data = string;
653
+ break;
654
+ default:
655
+ throw new Error(`parseDataByFilePath error, extension '${extension}' of file ${filePath} is not supported`);
656
+ }
657
+ return data;
658
+ }
659
+
660
+ function outputData(filePath, data) {
661
+ const res = stringifyDataByFilePath(data, filePath);
662
+ return fse.outputFile(filePath, res);
663
+ }
664
+
665
+ async function outputDataIfNeeded(filePath, data) {
666
+ const res = stringifyDataByFilePath(data, filePath);
667
+ const fileExists = await fse.pathExists(filePath);
668
+ const existingContent = fileExists ? await fse.readFile(filePath, 'utf8') : null;
669
+ if (!fileExists || res !== existingContent) {
670
+ await fse.outputFile(filePath, res);
671
+ return true;
672
+ }
673
+ return false;
674
+ }
675
+
676
+ function outputDataSync(filePath, data) {
677
+ const res = stringifyDataByFilePath(data, filePath);
678
+ fse.outputFileSync(filePath, res);
679
+ }
680
+
681
+ function stringifyDataByFilePath(data, filePath) {
682
+ const extension = path.extname(filePath).substring(1);
683
+ let result;
684
+ switch (extension) {
685
+ case 'yml':
686
+ case 'yaml':
687
+ result = yaml.safeDump(data, { noRefs: true });
688
+ break;
689
+ case 'json':
690
+ result = JSON.stringify(data, null, 4);
691
+ break;
692
+ case 'toml':
693
+ result = toml.stringify(data);
694
+ break;
695
+ case 'markdown':
696
+ case 'mdx':
697
+ case 'md':
698
+ result = '---\n' + yaml.safeDump(data.frontmatter, { noRefs: true }) + '---\n' + _.get(data, 'markdown', '');
699
+ break;
700
+ case 'js':
701
+ case 'jsx':
702
+ result = data;
703
+ break;
704
+ default:
705
+ throw new Error(`stringifyDataByFilePath error, extension '${extension}' of file ${filePath} is not supported`);
706
+ }
707
+ return result;
708
+ }
709
+
710
+ function parseMarkdownWithFrontMatter(string) {
711
+ string = string.replace('\r\n', '\n');
712
+ let frontmatter = null;
713
+ let markdown = string;
714
+ let frontMatterTypes = [
715
+ {
716
+ type: 'yaml',
717
+ startDelimiter: '---\n',
718
+ endDelimiter: '\n---',
719
+ parse: (string) => yaml.safeLoad(string, { schema: yaml.JSON_SCHEMA })
720
+ },
721
+ {
722
+ type: 'toml',
723
+ startDelimiter: '+++\n',
724
+ endDelimiter: '\n+++',
725
+ parse: (string) => toml.parse(string)
726
+ },
727
+ {
728
+ type: 'jsonmd',
729
+ startDelimiter: '---json\n',
730
+ endDelimiter: '\n---',
731
+ parse: (string) => JSON.parse(string)
732
+ },
733
+ {
734
+ type: 'json',
735
+ startDelimiter: '{\n',
736
+ endDelimiter: '\n}',
737
+ parse: (string) => JSON.parse(`{${string}}`)
738
+ }
739
+ ];
740
+ _.forEach(frontMatterTypes, (fmType) => {
741
+ if (string.startsWith(fmType.startDelimiter)) {
742
+ let index = string.indexOf(fmType.endDelimiter);
743
+ if (index !== -1) {
744
+ // The end delimiter must be followed by EOF or by a new line (possibly preceded with spaces)
745
+ // For example ("." used for spaces):
746
+ // |---
747
+ // |title: Title
748
+ // |---...
749
+ // |
750
+ // |Markdown Content
751
+ // |
752
+ // "index" points to the beginning of the second "---"
753
+ // "endDelimEndIndex" points to the end of the second "---"
754
+ // "afterEndDelim" is everything after the second "---"
755
+ // "afterEndDelimMatch" is the matched "...\n" after the second "---"
756
+ // frontmatter will be: {title: "Title"}
757
+ // markdown will be "\nMarkdown Content\n" (the first \n after end delimiter is discarded)
758
+ let endDelimEndIndex = index + fmType.endDelimiter.length;
759
+ let afterEndDelim = string.substring(endDelimEndIndex);
760
+ let afterEndDelimMatch = afterEndDelim.match(/^\s*?(\n|$)/);
761
+ if (afterEndDelimMatch) {
762
+ let data = string.substring(fmType.startDelimiter.length, index);
763
+ frontmatter = fmType.parse(data);
764
+ markdown = afterEndDelim.substring(afterEndDelimMatch[0].length);
765
+ }
766
+ }
767
+ }
768
+ });
769
+ return {
770
+ frontmatter: frontmatter,
771
+ markdown: markdown
772
+ };
773
+ }
774
+
775
+ function deepFreeze(obj) {
776
+ Object.freeze(obj);
777
+
778
+ Object.getOwnPropertyNames(obj).forEach((prop) => {
779
+ if (
780
+ obj.hasOwnProperty(prop) &&
781
+ obj[prop] !== null &&
782
+ (typeof obj[prop] === 'object' || typeof obj[prop] === 'function') &&
783
+ !Object.isFrozen(obj[prop])
784
+ ) {
785
+ this.deepFreeze(obj[prop]);
786
+ }
787
+ });
788
+
789
+ return obj;
790
+ }
791
+
792
+ function failFunctionWithTag(tag) {
793
+ return function fail(message) {
794
+ throw new Error(`[${tag}] ${message}`);
795
+ };
796
+ }
797
+
798
+ function assertFunctionWithFail(fail) {
799
+ return function assert(value, message) {
800
+ if (!value) {
801
+ fail(message);
802
+ }
803
+ };
804
+ }
805
+
806
+ function createLogger(scope, transport) {
807
+ const levels = ['log', 'info', 'debug', 'error'];
808
+ const logger = transport || console;
809
+ const noop = () => {};
810
+ const obj = {};
811
+
812
+ levels.forEach((level) => {
813
+ obj[level] = (...args) => {
814
+ (logger[level] || noop)(`[${scope}]`, ...args);
815
+ };
816
+ });
817
+
818
+ return obj;
819
+ }
820
+
821
+ function logObject(object, title) {
822
+ const label = title ? title : '';
823
+ console.group(label);
824
+ _.forEach(object, (value, key) => {
825
+ if (_.isString(value)) {
826
+ value = `"${value}"`;
827
+ }
828
+ console.log(`${key}: ${chalk.green(value)}`);
829
+ });
830
+ console.groupEnd(label);
831
+ }
832
+
833
+ function joinPathAndGlob(pathStr, glob) {
834
+ glob = globToArray(glob);
835
+ return _.map(glob, (globPart) => _.compact([pathStr, globPart]).join('/'));
836
+ }
837
+
838
+ function globToArray(glob) {
839
+ return _.chain(glob)
840
+ .castArray()
841
+ .compact()
842
+ .reduce((accum, globPart) => {
843
+ const globParts = _.chain(globPart).trim('{}').split(',').compact().value();
844
+ return _.concat(accum, globParts);
845
+ }, [])
846
+ .value();
847
+ }
848
+
849
+ /**
850
+ * Inverse of _.toPath()
851
+ *
852
+ * fromPath(['foo', 'hello.world', 'bar'])
853
+ * => 'foo["hello.world"].bar'
854
+ *
855
+ * @param {Array} pathArray
856
+ * @return {String}
857
+ */
858
+ function fromPath(pathArray) {
859
+ return _.reduce(
860
+ pathArray,
861
+ (accum, pathPart) => {
862
+ if (_.isString(pathPart) && pathPart.indexOf('.') !== -1) {
863
+ return accum + `["${pathPart}"]`;
864
+ }
865
+ return accum + (accum ? '.' : '') + pathPart;
866
+ },
867
+ ''
868
+ );
869
+ }
870
+
871
+ function omitDeep(object, paths) {
872
+ if (_.isPlainObject(object)) {
873
+ return _.mapValues(_.omit(object, paths), (val) => omitDeep(val, paths));
874
+ } else if (_.isArray(object)) {
875
+ return _.map(object, (val) => omitDeep(val, paths));
876
+ }
877
+ return object;
878
+ }
879
+
880
+ /**
881
+ * Reduces the provided `data` using the provided reducer function `reducerFunc`
882
+ * into an object with `data` and `errors` attributes.
883
+ *
884
+ * For every item in the provided `data, the reducer function is invoked with
885
+ * `value` and `key` arguments. The reducer function should return an object
886
+ * with optional `data` and `error` properties, or `null`.
887
+ *
888
+ * ```
889
+ * reducerFunc(value, key) => { data, errors }
890
+ * ```
891
+ *
892
+ * When reducer function returns an object with a `data` property, its value
893
+ * is added to the `data` property of final result. If the original `data` is an
894
+ * object, then the `data` returned by the reducer function is added under the
895
+ * same `key` that was passed to the reducer function. If the original `data` is
896
+ * an array, then the value is pushed to the reduced data.
897
+ * If `data` property is missing, the reduced data will not include that item.
898
+ *
899
+ * When reducer function returns an object with `errors`, which can be an array
900
+ * of error messages or a single error message, these errors are added to the
901
+ * reduced result under `errors` property.
902
+ *
903
+ * @param {Array|Object} data
904
+ * @param {Function} reducerFunc
905
+ * @return {{data: Array|Object, errors: Array}}
906
+ */
907
+ function dataReducerSync(data, reducerFunc) {
908
+ const isArray = Array.isArray(data);
909
+ const accum = {
910
+ data: isArray ? [] : {},
911
+ errors: []
912
+ };
913
+ const accumFunc = isArray ? (accum, value, key) => (accum.data = accum.data.concat(value)) : (accum, value, key) => (accum.data[key] = value);
914
+
915
+ const reducer = (accum, value, key) => {
916
+ const result = reducerFunc(value, key);
917
+ if (_.has(result, 'data')) {
918
+ const resValue = _.get(result, 'data');
919
+ accumFunc(accum, resValue, key);
920
+ }
921
+ if (_.has(result, 'errors')) {
922
+ accum.errors = accum.errors.concat(result.errors);
923
+ }
924
+ return accum;
925
+ };
926
+
927
+ return _.reduce(data, reducer, accum);
928
+ }
929
+
930
+ /**
931
+ * Same as dataReducerSync but receives asynchronous reducerFunc
932
+ *
933
+ * @param {Array|Object} data
934
+ * @param {Function} reducerFunc
935
+ * @return {{data: Array|Object, errors: Array}}
936
+ */
937
+ async function dataReducer(data, reducerFunc) {
938
+ const isArray = Array.isArray(data);
939
+ const accum = {
940
+ data: isArray ? [] : {},
941
+ errors: []
942
+ };
943
+ const reduceFunc = isArray ? reducePromise : reduceObjectPromise;
944
+ const accumFunc = isArray ? (accum, value, key) => (accum.data = accum.data.concat(value)) : (accum, value, key) => (accum.data[key] = value);
945
+
946
+ const reducer = async (accum, value, key) => {
947
+ const result = await reducerFunc(value, key);
948
+ if (_.has(result, 'data')) {
949
+ const resValue = _.get(result, 'data');
950
+ accumFunc(accum, resValue, key);
951
+ }
952
+ if (_.has(result, 'errors')) {
953
+ accum.errors = accum.errors.concat(result.errors);
954
+ }
955
+ return accum;
956
+ };
957
+
958
+ return reduceFunc(data, reducer, accum);
959
+ }
960
+
961
+ const JSX_ENCODE_ENTITIES = {
962
+ '{': '&#x7B;',
963
+ '}': '&#x7D;',
964
+ '<': '&lt;',
965
+ '>': '&gt;'
966
+ };
967
+ const JSX_DECODE_ENTITIES = _.invert(JSX_ENCODE_ENTITIES);
968
+
969
+ function encodeJsx(data) {
970
+ return (
971
+ data &&
972
+ Object.keys(JSX_ENCODE_ENTITIES).reduce((accum, entity) => {
973
+ return accum.replace(entity, JSX_ENCODE_ENTITIES[entity]);
974
+ }, data)
975
+ );
976
+ }
977
+
978
+ function decodeJsx(data) {
979
+ return (
980
+ data &&
981
+ Object.keys(JSX_DECODE_ENTITIES).reduce((accum, entity) => {
982
+ return accum.replace(entity, JSX_DECODE_ENTITIES[entity]);
983
+ }, data)
984
+ );
985
+ }
986
+
987
+ function replaceInRange(str, range, stringToInsert) {
988
+ return str.substr(0, range[0]) + stringToInsert + str.substr(range[1], str.length);
989
+ }
990
+
991
+ function isRelevantReactData(data) {
992
+ return (
993
+ data &&
994
+ data.length < 100 * 1024 && // 100kb
995
+ (data.includes('react') || data.includes('React'))
996
+ );
997
+ }