UNPKG

destiny

Version:

Prettier for file structures

1,049 lines (828 loc) 31.7 kB
#!/usr/bin/env node 'use strict'; 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;