1 | 'use strict';
|
2 |
|
3 | const fs = require('fs-extra');
|
4 | const path = require('path');
|
5 | const crypto = require('crypto');
|
6 | const zlib = require('zlib');
|
7 | const EventEmitter = require('events').EventEmitter;
|
8 | const util = require('util');
|
9 |
|
10 | const _ = require('underscore');
|
11 | const async = require('async');
|
12 | const dir = require('node-dir');
|
13 | const uglifyjs = require('uglify-js');
|
14 | const CleanCss = require('clean-css');
|
15 | const less = require('less');
|
16 | const request = require('request');
|
17 |
|
18 | const S3 = require('./s3');
|
19 |
|
20 | const ONE_YEAR_IN_SECONDS = 31536000;
|
21 | const THIRTY_DAYS_IN_SECONDS = 2592000;
|
22 | const SEVEN_DAYS_IN_SECONDS = 604800;
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 | function AssetProcessor(config) {
|
29 |
|
30 | let baseRoot;
|
31 | let currRelativeRoot;
|
32 |
|
33 | this.config = config;
|
34 |
|
35 | if (config.s3) {
|
36 | this.s3 = new S3(config.s3.bucket, config.s3.key, config.s3.secret);
|
37 | if (config.s3.cloudfrontMapping) {
|
38 | this.s3.setCloudfrontMapping(config.s3.cloudfrontMapping);
|
39 | }
|
40 | }
|
41 |
|
42 |
|
43 |
|
44 | if (this.config.targets.javascripts && !this.config.targets.javascripts.extensions) {
|
45 | this.config.targets.javascripts.extensions = ['.js'];
|
46 | }
|
47 | if (this.config.targets.stylesheets && !this.config.targets.stylesheets.extensions) {
|
48 | this.config.targets.stylesheets.extensions = ['.css'];
|
49 | }
|
50 | if (this.config.targets.images && !this.config.targets.images.extensions) {
|
51 | this.config.targets.images.extensions = ['.ico', '.png','.jpg','.jpeg','.gif','.bmp','.svg'];
|
52 | }
|
53 |
|
54 |
|
55 |
|
56 | if (!this.config.root) {
|
57 | throw new Error('Configuration root is required');
|
58 | } else if (!fs.existsSync(this.config.root)) {
|
59 | throw new Error('Invalid configuration root');
|
60 | }
|
61 |
|
62 | this.config.root = path.normalize(this.config.root);
|
63 |
|
64 | baseRoot = this.config.root;
|
65 |
|
66 | currRelativeRoot = this.config.targets.javascripts && this.config.targets.javascripts.root;
|
67 | this.config.javascriptsRoot = path.normalize(currRelativeRoot ? path.resolve(baseRoot, currRelativeRoot) : baseRoot);
|
68 |
|
69 | currRelativeRoot = this.config.targets.stylesheets && this.config.targets.stylesheets.root;
|
70 | this.config.stylesheetsRoot = path.normalize(currRelativeRoot ? path.resolve(baseRoot, currRelativeRoot) : baseRoot);
|
71 |
|
72 | currRelativeRoot = this.config.targets.images && this.config.targets.images.root;
|
73 | this.config.imagesRoot = path.normalize(currRelativeRoot ? path.resolve(baseRoot, currRelativeRoot) : baseRoot);
|
74 | this.config.imagesRootRelative = path.relative(this.config.root, this.config.imagesRoot);
|
75 |
|
76 | currRelativeRoot = this.config.targets.extras && this.config.targets.extras.root;
|
77 | this.config.extrasRoot = path.normalize(currRelativeRoot ? path.resolve(baseRoot, currRelativeRoot) : baseRoot);
|
78 | this.config.extrasRootRelative = path.relative(this.config.root, this.config.extrasRoot);
|
79 |
|
80 |
|
81 | this.forceCdnUpdate = this.config.forceCdnUpdate;
|
82 | }
|
83 |
|
84 | util.inherits(AssetProcessor, EventEmitter);
|
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 | AssetProcessor.prototype.getJavaScriptFiles = function (normalizedFullPath, callback) {
|
91 | _getFilesRelativeToRoot(this.config.root, this.config.javascriptsRoot, this.config.targets.javascripts, normalizedFullPath, callback);
|
92 | };
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 | AssetProcessor.prototype.getCssFiles = function (normalizedFullPath, callback) {
|
99 | _getFilesRelativeToRoot(this.config.root, this.config.stylesheetsRoot, this.config.targets.stylesheets, normalizedFullPath, callback);
|
100 | };
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 | AssetProcessor.prototype.getImageFiles = function (normalizedFullPath, callback) {
|
107 | _getFilesRelativeToRoot(this.config.root, this.config.imagesRoot, this.config.targets.images, normalizedFullPath, callback);
|
108 | };
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 | AssetProcessor.prototype.getExtraFiles = function (normalizedFullPath, callback) {
|
115 | _getFilesRelativeToRoot(this.config.root, this.config.extrasRoot, this.config.targets.extras, normalizedFullPath, callback);
|
116 | };
|
117 |
|
118 | function _getFilesRelativeToRoot(mainRoot, activeRoot, targets, normalizedFullPath, callback) {
|
119 | const relative = path.relative(mainRoot, activeRoot);
|
120 |
|
121 | if (typeof normalizedFullPath === 'function') {
|
122 | callback = normalizedFullPath;
|
123 | normalizedFullPath = false;
|
124 | }
|
125 |
|
126 |
|
127 | _getFiles(targets, activeRoot, normalizedFullPath, function(err, files) {
|
128 | if (normalizedFullPath) {
|
129 |
|
130 | callback(err, files);
|
131 | } else {
|
132 |
|
133 |
|
134 | const relativeFiles = _.map(files || [], function (file) {
|
135 | return path.join(relative, file).replace(/\\/gi,'/');
|
136 | });
|
137 |
|
138 | callback(err, relativeFiles);
|
139 |
|
140 | }
|
141 | });
|
142 | }
|
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 | AssetProcessor.prototype.compileLessFiles = function(callback) {
|
149 | const self = this;
|
150 | const lessTargetConfig = JSON.parse(JSON.stringify(this.config.targets.stylesheets));
|
151 | const cssFiles = [];
|
152 |
|
153 | lessTargetConfig.extensions = ['.less'];
|
154 |
|
155 | async.auto({
|
156 | getFiles: [function(next) {
|
157 | _getFiles(lessTargetConfig, self.config.stylesheetsRoot, true, next);
|
158 | }],
|
159 | processFiles: ['getFiles', function(next, results) {
|
160 |
|
161 |
|
162 | const files = results.getFiles.filter(function(file) {
|
163 | return path.extname(file).toLowerCase() === '.less';
|
164 | });
|
165 |
|
166 | async.eachSeries(files, function(file, eachNext) {
|
167 |
|
168 | async.auto({
|
169 | readFile: [function(subNext) {
|
170 | fs.readFile(file, subNext);
|
171 | }],
|
172 | render: ['readFile', function(subNext, results) {
|
173 | less.render(results.readFile.toString(), {
|
174 | filename: path.resolve(file)
|
175 | }, subNext);
|
176 | }],
|
177 | writeFile: ['render', function(subNext, results) {
|
178 | const cssFile = file.replace(/\.less$/i, '.css');
|
179 | fs.writeFile(cssFile, results.render, function(err) {
|
180 | if (!err) {
|
181 | cssFiles.push(cssFile);
|
182 | }
|
183 | subNext(err);
|
184 | });
|
185 | }]
|
186 | }, function(err) {
|
187 | eachNext(err);
|
188 | });
|
189 | }, function(err) {
|
190 | next(err);
|
191 | });
|
192 | }]
|
193 | }, function(err) {
|
194 | callback(err, cssFiles);
|
195 | });
|
196 | };
|
197 |
|
198 |
|
199 |
|
200 |
|
201 |
|
202 |
|
203 | AssetProcessor.prototype.processJavaScript = function(excludeSourceMap, callback) {
|
204 | const self = this;
|
205 |
|
206 | if (typeof excludeSourceMap === 'function') {
|
207 | callback = excludeSourceMap;
|
208 | excludeSourceMap = false;
|
209 | }
|
210 |
|
211 | async.auto({
|
212 | getFiles: [function(next) {
|
213 | self.getJavaScriptFiles(true, next);
|
214 | }],
|
215 | getPath: ['getFiles', function(next, results) {
|
216 | _getJavaScriptPath(results.getFiles, next);
|
217 | }],
|
218 | minify: ['getPath', function(next, results) {
|
219 | const jsFiles = results.getFiles;
|
220 | let uglifyResult = {};
|
221 | let err;
|
222 | const mapPath = results.getPath.replace('.js','.map');
|
223 | const publicMapPath = self.s3 ? self.s3.urlWithBucket(mapPath) : mapPath;
|
224 | const uglifyOptions = excludeSourceMap ? {} : {outSourceMap: publicMapPath};
|
225 |
|
226 | self.emit('minifyStarted', {type: 'js', files: jsFiles});
|
227 |
|
228 | if (!jsFiles.length) {
|
229 | next(null, {});
|
230 | } else {
|
231 | try {
|
232 | uglifyResult = uglifyjs.minify(jsFiles, uglifyOptions);
|
233 | } catch (uglifyError) {
|
234 | err = uglifyError;
|
235 | }
|
236 |
|
237 | next(err, { js: uglifyResult && uglifyResult.code, map: uglifyResult.map });
|
238 | }
|
239 |
|
240 |
|
241 | }]
|
242 | }, callback);
|
243 | }
|
244 |
|
245 |
|
246 |
|
247 |
|
248 |
|
249 |
|
250 | AssetProcessor.prototype.saveJavaScriptToFile = function(callback) {
|
251 |
|
252 | const self = this;
|
253 |
|
254 | async.auto({
|
255 | getSource: [function (next) {
|
256 | self.processJavaScript(false, next);
|
257 | }],
|
258 | saveFile: ['getSource', function (next, results) {
|
259 |
|
260 | let relativeUrl = "";
|
261 | const source = results.getSource.minify.js;
|
262 |
|
263 | if (source) {
|
264 | const fullLocalPath = self.config.javascriptsRoot + '/' + results.getSource.getPath;
|
265 | relativeUrl = '/' + results.getSource.getPath;
|
266 |
|
267 | fs.outputFileSync(fullLocalPath, source);
|
268 | }
|
269 |
|
270 | next(null, relativeUrl);
|
271 | }],
|
272 | }, function(err, results) {
|
273 | const result = {
|
274 | jsUrl: results && results.saveFile
|
275 | };
|
276 | callback(err, result);
|
277 | });
|
278 |
|
279 | };
|
280 |
|
281 |
|
282 |
|
283 |
|
284 |
|
285 |
|
286 | AssetProcessor.prototype.uploadJavaScriptToCdn = function(excludeSourceMap, callback) {
|
287 |
|
288 | const self = this;
|
289 |
|
290 | if (typeof excludeSourceMap === 'function') {
|
291 | callback = excludeSourceMap;
|
292 | excludeSourceMap = false;
|
293 | }
|
294 |
|
295 | async.auto({
|
296 | getSource: [function(next) {
|
297 | self.processJavaScript(excludeSourceMap, next);
|
298 | }],
|
299 | compressJs: ['getSource', function(next, results) {
|
300 | zlib.gzip(results.getSource.minify.js, next);
|
301 | }],
|
302 | uploadJs: ['compressJs', function(next, results) {
|
303 | const jsFiles = results.getSource.getFiles;
|
304 | const gzip = results.compressJs;
|
305 | const targetPath = results.getSource.getPath;
|
306 | const headers = {
|
307 | 'x-amz-acl': 'public-read',
|
308 | 'Content-Type': 'application/x-javascript',
|
309 | 'Content-Encoding': 'gzip',
|
310 | 'Content-Length': gzip.length,
|
311 | 'Cache-Control': 'public, max-age=' + ONE_YEAR_IN_SECONDS
|
312 | };
|
313 | self.emit('minifyEnded', {type: 'js', files: jsFiles});
|
314 | self.emit('uploadStarted', {type: 'js', target: targetPath, source: 'memory'});
|
315 | self.s3.putBuffer(gzip, targetPath, headers, next);
|
316 | }],
|
317 | uploadMap: ['getSource', function(next, results) {
|
318 | const mapPath = results.getSource.getPath.replace('.js','.map');
|
319 | if (!excludeSourceMap) {
|
320 | self.s3.putBuffer(results.getSource.minify.map, mapPath, next);
|
321 | } else {
|
322 | next();
|
323 | }
|
324 | }],
|
325 | uploadCompleted: ['uploadJs', function(next, results) {
|
326 | const targetPath = results.getSource.getPath;
|
327 | self.emit('uploadEnded', {type: 'js', target: targetPath, source: 'memory', url: results.uploadJs});
|
328 | next();
|
329 | }]
|
330 | }, function(err, results) {
|
331 | callback(err, results && results.uploadJs);
|
332 | });
|
333 | };
|
334 |
|
335 | AssetProcessor.prototype.processCss = function(callback) {
|
336 |
|
337 | const self = this;
|
338 |
|
339 | async.auto({
|
340 | getFiles: [function(next) {
|
341 | self.getCssFiles(true, next);
|
342 | }],
|
343 | getPath: ['getFiles', function(next, results) {
|
344 | _getCssPath(results.getFiles, next);
|
345 | }],
|
346 | cleanCss: ['getFiles', function(next, results) {
|
347 |
|
348 | const files = results.getFiles;
|
349 | let css = '';
|
350 |
|
351 | self.emit('minifyStarted', {type: 'css', files: files});
|
352 |
|
353 | async.eachSeries(files, function(file, eachNext) {
|
354 | let currCss = ''+fs.readFileSync(file);
|
355 | currCss = new CleanCss({noAdvanced: true, noRebase: true}).minify(currCss);
|
356 | currCss = _rebaseUrls(self, file, currCss);
|
357 | css += currCss+' \n';
|
358 | eachNext();
|
359 | }, function(err) {
|
360 | next(err, css);
|
361 | });
|
362 | }]
|
363 | }, callback);
|
364 | };
|
365 |
|
366 | AssetProcessor.prototype.saveCssToFile = function(callback) {
|
367 |
|
368 | const self = this;
|
369 |
|
370 | async.auto({
|
371 | getSource: [function(next) {
|
372 | self.processCss(next)
|
373 | }],
|
374 | saveFile: ['getSource', function (next, results) {
|
375 |
|
376 | var relativeUrl = "";
|
377 | const source = results.getSource.cleanCss;
|
378 |
|
379 | if (source) {
|
380 | const fullLocalPath = self.config.stylesheetsRoot + '/' + results.getSource.getPath;
|
381 | relativeUrl = '/' + results.getSource.getPath;
|
382 |
|
383 | fs.outputFileSync(fullLocalPath, source);
|
384 | }
|
385 |
|
386 | next(null, relativeUrl);
|
387 | }],
|
388 | }, function(err, results) {
|
389 | callback(err, {
|
390 | cssUrl: results && results.saveFile
|
391 | });
|
392 | });
|
393 |
|
394 | };
|
395 |
|
396 |
|
397 |
|
398 |
|
399 |
|
400 | AssetProcessor.prototype.uploadCssToCdn = function(callback) {
|
401 |
|
402 | const self = this;
|
403 |
|
404 | async.auto({
|
405 | getSource: [function(next) {
|
406 | self.processCss(next)
|
407 | }],
|
408 | compressCss: ['getSource', function(next, results) {
|
409 | zlib.gzip(results.getSource.cleanCss, next);
|
410 | }],
|
411 | uploadCss: ['compressCss', function(next, results) {
|
412 | const cssFiles = results.getSource.getFiles;
|
413 | const gzip = results.compressCss;
|
414 | const targetPath = results.getSource.getPath;
|
415 | const headers = {
|
416 | 'x-amz-acl': 'public-read',
|
417 | 'Content-Type': 'text/css',
|
418 | 'Content-Encoding': 'gzip',
|
419 | 'Content-Length': gzip.length,
|
420 | 'Cache-Control': 'public, max-age=' + ONE_YEAR_IN_SECONDS
|
421 | };
|
422 |
|
423 | self.emit('minifyEnded', {type: 'css', files: cssFiles});
|
424 | self.emit('uploadStarted', {type: 'css', target: targetPath, source: 'memory'});
|
425 | self.s3.putBuffer(gzip, targetPath, headers, next);
|
426 | }],
|
427 | uploadCompleted: ['uploadCss', function(next, results) {
|
428 | const targetPath = results.getSource.getPath;
|
429 | self.emit('uploadEnded', {type: 'css', target: targetPath, source: 'memory', url: results.uploadCss});
|
430 | next();
|
431 | }]
|
432 | }, function(err, results) {
|
433 | callback(err, results && results.uploadCss);
|
434 | });
|
435 | };
|
436 |
|
437 | AssetProcessor.prototype.processImages = function(callback) {
|
438 |
|
439 | const self = this;
|
440 |
|
441 | async.auto({
|
442 | getFiles: [function(next) {
|
443 | self.getImageFiles(true, next);
|
444 | }],
|
445 | getFolder: [function(next) {
|
446 | _getImageFolder(self.config.imagesRootRelative, next);
|
447 | }],
|
448 | }, callback);
|
449 | };
|
450 |
|
451 |
|
452 |
|
453 |
|
454 |
|
455 | AssetProcessor.prototype.uploadImagesToCdn = function(callback) {
|
456 |
|
457 | const self = this;
|
458 |
|
459 | async.auto({
|
460 | getFiles: [function(next) {
|
461 | self.getImageFiles(true, next);
|
462 | }],
|
463 | getFolder: [function(next) {
|
464 | _getImageFolder(self.config.imagesRootRelative, next);
|
465 | }],
|
466 | uploadImages: ['getFiles', 'getFolder', function(next, results) {
|
467 | _wrapUploadEmitter(self, _uploadRelativeToRoot(results.getFiles, self.config.imagesRoot, results.getFolder, self.s3, next), 'image');
|
468 | }]
|
469 | }, function(err, results) {
|
470 | callback(err, results && results.uploadImages);
|
471 | });
|
472 | };
|
473 |
|
474 |
|
475 |
|
476 |
|
477 |
|
478 | AssetProcessor.prototype.uploadExtrasToCdn = function(callback) {
|
479 |
|
480 | const self = this;
|
481 |
|
482 | async.auto({
|
483 | getFiles: [function(next) {
|
484 | self.getExtraFiles(true, next);
|
485 | }],
|
486 | getFolder: [function(next) {
|
487 | _getExtrasFolder(self.config.extrasRootRelative, next);
|
488 | }],
|
489 | uploadExtras: ['getFiles', 'getFolder', function(next, results) {
|
490 | _wrapUploadEmitter(self, _uploadRelativeToRoot(results.getFiles, self.config.extrasRoot, results.getFolder, self.s3, next), 'extra');
|
491 | }]
|
492 | }, function(err, results) {
|
493 | callback(err, results && results.uploadExtras);
|
494 | });
|
495 | };
|
496 |
|
497 |
|
498 |
|
499 |
|
500 |
|
501 | AssetProcessor.prototype.importLatestStylesheets = function(callback) {
|
502 |
|
503 | const self = this;
|
504 |
|
505 | const gitToken = self.config.git && self.config.git.token;
|
506 | const imports = self.config.targets && self.config.targets.stylesheets && self.config.targets.stylesheets.imports || {};
|
507 |
|
508 | const destDir = path.resolve(self.config.stylesheetsRoot, imports.destination || '');
|
509 | const sources = imports.sources || [];
|
510 | const mappings = imports.mappings || {};
|
511 | let numImported = 0;
|
512 |
|
513 | if (!fs.existsSync(destDir)) {
|
514 | console.log('Creating import directory '+destDir);
|
515 | fs.mkdirsSync(destDir);
|
516 | }
|
517 |
|
518 | console.log('Importing '+sources.length+' css files to '+destDir);
|
519 | if (Object.keys(mappings).length) {
|
520 | console.log('Will be applying the following remappings:', mappings);
|
521 | }
|
522 |
|
523 | async.eachSeries(sources, function(source, eachNext) {
|
524 | const destPath = path.resolve(destDir, path.basename(source));
|
525 | console.log('Importing '+source+' to '+destPath);
|
526 | _importFromGit(source, destPath, gitToken, function(err) {
|
527 | if (!err) {
|
528 | _applyImportMappings(destPath, mappings, function(err) {
|
529 | if (!err) {
|
530 | numImported += 1;
|
531 | }
|
532 | eachNext(err);
|
533 | });
|
534 | } else {
|
535 | console.error(err);
|
536 | eachNext(err);
|
537 | }
|
538 | });
|
539 | }, function(err) {
|
540 | console.log('Imported '+numImported+' of '+sources.length+' stylesheets');
|
541 | callback(err, numImported);
|
542 | });
|
543 | };
|
544 |
|
545 |
|
546 |
|
547 |
|
548 |
|
549 |
|
550 |
|
551 | function _wrapUploadEmitter(parentEmitter, childEmitter, type) {
|
552 | childEmitter.on('uploadStarted', function(ev) {
|
553 | parentEmitter.emit('uploadStarted', {type: type, source: ev.source, target: ev.target});
|
554 | });
|
555 | childEmitter.on('uploadEnded', function(ev) {
|
556 | parentEmitter.emit('uploadEnded', {type: type, source: ev.source, target: ev.target, url: ev.url});
|
557 | });
|
558 | }
|
559 |
|
560 |
|
561 |
|
562 |
|
563 |
|
564 |
|
565 |
|
566 |
|
567 |
|
568 |
|
569 | AssetProcessor.prototype.ensureAssets = function(callback) {
|
570 |
|
571 | const self = this;
|
572 |
|
573 |
|
574 |
|
575 | async.auto({
|
576 | ensureJs: [function(next) {
|
577 | _checkAndUpdateJavaScript(self, next);
|
578 | }],
|
579 | ensureCss: ['ensureJs', function(next) {
|
580 | _checkAndUpdateCss(self, next);
|
581 | }],
|
582 | ensureImages: ['ensureCss', function(next) {
|
583 | _checkAndUpdateImages(self, next);
|
584 | }],
|
585 | ensureExtras: ['ensureImages', function(next) {
|
586 | _checkAndUpdateExtras(self, next);
|
587 | }]
|
588 | }, function(err, results) {
|
589 | const result = _.extend({}, results && results.ensureJs, results && results.ensureCss, results && results.ensureImages, results && results.ensureExtras);
|
590 | callback(err, result);
|
591 | });
|
592 |
|
593 | };
|
594 |
|
595 | AssetProcessor.prototype.processAssets = function(callback) {
|
596 |
|
597 | const self = this;
|
598 |
|
599 | async.auto({
|
600 | processJs: [function(next) {
|
601 | self.saveJavaScriptToFile(next);
|
602 | }],
|
603 | processCss: [function(next) {
|
604 | self.saveCssToFile(next);
|
605 | }]
|
606 | }, function(err, results) {
|
607 |
|
608 | let result = {};
|
609 |
|
610 | if (results) {
|
611 | result = _.extend({},
|
612 | results.processJs,
|
613 | results.processCss,
|
614 | {imagesUrl: ''},
|
615 | {extrasUrl: ''}
|
616 | );
|
617 | }
|
618 |
|
619 | callback(err, result);
|
620 | });
|
621 |
|
622 | };
|
623 |
|
624 |
|
625 |
|
626 |
|
627 |
|
628 |
|
629 |
|
630 |
|
631 |
|
632 |
|
633 |
|
634 |
|
635 | function _uploadRelativeToRoot(files, activeRoot, targetFolder, s3, callback) {
|
636 | const eventEmitter = new EventEmitter();
|
637 | async.eachSeries(files, function(file, eachNext) {
|
638 | const target = targetFolder+'/'+_stripRoot(file, activeRoot);
|
639 | const headers = {
|
640 | 'x-amz-acl': 'public-read',
|
641 | 'Cache-Control': 'public, max-age=' + SEVEN_DAYS_IN_SECONDS
|
642 | };
|
643 | eventEmitter.emit('uploadStarted', {target: target, source: file});
|
644 | s3.putFile(file, target, headers, function(err, result) {
|
645 | eventEmitter.emit('uploadEnded', {source: file, target: target, url: result});
|
646 | eachNext(err);
|
647 | });
|
648 | }, function(err) {
|
649 | callback(err, s3.urlWithBucket(targetFolder));
|
650 | });
|
651 | return eventEmitter;
|
652 | }
|
653 |
|
654 |
|
655 |
|
656 |
|
657 |
|
658 |
|
659 |
|
660 |
|
661 |
|
662 |
|
663 | function _stripRoot(file, root) {
|
664 | const posixRoot = root && root.replace(/\\/g, '/');
|
665 | file = file.replace(/\\/g, '/');
|
666 | const index = file.indexOf(posixRoot);
|
667 | if (index !== -1) {
|
668 | file = file.substr(posixRoot.length);
|
669 | if (file[0] === '/') {
|
670 |
|
671 | file = file.substr(1);
|
672 | }
|
673 | }
|
674 | return file;
|
675 | }
|
676 |
|
677 |
|
678 |
|
679 |
|
680 |
|
681 |
|
682 |
|
683 |
|
684 |
|
685 | function _getFiles(config, activeRoot, normalizedFullPath, callback) {
|
686 |
|
687 | if (typeof normalizedFullPath === 'function') {
|
688 | callback = normalizedFullPath;
|
689 | normalizedFullPath = false;
|
690 | }
|
691 |
|
692 | async.auto({
|
693 | getRelevantFiles: [function(next) {
|
694 |
|
695 | let directories = config.directories;
|
696 | let specificFiles = config.files;
|
697 |
|
698 | if (!directories && !specificFiles) {
|
699 | directories = activeRoot;
|
700 | }
|
701 | if (directories && !(directories instanceof Array)) {
|
702 | directories = [directories];
|
703 | }
|
704 |
|
705 |
|
706 | directories = _.map(directories || [], function(directory) {
|
707 | return path.resolve(activeRoot, directory);
|
708 | });
|
709 |
|
710 |
|
711 | specificFiles = _.map(specificFiles || [], function(specificFile) {
|
712 | return path.resolve(activeRoot, specificFile);
|
713 | });
|
714 |
|
715 | const extensions = config.extensions || [];
|
716 | const preference = config.preference || [];
|
717 | const exclusions = config.exclude || [];
|
718 |
|
719 | _getRelevantFiles(activeRoot, directories, specificFiles, extensions, preference, exclusions, next);
|
720 | }],
|
721 | normalizeFullPathIfNeeded: ['getRelevantFiles', function(next, results) {
|
722 | let files = results.getRelevantFiles;
|
723 | if (normalizedFullPath) {
|
724 | files = _.map(files, function(file) {
|
725 | if (file[0] !== '/') {
|
726 | return path.normalize(path.join(activeRoot, file));
|
727 | }
|
728 | });
|
729 | }
|
730 | next(null, files);
|
731 | }]
|
732 | }, function(err, results) {
|
733 | callback(err, results && results.normalizeFullPathIfNeeded);
|
734 | });
|
735 | }
|
736 |
|
737 |
|
738 |
|
739 |
|
740 |
|
741 |
|
742 |
|
743 |
|
744 |
|
745 |
|
746 |
|
747 |
|
748 | function _getRelevantFiles(root, directories, specificFiles, extensions, preference, exclusions, callback) {
|
749 |
|
750 | async.auto({
|
751 | files: [function(next) {
|
752 |
|
753 | let files = specificFiles || [];
|
754 | async.each(directories, function(directory, eachNext) {
|
755 | dir.files(directory, function(err, dirFiles) {
|
756 | files = files.concat(dirFiles || []);
|
757 | eachNext(err);
|
758 | });
|
759 | }, function(err) {
|
760 | next(err, files);
|
761 | });
|
762 | }],
|
763 | process: ['files', function(next, results) {
|
764 |
|
765 |
|
766 | let files = _.map(results.files, function(file) {
|
767 | return _stripRoot(file, root);
|
768 | });
|
769 |
|
770 |
|
771 | files = files.filter(function(file) {
|
772 |
|
773 | const extensionMatch = _extensionMatch(file, extensions);
|
774 |
|
775 |
|
776 | const excluded = (exclusions || []).some(function(excludePath) {
|
777 | return file.toLowerCase().indexOf(excludePath.toLowerCase()) === 0;
|
778 | });
|
779 |
|
780 |
|
781 | const included = (preference || []).some(function(preference) {
|
782 | return file.toLowerCase() === preference.toLowerCase();
|
783 | });
|
784 |
|
785 | let include = false;
|
786 | if (included) {
|
787 | include = true;
|
788 | } else if (extensionMatch) {
|
789 | include = !excluded;
|
790 | }
|
791 |
|
792 |
|
793 | return include;
|
794 | });
|
795 |
|
796 |
|
797 | preference = (preference || []).concat(_.map(specificFiles, function(specificFile) {
|
798 | return _stripRoot(specificFile, root);
|
799 | }));
|
800 |
|
801 |
|
802 | files.sort(function(a,b) {
|
803 | const preferenceDiff = _preference(preference, a) - _preference(preference, b);
|
804 | return preferenceDiff !== 0 ? preferenceDiff : a < b ? -1 : a > b ? 1 : 0;
|
805 | });
|
806 |
|
807 |
|
808 | preference.forEach(function(preferencePath) {
|
809 | const fullPath = path.resolve(root, preferencePath);
|
810 |
|
811 | if (!fs.existsSync(fullPath) && (!path.extname(fullPath) || _extensionMatch(fullPath, extensions))) {
|
812 | console.warn('Warning: '+preferencePath+' was in configuration but cannot be found');
|
813 | }
|
814 | });
|
815 |
|
816 | next(null, files);
|
817 | }]
|
818 | }, function(err, results) {
|
819 | callback(err, results && results.process);
|
820 | });
|
821 | }
|
822 |
|
823 |
|
824 |
|
825 |
|
826 |
|
827 |
|
828 |
|
829 |
|
830 | function _preference(preferences, fileName) {
|
831 |
|
832 | let i;
|
833 | let currPreference;
|
834 |
|
835 | fileName = fileName.toLowerCase();
|
836 |
|
837 | for (i = 0; i < preferences.length; i += 1) {
|
838 | currPreference = preferences[i].toLowerCase();
|
839 | if (path.extname(currPreference)) {
|
840 |
|
841 | if (currPreference === fileName) {
|
842 | break;
|
843 | }
|
844 | } else {
|
845 |
|
846 |
|
847 | if (path.relative(currPreference, fileName).indexOf('..') === -1) {
|
848 | break;
|
849 | }
|
850 | }
|
851 | }
|
852 |
|
853 | return i;
|
854 | }
|
855 |
|
856 |
|
857 |
|
858 |
|
859 |
|
860 |
|
861 |
|
862 | function _hashFiles(files, callback) {
|
863 |
|
864 | const hash = crypto.createHash('md5');
|
865 |
|
866 | async.eachSeries(files, function(file, eachNext) {
|
867 | fs.readFile(file, function(err, data) {
|
868 | hash.update(data);
|
869 | eachNext(err);
|
870 | });
|
871 | }, function(err) {
|
872 | callback(err, !err && files.length && hash.digest('hex'));
|
873 | });
|
874 | }
|
875 |
|
876 | function _getJavaScriptPath(files, callback) {
|
877 | _hashAndGeneratePath(files, 'js', '.js', callback);
|
878 | }
|
879 |
|
880 | function _getCssPath(files, callback) {
|
881 | _hashAndGeneratePath(files, 'css', '.css', callback);
|
882 | }
|
883 |
|
884 | function _getImageFolder(imagesRootRelative, callback) {
|
885 | return _firstDirNameFromRootPath(imagesRootRelative, 'img', callback);
|
886 | }
|
887 |
|
888 | function _getExtrasFolder(extrasRootRelative, callback) {
|
889 | return _firstDirNameFromRootPath(extrasRootRelative, 'extra', callback);
|
890 | }
|
891 |
|
892 | function _firstDirNameFromRootPath(rootPath, defaultDirName, callback) {
|
893 | const folder = path.sep+(_firstDirName(rootPath) || defaultDirName);
|
894 | if (callback) {
|
895 | callback(null, folder);
|
896 | }
|
897 | return folder;
|
898 | }
|
899 |
|
900 | function _firstDirName(dirPath) {
|
901 | dirPath = (dirPath || '').replace(/[\\\/]/g, path.sep);
|
902 |
|
903 | if (dirPath[0] === path.sep) {
|
904 | dirPath = dirPath.substr(1);
|
905 | }
|
906 |
|
907 | return dirPath.split(path.sep)[0] || '';
|
908 | }
|
909 |
|
910 | function _hashAndGeneratePath(files, path, extension, callback) {
|
911 | async.auto({
|
912 | hashFiles: [function(next) {
|
913 | _hashFiles(files, next);
|
914 | }],
|
915 | path: ['hashFiles', function(next, results) {
|
916 | const hash = results.hashFiles;
|
917 | const uri = path+'/'+hash+extension;
|
918 | next(null, uri);
|
919 | }]
|
920 | }, function(err, results) {
|
921 | callback(err, results && results.path);
|
922 | });
|
923 | }
|
924 |
|
925 | function _checkAndUpdateJavaScript(self, callback) {
|
926 |
|
927 | async.auto({
|
928 | getJsFiles: [function (next) {
|
929 | self.getJavaScriptFiles(true, next);
|
930 | }],
|
931 | getJsPath: ['getJsFiles', function (next, results) {
|
932 | _getJavaScriptPath(results.getJsFiles, next);
|
933 | }],
|
934 | checkJs: ['getJsPath', function (next, results) {
|
935 | self.s3.fileExists(results.getJsPath, next);
|
936 | }],
|
937 | updateJsIfNeeded: ['checkJs', function (next, results) {
|
938 | const jsChanged = !results.checkJs;
|
939 |
|
940 | self.emit('filesChecked', {type: 'js', changed: jsChanged});
|
941 |
|
942 | if (jsChanged || self.forceCdnUpdate) {
|
943 |
|
944 | self.uploadJavaScriptToCdn(function (err, newUrl) {
|
945 | next(err, newUrl);
|
946 | });
|
947 | } else {
|
948 |
|
949 | next(null, self.s3.urlWithBucket(results.getJsPath));
|
950 | }
|
951 | }]
|
952 | }, function(err, results) {
|
953 | const result = {
|
954 | jsUrl: results && results.updateJsIfNeeded,
|
955 | jsChanged: results && !results.checkJs
|
956 | };
|
957 | callback(err, result);
|
958 | });
|
959 | }
|
960 |
|
961 | function _checkAndUpdateCss(self, callback) {
|
962 |
|
963 | async.auto({
|
964 | getCssFiles: [function (next) {
|
965 | self.getCssFiles(true, next);
|
966 | }],
|
967 | getCssPath: ['getCssFiles', function (next, results) {
|
968 | _getCssPath(results.getCssFiles, next);
|
969 | }],
|
970 | checkCss: ['getCssPath', function (next, results) {
|
971 | self.s3.fileExists(results.getCssPath, next);
|
972 | }],
|
973 | updateCssIfNeeded: ['checkCss', function (next, results) {
|
974 | const cssChanged = !results.checkCss;
|
975 |
|
976 | self.emit('filesChecked', {type: 'css', changed: cssChanged});
|
977 |
|
978 | if (cssChanged || self.forceCdnUpdate) {
|
979 |
|
980 | self.uploadCssToCdn(function (err, newUrl) {
|
981 | next(err, newUrl);
|
982 | });
|
983 | } else {
|
984 |
|
985 | next(null, self.s3.urlWithBucket(results.getCssPath));
|
986 | }
|
987 | }]
|
988 | }, function(err, results) {
|
989 | const result = {
|
990 | cssUrl: results && results.updateCssIfNeeded,
|
991 | cssChanged: results && !results.checkCss
|
992 | };
|
993 | callback(err, result);
|
994 | });
|
995 | }
|
996 |
|
997 | function _checkAndUpdateImages(self, callback) {
|
998 | _checkRelativeToRoot(self, 'image', callback);
|
999 | }
|
1000 |
|
1001 | function _checkAndUpdateExtras(self, callback) {
|
1002 | _checkRelativeToRoot(self, 'extra', callback);
|
1003 | }
|
1004 |
|
1005 | function _checkRelativeToRoot(self, type, callback) {
|
1006 |
|
1007 | async.auto({
|
1008 | getFiles: [function(next) {
|
1009 | if (type === 'image') {
|
1010 | self.getImageFiles(true, next);
|
1011 | } else {
|
1012 | self.getExtraFiles(true, next);
|
1013 | }
|
1014 | }],
|
1015 | getFolder: [function(next) {
|
1016 | if (type === 'image') {
|
1017 | _getImageFolder(self.config.imagesRootRelative, next);
|
1018 | } else {
|
1019 | _getExtrasFolder(self.config.extrasRootRelative, next);
|
1020 | }
|
1021 | }],
|
1022 | hashAndGeneratePath: ['getFiles', 'getFolder', function(next, results) {
|
1023 | _hashAndGeneratePath(results.getFiles, results.getFolder, '.txt', next);
|
1024 | }],
|
1025 | checkFiles: ['hashAndGeneratePath', function(next, results) {
|
1026 | self.s3.fileExists(results.hashAndGeneratePath, next);
|
1027 | }],
|
1028 | updateIfNeeded: ['checkFiles', function(next, results) {
|
1029 | const filesChanged = !results.checkFiles;
|
1030 | self.emit('filesChecked', {type: type, changed: filesChanged});
|
1031 |
|
1032 | if (filesChanged || self.forceCdnUpdate) {
|
1033 | if (type === 'image') {
|
1034 | self.uploadImagesToCdn(next);
|
1035 | } else {
|
1036 | self.uploadExtrasToCdn(next);
|
1037 | }
|
1038 | } else {
|
1039 | next(null, self.s3.urlWithBucket(results.getFolder));
|
1040 | }
|
1041 | }],
|
1042 | uploadIndicatorFileIfNeeded: ['updateIfNeeded', function(next, results) {
|
1043 | const targetPath = results.hashAndGeneratePath;
|
1044 | if (!results.checkFiles) {
|
1045 | self.emit('uploadStarted', {type: type, target: targetPath, source: 'memory'});
|
1046 | self.s3.putBuffer(results.getFiles.join('\n'), targetPath, function(err, result) {
|
1047 | self.emit('uploadEnded', {type: type, target: targetPath, source: 'memory', url: result});
|
1048 | next(err);
|
1049 | });
|
1050 | } else {
|
1051 | next();
|
1052 | }
|
1053 | }]
|
1054 | }, function(err, results) {
|
1055 | const result = {};
|
1056 |
|
1057 |
|
1058 | type = type+'s';
|
1059 |
|
1060 | result[type+'Url'] = results && results.updateIfNeeded;
|
1061 | result[type+'Changed'] = !results.checkFiles;
|
1062 |
|
1063 | callback(err, result);
|
1064 | });
|
1065 |
|
1066 | }
|
1067 |
|
1068 |
|
1069 |
|
1070 |
|
1071 |
|
1072 |
|
1073 |
|
1074 |
|
1075 |
|
1076 |
|
1077 |
|
1078 | function _rebaseUrls(self, file, css) {
|
1079 |
|
1080 | const cssRoot = self.config.stylesheetsRoot;
|
1081 |
|
1082 | let normalizedUrl;
|
1083 | let assetPath;
|
1084 | let rebasedUrl;
|
1085 |
|
1086 |
|
1087 | const urlRegex = /url\((['"]?([^'"\)]+)['"]?)\)/ig;
|
1088 | const externalHttpRegex = /^(http(s)?:)?\/\//i;
|
1089 | const absoluteUrlRegex = /^[\/\\][^\/]/;
|
1090 | const base64Regex = /^data:.+;base64/i;
|
1091 |
|
1092 | let match;
|
1093 | let url;
|
1094 | let startIndex;
|
1095 | let endIndex;
|
1096 |
|
1097 | file = path.normalize(file);
|
1098 |
|
1099 | while ((match = urlRegex.exec(css)) !== null) {
|
1100 | url = match[1];
|
1101 | startIndex = match.index+match[0].indexOf(url);
|
1102 | endIndex = startIndex + url.length;
|
1103 |
|
1104 |
|
1105 | url = url.replace(/^['"]/, '').replace(/['"]$/, '');
|
1106 |
|
1107 |
|
1108 | if (!externalHttpRegex.test(url) && !base64Regex.test(url)) {
|
1109 |
|
1110 |
|
1111 | normalizedUrl = path.normalize(url);
|
1112 |
|
1113 |
|
1114 | if (absoluteUrlRegex.test(normalizedUrl)) {
|
1115 | |
1116 |
|
1117 |
|
1118 |
|
1119 |
|
1120 |
|
1121 |
|
1122 | rebasedUrl = normalizedUrl
|
1123 | } else {
|
1124 | |
1125 |
|
1126 |
|
1127 |
|
1128 |
|
1129 |
|
1130 |
|
1131 |
|
1132 |
|
1133 |
|
1134 |
|
1135 | rebasedUrl = path
|
1136 | .resolve(path.dirname(file), normalizedUrl)
|
1137 | .replace(cssRoot, '');
|
1138 | }
|
1139 |
|
1140 |
|
1141 | css = css.substr(0, startIndex) + rebasedUrl + css.substr(endIndex);
|
1142 |
|
1143 |
|
1144 | urlRegex.lastIndex = endIndex;
|
1145 | }
|
1146 | }
|
1147 |
|
1148 | return css;
|
1149 | }
|
1150 |
|
1151 |
|
1152 |
|
1153 |
|
1154 |
|
1155 |
|
1156 |
|
1157 |
|
1158 |
|
1159 | function _importFromGit(sourceUrl, destPath, accessToken, callback) {
|
1160 |
|
1161 | const apiUrl = _toGitHubApiSource(sourceUrl);
|
1162 | const requestOptions = {
|
1163 | url: apiUrl,
|
1164 | headers: {
|
1165 | Authorization: 'token '+accessToken,
|
1166 | Accept: 'application/vnd.github.v3.raw',
|
1167 | 'User-Agent': 'NodeBot (+http://videoblocks.com)'
|
1168 | }
|
1169 | };
|
1170 |
|
1171 | const ws = fs.createWriteStream(destPath);
|
1172 |
|
1173 |
|
1174 | const req = request(requestOptions);
|
1175 | let statusCode;
|
1176 | let reqErr;
|
1177 |
|
1178 | req.on('response', function(response) {
|
1179 | statusCode = response.statusCode;
|
1180 | });
|
1181 |
|
1182 | req.on('end', function() {
|
1183 | const success = statusCode === 200;
|
1184 | reqErr = reqErr || (!success ? new Error('Error requesting '+apiUrl+'; status code '+statusCode) : null);
|
1185 | });
|
1186 |
|
1187 | req.on('error', function(err) {
|
1188 | reqErr = reqErr || err;
|
1189 | });
|
1190 |
|
1191 | ws.on('close', function() {
|
1192 | const success = statusCode === 200;
|
1193 | callback(reqErr, success);
|
1194 | });
|
1195 |
|
1196 | req.pipe(ws);
|
1197 | }
|
1198 |
|
1199 | function _applyImportMappings(filePath, mappings, callback) {
|
1200 |
|
1201 | async.auto({
|
1202 | readFile: [function(next) {
|
1203 | fs.readFile(filePath, 'utf8', next);
|
1204 | }],
|
1205 | replaceMappings: ['readFile', function(next, results) {
|
1206 | let css = results.readFile;
|
1207 |
|
1208 | Object.keys(mappings || {}).forEach(function(importedRoot) {
|
1209 | const search = _escapeRegExp(importedRoot);
|
1210 | const replacement = mappings[importedRoot];
|
1211 | css = css.replace(new RegExp(search, 'g'), replacement);
|
1212 | });
|
1213 |
|
1214 | next(null, css);
|
1215 | }],
|
1216 | writeFile: ['replaceMappings', function(next, results) {
|
1217 | const remappedCss = results.replaceMappings;
|
1218 | fs.writeFile(filePath, remappedCss, next);
|
1219 | }]
|
1220 | }, function(err) {
|
1221 | callback(err);
|
1222 | });
|
1223 | }
|
1224 |
|
1225 |
|
1226 |
|
1227 |
|
1228 |
|
1229 |
|
1230 |
|
1231 |
|
1232 |
|
1233 |
|
1234 | function _toGitHubApiSource(sourceUrl) {
|
1235 |
|
1236 |
|
1237 | const gitHubBlobUrlRegex = /^https?:\/\/github\.com\/([^\/]+)\/([^\/]+)\/blob\/[^\/]+\/(.*)$/;
|
1238 | const match = gitHubBlobUrlRegex.exec(sourceUrl);
|
1239 | let org;
|
1240 | let repo;
|
1241 | let path;
|
1242 | if (match) {
|
1243 |
|
1244 | org = match[1];
|
1245 | repo = match[2];
|
1246 | path = match[3];
|
1247 | sourceUrl = 'https://api.github.com/repos/'+org+'/'+repo+'/contents/'+path;
|
1248 | }
|
1249 |
|
1250 | if (!sourceUrl.match(/^https?:\/\/api\.github\.com\/repos\//)) {
|
1251 | throw new Error('Invalid github import source URL: '+sourceUrl);
|
1252 | }
|
1253 |
|
1254 | return sourceUrl;
|
1255 | }
|
1256 |
|
1257 |
|
1258 |
|
1259 |
|
1260 |
|
1261 |
|
1262 |
|
1263 |
|
1264 |
|
1265 | function _extensionMatch(filePath, extensions) {
|
1266 | filePath = (filePath || '').toLowerCase();
|
1267 | return !extensions || !extensions.length || extensions.some(function(extension) {
|
1268 | return path.extname(filePath).toLowerCase() === extension.toLowerCase();
|
1269 | });
|
1270 | }
|
1271 |
|
1272 | function _escapeRegExp(string) {
|
1273 | return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
|
1274 | }
|
1275 |
|
1276 | module.exports = AssetProcessor;
|