UNPKG

@maddimathon/utility-typescript

Version:

TypeScript utilities (types, functions, classes) to use in various projects.

478 lines (477 loc) 13.6 kB
/** * @since 0.2.0 * * @packageDocumentation */ /*! * @maddimathon/utility-typescript@2.0.0-beta.5 * @license MIT */ import NodeFS from 'node:fs'; import NodePath from 'node:path'; import { arrayUnique } from '../../functions/arrays/arrayUnique.js'; import { mergeArgs } from '../../functions/objects/mergeArgs.js'; import { NodeConsole } from './NodeConsole.js'; /** * A configurable class for working with files and paths in node. * * @since 0.2.0 */ export class NodeFiles { /* LOCAL PROPERTIES * ====================================================================== */ /** * A completed args object. * * @category Args */ args; /** * The instance of {@link NodeConsole} used within this class. * * @category Classes */ nc; /* Args ===================================== */ /** * Default args for this stage. * * @category Args * * @source */ get ARGS_DEFAULT() { return { copyFile: { force: true, rename: true, recursive: false, }, root: './', readDir: { recursive: false, }, readFile: {}, write: { force: false, rename: false, }, }; } /* CONSTRUCTOR * ====================================================================== */ constructor(args = {}, utils = {}) { this.args = mergeArgs(this.ARGS_DEFAULT, args, true); this.nc = utils.nc ?? new NodeConsole(this.ARGS_DEFAULT); const propNames = arrayUnique(Object.getOwnPropertyNames(NodeFiles.prototype) .concat(Object.getOwnPropertyNames(this.constructor.prototype))); for (const _name of propNames) { // continues on match switch (_name) { case 'args': case 'ARGS_DEFAULT': case 'constructor': case 'nc': continue; } // continues if (typeof this[_name] !== 'function') { continue; } // @ts-expect-error this[_name] = this[_name].bind(this); } } /* METHODS * ====================================================================== */ /* Files ===================================== */ /** * Copies a file to another path. * * @category Filers * * @param source Location to write file. * @param destination Location to copy the source path to. * @param args Optional configuration. * * @return Path to file if written, or false on failure. * * @since 2.0.0-alpha * @since 2.0.0-alpha.1 — Now checks that the file exists first. If so, * returns ''. * * @experimental */ copyFile(source, destination, args = {}) { source = this.pathResolve(source); destination = this.pathResolve(destination); args = mergeArgs(this.args.copyFile, args, false); // returns if (!this.exists(source)) { return false; } // returns if we aren't forcing or renaming if (this.exists(destination)) { // returns if (!args.force && !args.rename) { return false; } if (args.force) { this.delete([destination]); } else if (args.rename) { destination = this.uniquePath(destination); } } // returns if (!args.recursive && this.isDirectory(source)) { this.mkdir(destination); return this.exists(destination) && destination; } const destinationDir = this.dirname(destination); if (!this.exists(destinationDir)) { this.mkdir(destinationDir); } NodeFS.cpSync(source, destination, { ...args, errorOnExist: false, mode: undefined, preserveTimestamps: true, // verbatimSymlinks: true, }); return (this.exists(destination) || this.isSymLink(destination)) && destination; } /** * Deletes given files. * * @category Filers * * @param paths Paths to delete. Absolute or relative to root dir. * @param dryRun If true, files that would be deleted are printed to the console and not deleted. * @param logLevel Base depth for console messages (via NodeConsole). * * @since 2.0.0-alpha — Renamed to delete from deleteFiles. */ delete(paths, logLevel = 0, dryRun = false) { for (const path of paths) { // continues if (!this.exists(path)) { continue; } const stat = this.getStats(path); // continues if (!stat) { continue; } if (stat.isDirectory()) { if (dryRun) { this.nc.timestamp.log('deleting directory: ' + this.pathRelative(path).replace(' ', '%20'), { depth: logLevel, linesIn: 0, linesOut: 0, maxWidth: null }); } else { NodeFS.rmSync(path, { recursive: true, force: true }); } } else if (stat.isFile() || stat.isSymbolicLink()) { if (dryRun) { this.nc.timestamp.log('deleting file: ' + this.pathRelative(path).replace(' ', '%20'), { depth: logLevel, linesIn: 0, linesOut: 0, maxWidth: null }); } else { NodeFS.unlinkSync(path); } } } } /** * Gets the path dirname via {@link node:fs.dirname}. * * @category Path-makers * * @since 2.0.0-alpha * * @experimental */ dirname(path) { return NodePath.dirname(path); } /** * Gets the NodeFS.Stats value for the given path. * * @category Meta * * @since 2.0.0-alpha * * @experimental */ getStats(path) { if (!this.exists(path)) { return undefined; } return NodeFS.statSync(path); } /** * Checks if the given path is a directory. * * @category Meta * * @since 2.0.0-alpha * * @experimental */ isDirectory(path) { return this.getStats(path)?.isDirectory() ?? false; } /** * Checks if the given path is a file. * * @category Meta * * @since 2.0.0-alpha * * @experimental */ isFile(path) { return this.getStats(path)?.isFile() ?? false; } /** * Checks if the given path is a symbolic link. * * @category Meta * * @since 2.0.0-beta.3 * * @experimental */ isFileOrSymLink(path) { const stats = this.getStats(path); // returns if (!stats) { return false; } return stats.isFile() || stats.isSymbolicLink(); } /** * Checks if the given path is a symbolic link. * * @category Meta * * @since 2.0.0-alpha * * @experimental */ isSymLink(path) { return this.getStats(path)?.isSymbolicLink() ?? false; } /** * Creates a directory. * * @category Filers * * @since 2.0.0-alpha * @since 2.0.0-alpha.1 — Removed args param and now forces recursion. * * @experimental */ mkdir(path) { return NodeFS.mkdirSync(path, { recursive: true, }); } /** * Read the paths within a directory. * * @category Filers * * @param path Directory to read. * @param args Optional configuration. * * @return Paths within the given directory. * * @since 2.0.0-alpha * @since 2.0.0-alpha.1 — Now checks that the file exists first. If so, * returns ''. * * @experimental */ readDir(path, args = {}) { path = this.pathResolve(path); // returns if (!this.exists(path)) { return []; } args = mergeArgs(this.args.readDir, args, false); return NodeFS.readdirSync(path, { ...args, withFileTypes: false, encoding: 'utf-8', }).filter(path => !path.match(/(^|\/)\._/g)); } /** * Reads a file. * * @category Filers * * @param path File to read. * @param args Optional configuration. * * @return Contents of the file. * * @since 2.0.0-alpha.1 — Now checks that the file exists first. If so, returns ''. */ readFile(path, args = {}) { path = this.pathResolve(path); // returns if (!this.isFileOrSymLink(path)) { return ''; } args = mergeArgs(this.args.readFile, args, false); return NodeFS.readFileSync(path, { ...args, encoding: 'utf-8', }); } /** * Writes a file. * * @category Filers * * @param path Location to write file. * @param content Contents to write. * @param args Optional configuration. * * @return Path to file if written, or false on failure. * * @since 2.0.0-alpha — Renamed to write from writeFiles. */ write(path, content, args = {}) { path = this.pathResolve(path); content = Array.isArray(content) ? content.join('\n') : content; args = mergeArgs(this.args.write, args, false); // returns if we aren't forcing or renaming if (this.exists(path)) { // returns if (!args.force && !args.rename) { return false; } if (args.rename) { path = this.uniquePath(path); } } this.mkdir(this.dirname(path)); NodeFS.writeFileSync(path, content, args); return this.exists(path) && path; } /* Paths ===================================== */ /** * Gets the basename of the given path. * * @category Path-makers * * @since 2.0.0-alpha * * @experimental */ basename(path, suffix) { if (suffix === false) { suffix = undefined; } else if (!suffix) { suffix = NodePath.extname(path) || undefined; } return NodePath.basename(path, suffix); } /** * Changes just the file name of a path * * @category Path-makers * * @param path * @param newName * * @return Full path with updated basename. */ changeBaseName(path, newName) { const isRelative = !path.match(/^\//g); const newPath = this.pathResolve(this.dirname(path), newName + NodePath.extname(path)); return isRelative ? this.pathRelative(newPath) : newPath; } /** * Checks whether a file, directory, or link exists at the given path. * * @category Path-makers * * @since 2.0.0-alpha * * @experimental */ exists(path) { return NodeFS.existsSync(this.pathResolve(path)); } /** * Returns relative paths, based on the root defined the the opts. * * @category Path-makers * * @param path Path to make relative. */ pathRelative(path) { const relative = NodePath.relative(this.pathResolve(), path); return relative ? relative : '.'; } /** * Resolves relative to the root defined the the opts. * * @category Path-makers * * @param paths Paths to resolve. */ pathResolve(...paths) { return NodePath.resolve(this.args.root, ...paths); } /** * Returns a unique version of the inputPath (i.e., where no file exists) by * appending a number. * * @category Path-makers * * @see {@link NodeFiles.changeBaseName} Used to update the basename to test for uniqueness. * * @param inputPath Path to make unique. * * @return Absolute, unique version of the given `inputPath`. */ uniquePath(inputPath) { inputPath = this.pathResolve(inputPath); if (!this.exists(inputPath)) { return inputPath; } /** Used to iterate until we have a unique path. */ const inputBaseName = NodePath.basename(inputPath, NodePath.extname(inputPath) || undefined); /** Used as a base to append copyIndex to. */ const inputBaseNameWithoutNumber = inputBaseName.replace(/-(\d+)$/gi, ''); /** Copy index - a number to append to OG name. */ let copyIndex = Number(inputBaseName.replace(/^.+-(\d+)$/gi, '$1')) || 0; /** Full path with the updated unique basename. */ let uniqueFullPath = inputPath; // Iterate the index until the inputPath is unique while (this.exists(uniqueFullPath)) { // increments here because the index starts at 0 copyIndex++; uniqueFullPath = this.changeBaseName(inputPath, inputBaseNameWithoutNumber + `-${copyIndex}`); } return uniqueFullPath; } } /** * Used only for {@link NodeFiles}. * * @since 0.2.0 */ (function (NodeFiles) { ; ; ; ; ; })(NodeFiles || (NodeFiles = {}));