destiny
Version:
Prettier for file structures
1,049 lines (828 loc) • 31.7 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
require('core-js/modules/es.array.flat');
require('core-js/modules/es.array.unscopables.flat');
require('core-js/modules/es.promise');
var fs = _interopDefault(require('fs'));
var os = _interopDefault(require('os'));
var glob = _interopDefault(require('glob'));
var cosmiconfig = require('cosmiconfig');
var fs$1 = require('fs-extra');
var fs$1__default = _interopDefault(fs$1);
var path = _interopDefault(require('path'));
var chalk = _interopDefault(require('chalk'));
var Git = _interopDefault(require('simple-git/promise'));
var resolve = _interopDefault(require('resolve'));
require('core-js/modules/es.symbol.description');
let loggerStdout = ""; // https://github.com/chalk/ansi-regex/blob/master/index.js#L3
const removeANSI = text => text.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
const error = (err, code = 0) => {
const text = err instanceof Error ? err : chalk.red.bold("ERROR: ") + err;
loggerStdout += `${removeANSI(text.toString())}\n`;
console.error(text);
console.log("If you think this is a bug, you can report it: https://github.com/benawad/destiny/issues");
process.exit(code);
};
const info = msg => {
const text = chalk.green.bold("INFO: ") + msg;
loggerStdout += `${removeANSI(text)}\n`;
console.info(text);
};
const log = msg => {
loggerStdout += `${removeANSI(msg)}\n`;
console.log(msg);
};
const warn = msg => {
const text = chalk.yellow.bold("WARN: ") + msg;
loggerStdout += `${removeANSI(text)}\n`;
console.warn(text);
};
let isDebuggerEnabled = false;
let lastDebugTimestamp = null;
const enableDebugger = () => {
isDebuggerEnabled = true;
}; // eslint-disable-next-line @typescript-eslint/no-explicit-any
const debug = (msg, ...data) => {
if ( !isDebuggerEnabled) return;
const currentDebugTimestamp = Date.now();
const text = chalk.magenta.bold("DEBUG: ") + chalk.yellow.bold(`+${lastDebugTimestamp ? currentDebugTimestamp - lastDebugTimestamp : 0}ms `) + msg;
loggerStdout += `${removeANSI(text)}\n`;
console.info(text);
lastDebugTimestamp = currentDebugTimestamp;
if (data.length > 0) {
console.group();
data.forEach(d => {
console.dir(d, {
depth: Infinity,
maxArrayLength: Infinity
});
console.log();
loggerStdout += `${JSON.stringify(d, null, 2)}\n`;
});
console.groupEnd();
}
};
const writeDebugStdout = filePath => {
if ( !isDebuggerEnabled) return;
const resolvedFilePath = path.resolve(filePath);
if (fs.existsSync(resolvedFilePath)) error(`The debug file output already exist "${resolvedFilePath}".\nPlease give a path to a non existing file.`);
fs.writeFileSync(resolvedFilePath, loggerStdout, "utf8");
debug(`stdout written in "${resolvedFilePath}"`);
};
var logger = {
error,
info,
log,
warn,
enableDebugger,
debug,
writeDebugStdout
};
const isDirectory = filePath => fs$1__default.lstatSync(filePath).isDirectory();
const isFile = filePath => fs$1__default.lstatSync(filePath).isFile();
const globSearch = pattern => {
const matches = glob.sync(pattern) // convert forward slashes to backslashes on windows
.map(filePath => path.resolve(filePath));
const files = matches.filter(match => isFile(match));
logger.debug(`glob matches for "${pattern}":`, matches);
if (files.length === 0) {
logger.error("Could not find any files for: " + pattern, 1);
}
return files;
};
const getFilePaths = rootPath => {
const filePaths = [];
const paths = [rootPath];
while (paths.length > 0) {
const filePath = paths.shift();
if (filePath == null || filePath.length === 0) break;
const isGlobPattern = glob.hasMagic(filePath);
if (isGlobPattern) {
filePaths.push(...globSearch(filePath));
continue;
}
if (!fs$1__default.existsSync(filePath)) {
logger.error(`Unable to resolve the path: ${filePath}`);
break;
}
if (isDirectory(filePath)) {
paths.push(path.resolve(filePath, "./**/*.*"));
continue;
}
if (isFile(filePath)) {
filePaths.push(filePath);
}
}
return filePaths;
};
/** Get a restructure map with rootPath keys and filePaths values. */
const getRestructureMap = rootPaths => rootPaths.reduce((acc, rootPath) => ({ ...acc,
[rootPath]: getFilePaths(rootPath)
}), {});
async function isFileGitTracked(git, location) {
return git.silent(true).raw(["ls-files", "--error-unmatch", location]).then(() => true).catch(() => false);
}
/** Moves each file in the tree from old path to new path. */
async function moveFiles(tree, parentFolder) {
const git = Git(parentFolder);
const isFolderGitTracked = await git.checkIsRepo();
const entries = Object.entries(tree);
const fileAlreadyExistsEntries = [];
while (entries.length || fileAlreadyExistsEntries.length) {
/* eslint-disable @typescript-eslint/no-non-null-assertion */
const [oldPath, newPath] = entries.length ? entries.pop() : fileAlreadyExistsEntries.pop();
/* eslint-enable */
// skip globals
if (oldPath.includes("..")) continue;
const oldAbsolutePath = path.resolve(parentFolder, oldPath);
const newAbsolutePath = path.resolve(parentFolder, newPath);
if (oldAbsolutePath === newAbsolutePath) continue; // Create folder for files
const newDirname = path.dirname(newAbsolutePath);
fs$1__default.ensureDirSync(newDirname);
if (fs$1__default.existsSync(newAbsolutePath)) {
if (entries.length) {
// try moving this file later after the other files have been moved
fileAlreadyExistsEntries.push([oldPath, newPath]);
} else {
logger.warn(`not moving "${oldAbsolutePath}" to "${newAbsolutePath}" because "${newAbsolutePath}" already exists`);
}
continue;
}
const shouldGitMv = isFolderGitTracked && (await isFileGitTracked(git, oldAbsolutePath));
logger.debug(`moving "${oldAbsolutePath}" to "${newAbsolutePath}"`);
if (shouldGitMv) {
await git.mv(oldAbsolutePath, newAbsolutePath);
} else {
fs$1__default.renameSync(oldAbsolutePath, newAbsolutePath);
}
}
}
/** Recursively removes all empty folders. */
function removeEmptyFolders(directory) {
const files = fs.readdirSync(directory);
if (!files) return fs.rmdirSync(directory);
for (const filePath of files) {
const fullPath = path.resolve(directory, filePath);
const isDirectory = fs.lstatSync(fullPath).isDirectory();
if (!isDirectory) continue;
removeEmptyFolders(fullPath);
const isEmpty = fs.readdirSync(fullPath).length === 0;
if (isEmpty) {
fs.rmdirSync(fullPath);
logger.debug(`removing "${fullPath}" as empty folder`);
}
}
}
/** Find all imports for file path. */
function findImports(filePath) {
// match es5 & es6 imports
const _reImport = `(?:(?:import|from)\\s+|(?:import|require)\\s*\\()\\\\?['"\`]((?:\\.{1,2})(?:\\/.+)?)\\\\?['"\`]`;
const reImport = new RegExp(_reImport, "gm"); // match one & multi line(s) comments
const reComment = /\/\*[\s\S]*?\*\/|\/\/.*/gm; // match string which contain an import https://github.com/benawad/destiny/issues/111
const reOneLineString = new RegExp(`["'\`].*(${_reImport}).*["'\`]`, "g"); // match multi lines string which contain an import https://github.com/benawad/destiny/issues/111
const reMultiLinesString = new RegExp(`\`[^\`]*(${_reImport})[^\`]*\``, "gm");
const replaceBySpaces = match => " ".repeat(match.length);
const importPaths = [];
const fileContent = fs.readFileSync(filePath, {
encoding: "utf8"
}).replace(reComment, replaceBySpaces).replace(reOneLineString, replaceBySpaces).replace(reMultiLinesString, replaceBySpaces);
let matches;
while ((matches = reImport.exec(fileContent)) !== null) {
importPaths.push({
path: matches[1],
start: reImport.lastIndex - 1 - matches[1].length,
end: reImport.lastIndex - 1
}); // This is necessary to avoid infinite loops with zero-width matches.
if (matches.index === reImport.lastIndex) reImport.lastIndex++;
}
return importPaths;
}
function getExtensionFromImport(relativePath) {
const ext = path.extname(relativePath);
const includeExtension = [".js", ".jsx", ".ts", ".tsx"].includes(ext);
return includeExtension ? ext : undefined;
}
const makeImportPath = (fromPath, toPath) => {
const fromDirectory = path.dirname(fromPath);
const relativePath = path.relative(fromDirectory, toPath);
const relativeDirectory = path.dirname(relativePath);
const ext = getExtensionFromImport(relativePath);
let fileName = path.basename(relativePath, ext); // this will cleanup index imports
// path.join("../add", ".") => "../add"
// instead of: path.join("../add", "index") => "../add/index"
if (fileName === "index") {
fileName = ".";
}
let newImport = path.join(relativeDirectory, fileName); // Ensures relative imports.
const notRelative = !newImport.startsWith(".");
if (notRelative) {
newImport = "./" + newImport;
} // Replace \\ by /.
if (process.platform === "win32") {
newImport = newImport.replace(/\\/g, "/");
}
return newImport;
};
const extensions = [".js", ".json", ".jsx", ".sass", ".scss", ".svg", ".ts", ".d.ts", ".tsx", ".js.flow", ".png", ".jpeg", ".jpg"];
/** Resolve with a list of predefined extensions. */
const customResolve = (id, basedir) => {
try {
return resolve.sync(id, {
basedir,
extensions
});
} catch (err) {
return null;
}
};
const getNewFilePath = (file, rootOptions) => {
for (const {
tree,
parentFolder
} of rootOptions) {
const key = path.relative(parentFolder, file);
if (key in tree) {
return path.resolve(path.join(parentFolder, tree[key]));
}
}
return file;
};
const getNewImportPath = (absImportPath, newFilePath, rootOptions) => {
for (const {
tree,
parentFolder
} of rootOptions) {
const key = path.relative(parentFolder, absImportPath);
if (key in tree) {
return makeImportPath(newFilePath, path.resolve(path.join(parentFolder, tree[key])));
}
}
return makeImportPath(newFilePath, absImportPath);
};
const getNumOfNewChar = (a, b) => {
const numOfNewChar = b - a; // less char than before
if (a > b) return -Math.abs(numOfNewChar);
return numOfNewChar;
};
const fixImports = (filePaths, rootOptions) => {
for (const filePath of filePaths) {
logger.debug(`checking imports of "${filePath}"`);
const importPaths = findImports(filePath);
if (importPaths.length === 0) {
logger.debug(`no import found in "${filePath}"`);
continue;
}
const basedir = path.dirname(filePath);
const newFilePath = getNewFilePath(filePath, rootOptions);
const ogText = fs$1.readFileSync(filePath).toString();
let newText = ogText;
let numOfNewChar = 0;
for (const _import of importPaths) {
const absPath = customResolve(_import.path, basedir);
if (absPath == null) {
logger.error(`Cannot find import ${_import.path} for ${basedir}`);
continue;
}
const newImportPath = getNewImportPath(absPath, newFilePath, rootOptions);
if (newImportPath != null && _import.path !== newImportPath) {
logger.debug(`replacing import of "${_import.path}" by "${newImportPath}" in "${filePath}"`);
newText = `${newText.substr(0, _import.start + numOfNewChar)}${newImportPath}${newText.substring(_import.end + numOfNewChar)}`;
numOfNewChar += getNumOfNewChar(_import.path.length, newImportPath.length);
}
}
if (newText !== ogText) {
logger.debug(`writing new imports of "${filePath}"`);
fs$1.writeFileSync(filePath, newText);
}
}
};
const formatFileStructure = async (filePaths, rootOptions) => {
logger.info("Fixing imports.");
fixImports(filePaths, rootOptions);
for (const {
tree,
parentFolder
} of rootOptions) {
logger.info("Moving files.");
await moveFiles(tree, parentFolder);
removeEmptyFolders(parentFolder);
}
logger.info("Restructure was successful!");
};
const createBranchFromParts = (parts, count) => path.join(...parts.slice(0, count));
/** Remove path that matches `match` but save '/' to calculate position. */
const removePathDuplication = (target, match) => target.replace(new RegExp(`^(/*)${match.replace(/\//g, "")}(/+)`), "$1$2");
/**
* Matches anything between '/' and '.' and prepends the highest possible char
* code to it. This enables us sort files lower than directories.
*/
const prependMaxCharCodeToFile = text => text.replace(/([^/]+)(?=\.)/g, String.fromCharCode(Number.MAX_SAFE_INTEGER) + "$1");
const compareLeafs = (a, b) => prependMaxCharCodeToFile(a).localeCompare(prependMaxCharCodeToFile(b));
/** Check if leaf is the last of its siblings excluding children. */
const isLeafLastSibling = (leaf, remainingLeafs) => {
for (const remaningLeaf of remainingLeafs) {
if (remaningLeaf.position > leaf.position) continue;
if (remaningLeaf.position < leaf.position) return true;
return false;
}
return true;
};
const removeIllicitIndentGuidelines = (line, pastLine) => {
const isCharEndConnectorOrWhitespace = char => char === "└" || char === " ";
return Array.from(line).map((char, idx) => char === "│" && isCharEndConnectorOrWhitespace(pastLine[idx]) ? " " : char).join("");
};
/** Resolve every unique leaf from a list of paths. */
const resolveLeafs = paths => {
const leafs = paths.reduce((acc, target) => {
const parts = target.split("/");
parts.forEach((_, idx) => acc.add(createBranchFromParts(parts, idx + 1)));
return acc;
}, new Set());
return Array.from(leafs).sort(compareLeafs);
};
/**
* Iterates over all leafs and positions them on the correct branches, ie.
* resolving their end name in relation to their branch and their position.
*/
const positionLeafs = leafs => {
const res = [];
let queue = [...leafs];
while (queue.length > 0) {
const leaf = queue.shift();
if (leaf == null) break;
queue = queue.map(queuedLeaf => removePathDuplication(queuedLeaf, leaf));
res.push(leaf);
}
return res.map(x => {
var _parts$pop;
const parts = x.split("/");
return {
text: (_parts$pop = parts.pop()) != null ? _parts$pop : "",
position: parts.length
};
});
};
/** Print a visualization of a tree of paths. */
const printTree = paths => {
const leafs = resolveLeafs(paths);
const positionedLeafs = positionLeafs(leafs);
const treeVisualization = positionedLeafs.reduce((lines, currentLeaf, idx) => {
var _lines;
const pastLine = (_lines = lines[idx - 1]) != null ? _lines : "";
const remainingLeafs = positionedLeafs.slice(idx + 1);
const isDirectory = remainingLeafs.length > 0 && remainingLeafs[0].position > currentLeaf.position;
const indent = currentLeaf.position > 0 ? "│ ".repeat(currentLeaf.position) : "";
const connector = isLeafLastSibling(currentLeaf, remainingLeafs) ? "└──" : "├──";
const text = isDirectory ? chalk.bold.blue(currentLeaf.text) : currentLeaf.text;
const line = indent + connector + text;
lines.push(removeIllicitIndentGuidelines(line, pastLine));
return lines;
}, []).join("\n");
logger.log(treeVisualization);
return treeVisualization;
};
/** Find the common parent directory between all paths. */
const findSharedParent = paths => {
if (paths.length === 1) return path.dirname(paths[0]);
const [shortest, secondShortest] = paths.length > 2 ? paths.sort((a, b) => a.length - b.length) : paths;
const secondShortestParts = secondShortest.split(path.sep);
return shortest.split(path.sep).filter((part, idx) => part === secondShortestParts[idx]).join(path.sep);
};
const isFilePathIgnored = filePath => {
const ignoreList = [/^\.git|node_modules/];
return ignoreList.some(re => re.test(filePath));
};
/** Build graph of all file paths and their own imports. */
function buildGraph(filePaths) {
const parentFolder = findSharedParent(filePaths);
const graph = {};
const totalFiles = [];
for (let filePath of filePaths) {
if (isFilePathIgnored(filePath)) continue;
filePath = path.resolve(filePath);
const start = path.relative(parentFolder, filePath);
totalFiles.push(start);
if (!Array.isArray(graph[start])) {
graph[start] = [];
}
findImports(filePath).forEach(_import => {
const pathWithExtension = customResolve(_import.path, path.dirname(filePath));
if (pathWithExtension == null) {
logger.error(`Cannot find import ${_import.path} for ${filePath}`);
return;
}
const end = path.relative(parentFolder, pathWithExtension);
if (!graph[start].includes(end)) {
graph[start].push(end);
}
});
}
return {
files: totalFiles,
graph,
parentFolder
};
}
const isTestFile = f => /\.test\.|\.spec\.|\.story\.|\.stories\./.test(f);
const invertGraph = graph => {
const invertedGraph = {};
Object.entries(graph).forEach(([filePath, imports]) => {
imports.forEach(importPath => {
if (!(importPath in invertedGraph)) {
invertedGraph[importPath] = [];
}
invertedGraph[importPath].push(filePath);
});
});
return invertedGraph;
};
function findEntryPoints(graph) {
const invertedGraph = invertGraph(graph);
const possibleEntryPoints = Object.keys(graph);
const entryPoints = possibleEntryPoints.filter(file => {
const importedBy = invertedGraph[file];
if (!importedBy || !importedBy.length) {
return true;
}
return importedBy.every(x => isTestFile(x));
});
if (entryPoints.length) {
return entryPoints;
}
const levelMap = {};
possibleEntryPoints.forEach(filePath => {
const n = filePath.split("/").length;
if (!(n in levelMap)) {
levelMap[n] = [];
}
levelMap[n].push(filePath);
});
for (let i = 1; i < 10; i++) {
if (i in levelMap) {
return levelMap[i];
}
}
return [];
}
const hasCycle = (node, graph, visited) => {
const edges = graph[node];
if (visited.has(node)) {
return [...visited, node];
}
visited.add(node);
if (edges == null || edges.length === 0) return null;
for (const edge of edges) {
const cycle = hasCycle(edge, graph, new Set(visited));
if (cycle) return cycle;
}
return null;
};
const linkedRegex = /\.test\.|\.spec\.|\.d\.|@2x\.|@3x\.|\.snap|\.story\.|\.stories\./;
const isLinkedFile = f => linkedRegex.test(f); // add.test.js => add.js
const linkedFileToOriginal = f => {
if (f.endsWith(".snap")) {
return f.replace(".snap", "");
}
return f.replace(linkedRegex, ".");
};
const fileWithoutExtension = f => path.basename(f, path.extname(f));
function toFractalTree(graph, entryPoints) {
const tree = {};
const treeSet = new Set();
const dependencies = {};
const linkedFiles = new Set();
let containsCycle = false;
const addDependency = (key, location) => {
if (!Array.isArray(dependencies[key])) {
dependencies[key] = [];
}
dependencies[key].push(location);
};
const checkDuplicates = (location, dirname, filePath) => {
const hasLocation = treeSet.has(location);
if (hasLocation) {
const newLocation = path.join(dirname, filePath.replace(/\//g, "-"));
logger.info(`File renamed: ${filePath} -> ${newLocation}`);
return newLocation;
}
return location;
};
const changeImportLocation = (filePath, newLocation) => {
tree[filePath] = newLocation;
treeSet.add(newLocation);
};
const fn = (filePath, folderPath, graph) => {
const basename = path.basename(filePath);
if (isLinkedFile(basename)) {
linkedFiles.add(filePath);
return;
}
let directoryName = path.basename(filePath, path.extname(filePath));
const currentFolder = path.basename(path.dirname(filePath));
const isGlobal = filePath.includes("..");
const tempLocation = isGlobal ? filePath : path.join(folderPath, directoryName === "index" && currentFolder && currentFolder !== "." ? currentFolder + path.extname(filePath) : basename);
const location = checkDuplicates(tempLocation, folderPath, filePath);
directoryName = path.basename(location, path.extname(location));
if (!isGlobal) {
changeImportLocation(filePath, location);
}
const imports = graph[filePath];
if ((imports == null ? void 0 : imports.length) > 0) {
const newDestination = path.join(folderPath, directoryName);
for (const importFilePath of imports) {
// if importFilePath includes .. then it's a global
// we don't store globals in tree, so check if cycle
if (importFilePath in tree || importFilePath.includes("..")) {
const cycle = hasCycle(importFilePath, graph, new Set());
if (cycle) {
containsCycle = true;
logger.warn(`Dependency cycle detected: ${cycle.join(" -> ")}`);
} else {
addDependency(importFilePath, location);
}
continue;
}
addDependency(importFilePath, location);
fn(importFilePath, newDestination, graph);
}
}
};
for (const filePath of entryPoints) {
fn(filePath, "", graph);
}
if (!containsCycle) {
Object.entries(dependencies).forEach(([currentPath, dependencies]) => {
if (dependencies.length <= 1 || currentPath.includes("..")) {
return;
}
const parent = findSharedParent(dependencies);
const filename = path.basename(currentPath);
const currentDir = path.dirname(currentPath);
const newFilePath = path.join(parent, "shared", path.basename(filename, path.extname(filename)) === "index" && currentDir && currentDir !== "." ? path.join(currentDir + path.extname(filename)) : filename);
changeImportLocation(currentPath, newFilePath);
});
}
const treeKeys = Object.keys(tree);
if (linkedFiles.size > 0) {
// const globalTests = [];
for (const linkedFile of linkedFiles) {
const sourceFile = linkedFileToOriginal(linkedFile);
const oneDirUp = path.join(path.dirname(sourceFile), "..", path.basename(sourceFile)); // source file will either be in the current dir or one up
let sourceFilePath = tree[sourceFile] || tree[oneDirUp]; // sometimes the test is add.test.jsx and the source is add.test.js
// so we have to do a linear search to find source key
// we could probably optimize this if needed by doing some work in the fn above
if (!sourceFilePath) {
const sourceFileWithoutFileExtension = fileWithoutExtension(sourceFile);
for (const key of treeKeys) {
if (path.basename(key).startsWith(sourceFileWithoutFileExtension)) {
sourceFilePath = tree[key];
break;
}
}
}
if (!sourceFilePath && isTestFile(linkedFile)) {
// could not link by filename
// so the backup is linking by first relative import
const [firstRelativeImport] = graph[linkedFile];
if (firstRelativeImport) {
sourceFilePath = tree[firstRelativeImport];
}
}
if (!sourceFilePath) {
logger.warn(`could not find source file that is linked to ${chalk.blueBright(linkedFile)} | 2 locations were checked: ${chalk.blueBright(sourceFile)} and ${chalk.blueBright(oneDirUp)}`);
continue;
}
const isSnapshot = linkedFile.endsWith(".snap");
const location = checkDuplicates(path.join(path.dirname(sourceFilePath), isSnapshot ? "__snapshot__" : "", path.basename(linkedFile)), path.dirname(sourceFilePath), linkedFile);
changeImportLocation(linkedFile, location);
}
}
return tree;
}
const extractParentDirectory = destination => {
const parts = destination.split(path.sep);
if (parts.length === 1) {
return;
}
parts.pop();
return parts.join(path.sep);
};
const moveUp = destinationPath => {
const parts = destinationPath.split(path.sep);
return [...parts.slice(0, parts.length - 2), parts[parts.length - 1]].join(path.sep);
};
const detectLonelyFiles = tree => {
const fractalTree = { ...tree
}; // Reverse lookup destination -> current location
const reversedFractalTree = {};
for (const [currentFilePath, destinationFilePath] of Object.entries(fractalTree)) {
reversedFractalTree[destinationFilePath] = currentFilePath;
}
const dirCounter = {}; // Sort is important here since we want to go from deep in the file structure to top
const currentDestinations = Object.values(fractalTree).sort((a, b) => b.length - a.length); // Count all occurencies of the parent dirs of the current destinations
for (const currentDestination of currentDestinations) {
const parentDir = extractParentDirectory(currentDestination);
if (!parentDir) {
continue;
}
if (parentDir in dirCounter) {
dirCounter[parentDir] = dirCounter[parentDir] + 1;
continue;
}
dirCounter[parentDir] = 1;
}
/**
* Loop over all the destinations again and move them up if they are lonely files
*/
for (const currentDestination of currentDestinations) {
const startParentDir = extractParentDirectory(currentDestination);
if (!startParentDir) {
continue;
}
let counter = dirCounter[startParentDir];
let newDestination = currentDestination;
let parentDir = startParentDir;
while (counter === 1) {
parentDir = extractParentDirectory(newDestination);
if (!parentDir) {
break;
}
if (startParentDir === parentDir) {
counter = 1;
} else if (parentDir in dirCounter) {
counter = dirCounter[parentDir] + 1;
}
dirCounter[parentDir] = counter;
if (counter === 1) {
newDestination = moveUp(newDestination);
}
}
fractalTree[reversedFractalTree[currentDestination]] = newDestination;
}
return fractalTree;
};
const getRootFolder = parentDir => parentDir.split(path.sep).pop();
function generateTrees(restructureMap, {
avoidSingleFile
}) {
return Object.entries(restructureMap).reduce((rootOptions, [rootPath, filePaths]) => {
if (filePaths.length <= 1) return rootOptions;
logger.info(`Generating tree for: ${rootPath}`);
const {
graph,
parentFolder
} = buildGraph(filePaths);
const entryPoints = findEntryPoints(graph);
let tree = toFractalTree(graph, entryPoints);
if (avoidSingleFile) {
tree = detectLonelyFiles(tree);
} // entryPoints that don't import anything might be unused??
// @todo let user ignore some of these
const unusedFiles = entryPoints.filter(filePath => !isTestFile(filePath) && !filePath.endsWith(".snap") && !filePath.endsWith(".d.ts") && !graph[filePath].length);
logger.log(chalk.bold.blue(getRootFolder(parentFolder)));
printTree(Object.values(tree));
if (unusedFiles.length > 0) {
logger.warn(`Found ${unusedFiles.length} unused files:` + "\n" + unusedFiles.join("\n"));
}
rootOptions.push({
parentFolder,
tree
});
return rootOptions;
}, []);
}
var version = "0.7.1";
/** Format the given options and print the help message */
const printHelpMessage = options => {
const indent = " ";
console.log(chalk`{blue destiny} - Prettier for file structures.
{bold USAGE}
${indent}{blue destiny} [option...] [{underline path}]
${indent}The {underline path} argument can consist of either a {bold file path} or a {bold glob}.
{bold OPTIONS}
`); // options
const optionsWithJoinedFlags = options.map(opt => ({ ...opt,
flags: opt.flags.join(", ")
}));
const [longestFlag] = [...optionsWithJoinedFlags].sort((a, b) => b.flags.length - a.flags.length);
const descriptionsPosX = `${longestFlag.flags}${indent.repeat(2)}`.length;
const parsedOptionsMessage = optionsWithJoinedFlags.map(({
flags,
description
}) => {
const numOfSpacesToAdd = descriptionsPosX - flags.length;
return `${indent}${flags}${" ".repeat(numOfSpacesToAdd)}${description}`;
}).join("\n");
console.log(parsedOptionsMessage, "\n");
};
const {
argv
} = process;
const defaultConfig = {
help: false,
include: [],
version: false,
write: false,
avoidSingleFile: false,
debug: false
};
const resolveHomeDir = path => path.replace(/~\/|~(?!.)/, `${os.homedir()}/`);
const printVersion = () => console.log("v" + version);
const printHelp = exitCode => {
printHelpMessage([{
flags: ["-V", "--version"],
description: "Output version number"
}, {
flags: ["-h", "--help"],
description: "Output usage information"
}, {
flags: ["-w", "--write"],
description: "Restructure and edit folders and files"
}, {
flags: ["-S", "--avoid-single-file"],
description: "Flag to indicate if single files in folders should be avoided"
}, {
flags: ["--debug [?output file]"],
description: "Print debugging info"
}]);
return process.exit(exitCode);
};
const parseArgs = args => {
const cliConfig = {};
while (args.length > 0) {
const arg = args.shift();
if (arg == null) break;
switch (arg) {
case "-h":
case "--help":
cliConfig.help = true;
break;
case "-V":
case "--version":
cliConfig.version = true;
break;
case "-w":
case "--write":
cliConfig.write = true;
break;
case "-S":
case "--avoid-single-file":
cliConfig.avoidSingleFile = true;
break;
case "--debug":
cliConfig.debug = !args[0] || args[0].startsWith("-") ? true : args.shift();
break;
default:
{
if (fs.existsSync(resolveHomeDir(arg)) || glob.hasMagic(arg)) {
var _cliConfig$include;
cliConfig.include = [...((_cliConfig$include = cliConfig.include) != null ? _cliConfig$include : []), arg];
}
}
}
}
return cliConfig;
};
const getMergedConfig = cliConfig => {
var _ref, _cosmiconfigSync$sear;
const externalConfig = (_ref = (_cosmiconfigSync$sear = cosmiconfig.cosmiconfigSync("destiny").search()) == null ? void 0 : _cosmiconfigSync$sear.config) != null ? _ref : {};
return { ...defaultConfig,
...externalConfig,
...cliConfig
};
};
const run = async args => {
const cliConfig = parseArgs(args);
const mergedConfig = getMergedConfig(cliConfig);
if (mergedConfig.help) return printHelp(0);
if (mergedConfig.version) return printVersion();
if (mergedConfig.include.length === 0) return printHelp(1);
if (mergedConfig.debug) logger.enableDebugger();
process.on("exit", () => {
if (typeof mergedConfig.debug === "string") {
logger.writeDebugStdout(mergedConfig.debug);
}
logger.debug("exiting");
});
logger.debug(`version: ${version}`);
logger.debug("config used:", mergedConfig);
mergedConfig.include = mergedConfig.include.map(resolveHomeDir);
const restructureMap = getRestructureMap(mergedConfig.include);
const filesToEdit = Object.values(restructureMap).flat();
logger.debug("restructured map:", restructureMap);
if (filesToEdit.length === 0) {
logger.error("Could not find any files to restructure", 1);
return;
}
const rootOptions = generateTrees(restructureMap, mergedConfig);
logger.debug("generated tree(s):", rootOptions);
if (mergedConfig.write) {
await formatFileStructure(filesToEdit, rootOptions);
}
};
{
run(argv.slice(2, argv.length));
}
exports.run = run;