src/Linker/Linker.js
import chalk from "chalk";
import fs from "fs";
import nodePath from "path";
import ProgressBar from "progress";
import Logger from "../Logger";
/**
* Strings used in composing messages.
* @type {{ linking: Verbs, unlinking: Verbs }}
*/
export const verbs = {
linking: { present: "Link", past: "Linked", gerund: "Linking" },
unlinking: { present: "Unlink", past: "Unlinked", gerund: "Unlinking" }
};
/**
* Handles creating and destroying symlinks.
*/
export default class Linker {
/**
* Paths and associated errors.
* @type {LinkError[]}
*/
errorList = [];
/**
* Number of files and directories.
* @type {number}
*/
total;
/**
* Length of longest path.
* @type {number}
*/
maxLength;
/**
* Progress bar.
* @type {ProgressBar}
*/
bar;
/**
* Current working directory, used to build absolute paths.
* @type {string}
*/
cwd = process.cwd();
/**
* Verbs to use in composing messages.
* @type {Verbs}
*/
verbs = {};
/**
* Command arguments.
* @type {Cmd}
*/
cmd;
/**
* Config to pull linkables from.
* @type {Config}
*/
config = { link: { files: [], directories: [] } };
/**
* logger#log is used to output progress, e.g. {@link Logger#log} or {@link console#log}.
* @type {LogObject}
*/
logger;
/**
* @param {Cmd} cmd
* @param {Config} [config=this.config]
* @param {LogObject} [logger=new Logger()]
*/
constructor(cmd, config, logger = new Logger()) {
this.cmd = cmd;
this.config = config || this.config;
this.logger = logger;
this.total = this.config.link.files.length + this.config.link.directories.length;
this.maxLength = this.getMaxLength();
this.bar = new ProgressBar("\n:bar [:current/:total]", {
total: this.total
});
}
/**
* Links items and outputs progress.
*
* @param {Config} config
* @property {string[]} config.files
* @property {string[]} config.directories
* @returns {Linker}
*/
link({ files = [], directories = [] } = this.config.link) {
this.verbs = verbs.linking;
files.forEach(path => this.makeLink(path, "file"));
directories.forEach(path => this.makeLink(path, "dir"));
this.errorList.length ? this.outputErrors() : this.outputSuccess();
return this;
}
/**
* Unlinks items and outputs progress.
*
* @param {Config} config
* @property {string[]} config.files
* @property {string[]} config.directories
* @returns {Linker}
*/
unlink({ files = [], directories = [] } = this.config.link) {
this.verbs = verbs.unlinking;
files.forEach(this.destroyLink);
directories.forEach(this.destroyLink);
this.errorList.length ? this.outputErrors() : this.outputSuccess();
return this;
}
/**
* Destroys symlink and outputs progress.
*
* @method
* @param {string} path - Path to file or directory
*/
destroyLink = path => {
const { mybb_root } = this.config;
let error = null;
try {
fs.unlinkSync(nodePath.resolve(mybb_root, path));
} catch (e) {
error = e;
this.errorList.push({ path, error });
}
this.outputProgress(path, error);
};
/**
* Creates symlink and outputs progress.
*
* @method
* @param {string} path - Path to file or directory
* @param {string} [type="file"] - <code>file</code> or <code>dir</code>
*/
makeLink = (path, type = "file") => {
const { mybb_root } = this.config;
let error = null;
try {
fs.symlinkSync(
nodePath.resolve(this.cwd, path),
nodePath.resolve(mybb_root, path),
type
);
} catch (e) {
error = e;
this.errorList.push({ path, error });
}
this.outputProgress(path, error);
};
/**
* Outputs status message and updates progress bar.
* @param {string} path
* @param {?Error} [error]
*/
outputProgress(path, error) {
const str = this.progress(path, error);
this.bar.interrupt(str);
this.bar.tick();
}
/**
* Builds progress message in format `(Un)Linking... to/a/path Status`
*
* @param {string} path
* @param {?Error} [error]
* @returns {string}
*/
progress(path, error) {
const verb = this.verbs.gerund;
let suffix = error ? chalk.red(error.code) : chalk.green("Success");
let str = path.padEnd(this.maxLength);
str = `${str} ${suffix}`;
return chalk.gray(`${verb}... `) + str;
}
/**
* Builds error messages.
*
* @returns {string[]}
*/
errors() {
const { verbose } = this.cmd;
const verb = this.verbs.present.toLowerCase();
let str = [chalk.red(`\nFailed to ${verb} ${this.errorList.length} files or directories`)];
if (verbose) {
this.errorList.forEach(({ path, error }) => {
str.push(`\n${path}\n`);
str.push(error);
});
} else {
str.push(chalk.gray("Rerun with -v for verbose error messages."));
}
return str;
}
/**
* Builds success message.
*
* @returns {string}
*/
success() {
const verb = this.verbs.past.toLowerCase();
return chalk.green(`\nSuccessfully ${verb} ${this.total} files and directories`);
}
/**
* Outputs error messages.
*/
outputErrors() {
const str = this.errors();
str.forEach(err => this.logger.log(err));
}
/**
* Outputs success message.
*/
outputSuccess() {
const str = this.success();
this.logger.log(str);
}
/**
* Calculates length of longest file or directory name.
*
* @returns {number}
*/
getMaxLength() {
const { files, directories } = this.config.link;
return [...files, ...directories].reduce((a, b) => (a.length > b.length ? a : b), [])
.length;
}
}