src/index.js
import { existsSync, readFile, stat } from 'fs';
import { join, relative } from 'path';
import { gzip } from 'zlib';
import Promise, { promisify, resolve } from 'bluebird';
import { green, grey, stripColor, yellow } from 'chalk';
import filesize from 'filesize';
import { isArray, isUndefined, merge } from 'lodash';
import Plugin from 'broccoli-plugin';
import rimraf from 'rimraf';
import symlinkOrCopy from 'symlink-or-copy';
import walk from 'walk';
const [preadFile, pstat, pgzip] = [readFile, stat, gzip].map(promisify);
/**
* A Broccoli plugin that display sizes of all files found in its input path.
*/
export default class FileSizePlugin extends Plugin {
/**
* Create a new FileSizePlugin instances
* @param {Node|Node[]} inputNodes Input nodes, see `broccoli-plugin` docs
* @param {Object} options Plugin options
*/
constructor(inputNodes, options) {
super(isArray(inputNodes) ? inputNodes : [inputNodes], options);
/**
* Broccoli plugin options
* @type {Object}
* @property {Boolean} [gzipped=true] Whether to display gzipped size or not
* @property {Boolean} [colors=true] Whether to use colors or not
*/
this.options = merge({
gzipped: true,
colors: true,
}, options);
}
/**
* Builds this node.
*
* Symlinks the first input path to this node's output path, then look for all
* files in the input path, and process each of them by either:
* - reading the file's contents and calculating its gzipped sie (when
* `gzipped` options is `true`)
* - or, stating the file (if `gzipped` option is `false`).
* Then, display size(s) on standard output.
*
* @return {Promise} A new Promise that is resolved once all input files have
* been processed, and sizes have been displayed. Alternatively, it can be
* rejected in case of errors.
*/
build() {
const [inputPath] = this.inputPaths;
// Symlink/copy input -> output
this.symlinkOrCopy(inputPath, this.outputPath);
// Process output directory
return this.listFiles(inputPath, relativePath =>
this.processFile(join(inputPath, relativePath))
.then(sizes => this.print(relativePath, ...sizes))
);
}
/**
* Lists files of a directory recursively. Follows symbolic links.
* @param {String} dir Directory to scan
* @param {Function()} callback Callback executed when a file is found. Can return a Promise.
* @return {Promise} A new promise that is resolved once the given directory
* has been scanned entirely and all callbacks have completed.
*/
listFiles(dir, callback) {
return new Promise((resolveThis, reject) => {
walk.walk(dir, { followLinks: true })
.on('file', (root, stats, next) => {
const destDir = relative(dir, root);
const relativePath = destDir ? join(destDir, stats.name) : stats.name;
resolve(relativePath).then(callback).then(next);
})
.on('errors', reject)
.on('end', resolveThis);
});
}
/**
* Symlink or copy a directory
* @param {String} dir Path to an existing
* @param {String} target Path of the symlink to create
*/
symlinkOrCopy(dir, target) {
try {
symlinkOrCopy.sync(dir, target);
} catch (e) {
if (existsSync(target)) {
rimraf.sync(target);
}
symlinkOrCopy.sync(dir, target);
}
}
/**
* Either reads the given file's contents and calculating its gzipped sie
* (when `gzipped` options is `true`), or stats the given file
* (if `gzipped` option is `false`).
* @param {String} absolutePath Absolute path to the file
* @return {Promise} A new Promise that is resolved when file has been
* processed. The resolved value is an array containing the file size and the
* gzipped file size (or undefined)
*/
processFile(absolutePath) {
// Stats the file if `gzipped` option is set to false
if (!this.options.gzipped) {
return pstat(absolutePath).then(stats => [stats.size]);
}
// Otherwise, reads the file contents and calculate gzipped size
return preadFile(absolutePath)
.then(contents => [contents, pgzip(contents)])
.then(buffers => buffers.map(buffer => buffer.toString().length));
}
/**
* Prints calculated sizes
* @param {String} relativePath Relative path of the input file
* @param {Number]} size Size of the input file in bytes
* @param {Number} [gzippedSize] Size of the gzipped version of the file
*/
print(relativePath, size, gzippedSize) {
let message = `${yellow(relativePath)} => ${green(filesize(size))}`;
if (!isUndefined(gzippedSize)) {
message += grey(` (${filesize(gzippedSize)} gzipped)`);
}
if (!this.options.colors) {
message = stripColor(message);
}
process.stdout.write(`${message}\n`);
}
}