UNPKG

29.1 kBJavaScriptView Raw
1var path = require('path'),
2 fs = require('fs');
3
4var async = require('async'),
5 _ = require('lodash'),
6 fastGlob = require('fast-glob'),
7 globParent = require('glob-parent'),
8 isGlob = require('is-glob'),
9 md5 = require('md5');
10
11var unixify = require('unixify');
12
13var logger = require('note-down');
14logger.removeOption('showLogLine');
15var chalk = logger.chalk;
16
17var packageJson = require('./package.json');
18
19var utils = require('./utils.js');
20
21var main = function (params) {
22 var paramVerbose = params.paramVerbose;
23 var paramOutdated = params.paramOutdated;
24 var paramWhenFileExists = params.paramWhenFileExists;
25 var cwd = params.cwd || unixify(process.cwd());
26 var copyFiles = params.copyFiles || [];
27 var copyFilesSettings = params.copyFilesSettings || {};
28 var bail = copyFilesSettings.bail || false; // TODO: Document this feature
29 var configFileSourceDirectory = params.configFileSourceDirectory || cwd;
30 var mode = params.mode || 'default';
31
32 // ".only" is useful for debugging (This feature is not mentioned in documentation)
33 copyFiles = (function (copyFiles) {
34 var copyFilesWithOnly = [];
35
36 copyFiles.forEach(function (copyFile) {
37 if (copyFile.only) {
38 copyFilesWithOnly.push(copyFile);
39 }
40 });
41
42 if (copyFilesWithOnly.length) {
43 return copyFilesWithOnly;
44 } else {
45 return copyFiles;
46 }
47 }(copyFiles));
48
49 if (paramOutdated) {
50 var arrFromAndLatest = [];
51
52 for (var i = 0; i < copyFiles.length; i++) {
53 var copyFile = copyFiles[i];
54 var from = copyFile.from;
55 if (from && typeof from === 'object') {
56 var modes = Object.keys(from);
57 for (var j = 0; j < modes.length; j++) {
58 var thisMode = modes[j],
59 fromMode = from[thisMode];
60 if (fromMode.src && fromMode.latest) {
61 var ob = {
62 src: fromMode.src,
63 latest: fromMode.latest
64 };
65 arrFromAndLatest.push(ob);
66 }
67 }
68 }
69 }
70
71 if (paramVerbose) {
72 logger.verbose('Need to check for updates for the following entries:');
73 logger.verbose(JSON.stringify(arrFromAndLatest, null, ' '));
74 }
75
76 var compareSrcAndLatest = function (arrSrcAndLatest) {
77 async.eachLimit(
78 arrSrcAndLatest,
79 8,
80 function (srcAndLatest, callback) {
81 var resourceSrc = srcAndLatest.src,
82 resourceSrcContents = null;
83 var resourceLatest = srcAndLatest.latest,
84 resourceLatestContents = null;
85
86 async.parallel(
87 [
88 function (cb) {
89 utils.readContents(resourceSrc, function (err, contents, encoding) {
90 if (err) {
91 logger.error(' ✗ Could not read: ' + resourceSrc);
92 } else {
93 if (encoding === 'binary')
94 resourceSrcContents = md5(contents);
95 else
96 resourceSrcContents = contents;
97 }
98 cb();
99 });
100 },
101 function (cb) {
102 utils.readContents(resourceLatest, function (err, contents, encoding) {
103 if (err) {
104 logger.error(' ✗ (Could not read) ' + chalk.gray(resourceLatest));
105 } else {
106 if (encoding === 'binary')
107 resourceLatestContents = md5(contents);
108 else
109 resourceLatestContents = contents;
110 }
111 cb();
112 });
113 }
114 ],
115 function () {
116 if (resourceSrcContents !== null && resourceLatestContents !== null) {
117 if (resourceSrcContents === resourceLatestContents) {
118 logger.success(' ✓' + chalk.gray(' (Up to date) ' + resourceSrc));
119 } else {
120 logger.warn(' 🔃 ("src" is outdated w.r.t. "latest") ' + chalk.gray(resourceSrc));
121 }
122 }
123 callback();
124 }
125 );
126 }
127 );
128 };
129
130 if (arrFromAndLatest.length) {
131 compareSrcAndLatest(arrFromAndLatest);
132 } else {
133 logger.warn('There are no "from" entries for which "from.<mode>.src" file needs to be compared with "from.<mode>.latest" file.');
134 }
135 } else {
136 var WHEN_FILE_EXISTS_NOTIFY_ABOUT_AVAILABLE_CHANGE = 'notify-about-available-change',
137 WHEN_FILE_EXISTS_OVERWRITE = 'overwrite',
138 WHEN_FILE_EXISTS_DO_NOTHING = 'do-nothing',
139 ARR_WHEN_FILE_EXISTS = [
140 WHEN_FILE_EXISTS_NOTIFY_ABOUT_AVAILABLE_CHANGE,
141 WHEN_FILE_EXISTS_OVERWRITE,
142 WHEN_FILE_EXISTS_DO_NOTHING
143 ];
144 var whenFileExists = paramWhenFileExists;
145 if (ARR_WHEN_FILE_EXISTS.indexOf(whenFileExists) === -1) {
146 whenFileExists = copyFilesSettings.whenFileExists;
147 if (ARR_WHEN_FILE_EXISTS.indexOf(whenFileExists) === -1) {
148 whenFileExists = WHEN_FILE_EXISTS_DO_NOTHING;
149 }
150 }
151 var overwriteIfFileAlreadyExists = (whenFileExists === WHEN_FILE_EXISTS_OVERWRITE),
152 notifyAboutAvailableChange = (whenFileExists === WHEN_FILE_EXISTS_NOTIFY_ABOUT_AVAILABLE_CHANGE);
153
154 var warningsEncountered = 0;
155
156 copyFiles = copyFiles.map(function normalizeData(copyFile) {
157 var latest = null;
158 var from = null,
159 skipFrom = null;
160 if (typeof copyFile.from === 'string' || Array.isArray(copyFile.from)) {
161 from = copyFile.from;
162 } else {
163 var fromMode = copyFile.from[mode] || copyFile.from['default'] || {};
164 if (typeof fromMode === 'string') {
165 from = fromMode;
166 } else {
167 from = fromMode.src;
168 skipFrom = !!fromMode.skip;
169 latest = fromMode.latest;
170 }
171 }
172
173 var to = null,
174 skipTo = null,
175 removeSourceMappingURL = null,
176 uglify = null;
177 if (typeof copyFile.to === 'string') {
178 to = copyFile.to;
179 uglify = utils.booleanIntention(copyFilesSettings.uglifyJs, false);
180 } else {
181 var toMode = copyFile.to[mode] || copyFile.to['default'] || {};
182 if (typeof toMode === 'string') {
183 to = toMode;
184 } else {
185 to = toMode.dest;
186 skipTo = !!toMode.skip;
187 }
188
189 if (typeof toMode === 'object' && toMode.removeSourceMappingURL !== undefined) {
190 removeSourceMappingURL = utils.booleanIntention(toMode.removeSourceMappingURL, false);
191 } else {
192 removeSourceMappingURL = utils.booleanIntention(copyFilesSettings.removeSourceMappingURL, false);
193 }
194
195 if (typeof toMode === 'object' && toMode.uglifyJs !== undefined) {
196 uglify = utils.booleanIntention(toMode.uglifyJs, false);
197 } else {
198 uglify = utils.booleanIntention(copyFilesSettings.uglifyJs, false);
199 }
200 }
201
202 if (isGlob(to)) {
203 warningsEncountered++;
204 logger.log('');
205 logger.warn('The "to" entries should not be a "glob" pattern. ' + chalk.blue('(Reference: https://github.com/isaacs/node-glob#glob-primer)'));
206
207 to = null;
208 }
209
210 var toFlat = null;
211 if (copyFile.toFlat) {
212 toFlat = true;
213 }
214
215 if ((typeof from === 'string' || Array.isArray(from)) && typeof to === 'string') {
216 if (!Array.isArray(from) && (from.match(/\.js$/) || to.match(/\.js$/))) {
217 // If "from" or "to" path ends with ".js", that indicates that it is a JS file
218 // So, retain the uglify setting.
219 // It is a "do nothing" block
220 } else {
221 // It does not seem to be a JS file. So, don't uglify it.
222 uglify = false;
223 }
224
225 return {
226 intendedFrom: from,
227 intendedTo: to,
228 latest: latest,
229 from: (function () {
230 if (utils.isRemoteResource(from)) {
231 return from;
232 }
233 if (Array.isArray(from)) {
234 // If array, it's a glob instruction. Any objects are
235 var globPatterns = [];
236 var globSettings = {};
237 from.forEach( globPart => {
238 if (typeof globPart === 'string') {
239 if (globPart.charAt(0) === '!')
240 globPatterns.push('!' + unixify(path.join(configFileSourceDirectory, globPart.substring(1))));
241 else
242 globPatterns.push(unixify(path.join(configFileSourceDirectory, globPart)));
243 } else {
244 Object.assign(globSettings, globPart);
245 }
246 });
247 return {
248 globPatterns: globPatterns,
249 globSettings: globSettings,
250 };
251 }
252 return unixify(path.join(configFileSourceDirectory, from));
253 }()),
254 to: unixify(path.join(configFileSourceDirectory, to)),
255 toFlat: toFlat,
256 removeSourceMappingURL: removeSourceMappingURL,
257 uglify: uglify
258 };
259 } else {
260 if (
261 (typeof from !== 'string' && !skipFrom) ||
262 (typeof to !== 'string' && !skipTo)
263 ) {
264 warningsEncountered++;
265 if (warningsEncountered === 1) { // Show this only once
266 logger.log('');
267 logger.warn('Some entries will not be considered in the current mode (' + mode + ').');
268 }
269
270 logger.log('');
271
272 var applicableModesForSkip = _.uniq(['"default"', '"' + mode + '"']).join(' / ');
273 if (typeof from !== 'string' && !skipFrom) {
274 var fromValuesToCheck = _.uniq([
275 '"from"',
276 '"from.default"',
277 '"from.default.src"',
278 '"from.' + mode + '"',
279 '"from.' + mode + '.src"'
280 ]).join(' / ');
281 logger.warn(' Please ensure that the value for ' + fromValuesToCheck + ' is a string.');
282 logger.warn(' Otherwise, add ' + chalk.blue('"skip": true') + ' under "from" for mode: ' + applicableModesForSkip);
283 }
284 if (typeof to !== 'string' && !skipTo) {
285 var toValuesToCheck = _.uniq([
286 '"to"',
287 '"to.default"',
288 '"to.default.dest"',
289 '"to.' + mode + '"',
290 '"to.' + mode + '.dest"'
291 ]).join(' / ');
292 logger.warn(' Please ensure that the value for ' + toValuesToCheck + ' is a string.');
293 logger.warn(' Otherwise, add ' + chalk.blue('"skip": true') + ' under "to" for mode: ' + applicableModesForSkip);
294 }
295
296 logger.warn(' ' + JSON.stringify(copyFile, null, ' ').replace(/\n/g, '\n '));
297 }
298 }
299 });
300
301 copyFiles = (function () {
302 var arr = [];
303 copyFiles.forEach(function (copyFile) {
304 if (copyFile && copyFile.from) {
305 var entries = function() {
306 if (typeof copyFile.from === 'string' && isGlob(copyFile.from)) {
307 return fastGlob.sync([copyFile.from], { dot: !copyFilesSettings.ignoreDotFilesAndFolders });
308 } else if (copyFile.from.globPatterns) {
309 return fastGlob.sync(
310 copyFile.from.globPatterns,
311 Object.assign({ dot: !copyFilesSettings.ignoreDotFilesAndFolders }, copyFile.from.globSettings)
312 );
313 } else {
314 return null;
315 }
316 }();
317 if (entries && entries.length) {
318 entries.forEach(function (entry) {
319 var ob = JSON.parse(JSON.stringify(copyFile));
320 ob.from = entry;
321
322 var intendedFrom = ob.intendedFrom;
323 if (Array.isArray(intendedFrom)) {
324 intendedFrom = intendedFrom[0];
325 }
326 var targetTo = unixify(
327 path.relative(
328 path.join(
329 configFileSourceDirectory,
330 globParent(intendedFrom)
331 ),
332 ob.from
333 )
334 );
335 if (copyFile.toFlat) {
336 var fileName = path.basename(targetTo);
337 targetTo = fileName;
338 }
339
340 ob.to = unixify(
341 path.join(
342 ob.to,
343 targetTo
344 )
345 );
346 arr.push(ob);
347 });
348 } else {
349 arr.push(copyFile);
350 }
351 }
352 });
353 return arr;
354 }());
355
356 var writeContents = function (copyFile, options, cb) {
357 var to = copyFile.to,
358 intendedFrom = copyFile.intendedFrom;
359 var contents = options.contents,
360 consoleCommand = options.consoleCommand,
361 overwriteIfFileAlreadyExists = options.overwriteIfFileAlreadyExists;
362
363 utils.ensureDirectoryExistence(to);
364
365 var fileExists = fs.existsSync(to),
366 fileDoesNotExist = !fileExists;
367
368 var avoidedFileOverwrite;
369 var finalPath = '';
370 if (
371 fileDoesNotExist ||
372 (fileExists && overwriteIfFileAlreadyExists)
373 ) {
374 try {
375 if (to[to.length-1] === '/') {
376 var stats = fs.statSync(to);
377 if (stats.isDirectory()) {
378 if (typeof intendedFrom === 'string' && !isGlob(intendedFrom)) {
379 var fileName = path.basename(intendedFrom);
380 to = unixify(path.join(to, fileName));
381 }
382 }
383 }
384
385 fs.writeFileSync(to, contents, copyFile.encoding === 'binary' ? null : 'utf8');
386 finalPath = to;
387 } catch (e) {
388 cb(e);
389 return;
390 }
391 if (copyFilesSettings.addReferenceToSourceOfOrigin) {
392 var sourceDetails = intendedFrom;
393 if (consoleCommand) {
394 if (consoleCommand.sourceMappingUrl) {
395 sourceDetails += '\n\n' + consoleCommand.sourceMappingUrl;
396 }
397 if (consoleCommand.uglifyJs) {
398 sourceDetails += '\n\n' + consoleCommand.uglifyJs;
399 }
400 }
401
402 /*
403 TODO: Handle error scenario for this ".writeFileSync()" operation.
404 Not handling it yet because for all practical use-cases, if
405 the code has been able to write the "to" file, then it should
406 be able to write the "<to>.source.txt" file
407 */
408 fs.writeFileSync(to + '.source.txt', sourceDetails, 'utf8');
409 }
410 avoidedFileOverwrite = false;
411 } else {
412 avoidedFileOverwrite = true;
413 }
414 cb(null, avoidedFileOverwrite, finalPath || to);
415 };
416
417 var checkForAvailableChange = function (copyFile, contentsOfFrom, config, cb) {
418 var notifyAboutAvailableChange = config.notifyAboutAvailableChange;
419
420 if (notifyAboutAvailableChange) {
421 var to = copyFile.to;
422 utils.readContents(to, function (err, contentsOfTo, encoding) {
423 if (err) {
424 cb(chalk.red(' (unable to read "to.<mode>.dest" file at path ' + to + ')'));
425 warningsEncountered++;
426 } else {
427 copyFile.encoding = encoding;
428 var needsUglify = copyFile.uglify;
429 var removeSourceMappingURL = copyFile.removeSourceMappingURL;
430
431 var response = utils.additionalProcessing({
432 needsUglify: needsUglify,
433 removeSourceMappingURL: removeSourceMappingURL
434 }, contentsOfFrom);
435 var processedCode = response.code;
436
437 if (copyFile.encoding === 'binary') {
438 // Only run resource-intensive md5 on binary files
439 if (md5(processedCode) === md5(contentsOfTo)) {
440 cb(chalk.gray(' (up to date)'));
441 } else {
442 cb(chalk.yellow(' (md5 update is available)'));
443 }
444 } else {
445 if (processedCode === contentsOfTo) {
446 cb(chalk.gray(' (up to date)'));
447 } else {
448 cb(chalk.yellow(' (update is available)'));
449 }
450 }
451 }
452 });
453 } else {
454 cb();
455 }
456 };
457
458 var preWriteOperations = function (copyFile, contents, cb) {
459 var needsUglify = copyFile.uglify;
460 var removeSourceMappingURL = copyFile.removeSourceMappingURL;
461 var response = utils.additionalProcessing({
462 needsUglify: needsUglify,
463 removeSourceMappingURL: removeSourceMappingURL
464 }, contents);
465
466 var processedCode = response.code;
467 var consoleCommand = response.consoleCommand;
468
469 var data = {};
470 data.contentsAfterPreWriteOperations = processedCode;
471 if (consoleCommand) {
472 if (consoleCommand.uglifyJs) {
473 consoleCommand.uglifyJs = (
474 '$ ' + consoleCommand.uglifyJs +
475 '\n' +
476 '\nWhere:' +
477 '\n uglifyjs = npm install -g uglify-js@' + packageJson.dependencies['uglify-js'] +
478 '\n <source> = File ' + copyFile.intendedFrom +
479 '\n <destination> = File ./' + path.basename(copyFile.intendedTo)
480 );
481 }
482
483 data.consoleCommand = consoleCommand;
484 }
485
486 cb(data);
487 };
488
489 var postWriteOperations = function (copyFile, originalContents, contentsAfterPreWriteOperations, config, cb) {
490 checkForAvailableChange(copyFile, originalContents, config, function (status) {
491 cb(status);
492 });
493 };
494
495 var doCopyFile = function (copyFile, cb) {
496 var from = copyFile.from,
497 to = copyFile.to;
498
499 var printFrom = ' ' + chalk.gray(utils.getRelativePath(cwd, from));
500 var printFromToOriginal = ' ' + chalk.gray(utils.getRelativePath(cwd, to));
501
502 var successMessage = ' ' + chalk.green('✓') + ` Copied `,
503 successMessageAvoidedFileOverwrite = ' ' + chalk.green('✓') + chalk.gray(' Already exists'),
504 errorMessageCouldNotReadFromSrc = ' ' + chalk.red('✗') + ' Could not read',
505 errorMessageFailedToCopy = ' ' + chalk.red('✗') + ' Failed to copy';
506
507 var destFileExists = fs.existsSync(to),
508 destFileDoesNotExist = !destFileExists;
509 if (
510 destFileDoesNotExist ||
511 (
512 destFileExists &&
513 (
514 overwriteIfFileAlreadyExists ||
515 notifyAboutAvailableChange
516 )
517 )
518 ) {
519 utils.readContents(copyFile.from, function (err, contentsOfFrom, encoding) {
520 if (err) {
521 warningsEncountered++;
522 if (destFileExists && notifyAboutAvailableChange) {
523 logger.log(errorMessageCouldNotReadFromSrc + printFrom);
524 } else {
525 logger.log(errorMessageFailedToCopy + printFromToOriginal);
526 if (bail) {
527 logger.error(`An error occurred in reading file (From: ${copyFile.from} ; To: ${copyFile.to}).`);
528 logger.error(`Exiting the copy-files-from-to operation with exit code 1 since the "bail" option was set.`);
529 process.exit(1);
530 }
531 }
532 cb();
533 return;
534 }
535 copyFile.encoding = encoding;
536 var typeString = `[${utils.getColoredTypeString(encoding)}]`;
537
538 preWriteOperations(copyFile, contentsOfFrom, function (options) {
539 var contentsAfterPreWriteOperations = options.contentsAfterPreWriteOperations,
540 consoleCommand = options.consoleCommand;
541 writeContents(
542 copyFile,
543 {
544 contents: contentsAfterPreWriteOperations,
545 consoleCommand: consoleCommand,
546 overwriteIfFileAlreadyExists: overwriteIfFileAlreadyExists
547 },
548 function (err, avoidedFileOverwrite, finalPath) {
549 if (err) {
550 warningsEncountered++;
551 logger.log(errorMessageFailedToCopy + printFromTo);
552 if (bail) {
553 logger.error(`An error occurred in writing file (From: ${copyFile.from} ; To: ${copyFile.to}).`);
554 logger.error(`Exiting the copy-files-from-to operation with exit code 1 since the "bail" option was set.`);
555 process.exit(1);
556 }
557 cb();
558 return;
559 } else {
560 var printTo = ' ' + chalk.gray(utils.getRelativePath(cwd, finalPath));
561 var printFromTo = printFrom + ' to' + printTo;
562
563 postWriteOperations(
564 copyFile,
565 contentsOfFrom,
566 contentsAfterPreWriteOperations,
567 {
568 notifyAboutAvailableChange: notifyAboutAvailableChange
569 },
570 function (appendToSuccessMessage) {
571 if (avoidedFileOverwrite) {
572 logger.log(successMessageAvoidedFileOverwrite + (appendToSuccessMessage || '') + printTo);
573 } else {
574 // Copying value of "destFileDoesNotExist" to "destFileDidNotExist" since that has a better
575 // sematic name for the given context
576 var destFileDidNotExist = destFileDoesNotExist;
577 if (destFileDidNotExist) {
578 logger.log(successMessage + typeString + printFromTo);
579 } else {
580 logger.log(successMessage + typeString + (appendToSuccessMessage || '') + printFromTo);
581 }
582 }
583 cb();
584 }
585 );
586 }
587 }
588 );
589 });
590 });
591 } else {
592 logger.log(successMessageAvoidedFileOverwrite + printFromToOriginal);
593 cb();
594 }
595 };
596
597 var done = function (warningsEncountered) {
598 if (warningsEncountered) {
599 if (warningsEncountered === 1) {
600 logger.warn('\nEncountered ' + warningsEncountered + ' warning. Please check.');
601 } else {
602 logger.warn('\nEncountered ' + warningsEncountered + ' warnings. Please check.');
603 }
604 logger.error('Error: Please resolve the above mentioned warnings. Exiting with code 1.');
605 process.exit(1);
606 }
607 };
608
609 if (copyFiles.length) {
610 logger.log(
611 chalk.blue('\nStarting copy operation in "' + (mode || 'default') + '" mode:') +
612 (overwriteIfFileAlreadyExists ? chalk.yellow(' (overwrite option is on)') : '')
613 );
614
615 async.eachLimit(
616 copyFiles,
617 8,
618 function (copyFile, callback) {
619 // "copyFile" would be "undefined" when copy operation is not applicable
620 // in current "mode" for the given file
621 if (copyFile && typeof copyFile === 'object') {
622 doCopyFile(copyFile, function () {
623 callback();
624 });
625 } else {
626 callback();
627 }
628 },
629 function () {
630 done(warningsEncountered);
631 }
632 );
633 } else {
634 logger.warn('No instructions applicable for copy operation.');
635 }
636 }
637};
638
639module.exports = main;