diff --git a/__tests__/commands/ls.js b/__tests__/commands/ls.js new file mode 100644 index 0000000000..fce060bc57 --- /dev/null +++ b/__tests__/commands/ls.js @@ -0,0 +1,73 @@ +/* @flow */ + +import * as ls from '../../src/cli/commands/ls.js'; + +test('getParent should extract a parent object from a hash, if the parent key exists', () => { + const mockTreesByKey = {}; + + mockTreesByKey['parentPkg'] = { + name: 'parent@1.1.1', + children: [], + }; + const res = ls.getParent('parentPkg#childPkg', mockTreesByKey); + + expect(res instanceof Object).toBe(true); + expect(res.name).toBe('parent@1.1.1'); + expect(res.children.length).toBe(0); +}); + +test('getParent should return undefined if the key does not exist in hash', () => { + const mockTreesByKey = {}; + mockTreesByKey['parentPkg'] = { }; + + const res = ls.getParent('parentPkg#childPkg', mockTreesByKey); + expect(res.name).not.toBeDefined(); + expect(res.children).not.toBeDefined(); +}); + +test('setFlags should set options for --depth', () => { + const flags = ['--depth']; + const commander = require('commander'); + ls.setFlags(commander); + + const commanderOptions = commander.options; + const optsLen = commanderOptions.length; + flags.map((flag) => { + let currFlagExists = false; + for (let i = 0; i < optsLen; i++) { + if (commanderOptions[i].long === flag) { + currFlagExists = true; + } + } + expect(currFlagExists).toBeTruthy(); + }); +}); + +test('setFlags should set options for --depth', () => { + const flags = ['--foo', '--bar', '--baz']; + const commander = require('commander'); + ls.setFlags(commander); + + const commanderOptions = commander.options; + const optsLen = commanderOptions.length; + flags.map((flag) => { + let currFlagExists = false; + for (let i = 0; i < optsLen; i++) { + if (commanderOptions[i].long === flag) { + currFlagExists = true; + } + } + expect(currFlagExists).not.toBeTruthy(); + }); +}); + +test('getReqDepth should return a number if valid', () => { + expect(ls.getReqDepth('1')).toEqual(1); + expect(ls.getReqDepth('01')).toEqual(1); +}); + +test('getReqDepth should return -1 if invalid', () => { + expect(ls.getReqDepth('foo')).toEqual(-1); + expect(ls.getReqDepth('bar')).toEqual(-1); + expect(ls.getReqDepth('')).toEqual(-1); +}); diff --git a/src/cli/commands/add.js b/src/cli/commands/add.js index bd0f71a597..854d20ac0c 100644 --- a/src/cli/commands/add.js +++ b/src/cli/commands/add.js @@ -4,6 +4,7 @@ import type {Reporter} from '../../reporters/index.js'; import type {InstallCwdRequest, InstallPrepared} from './install.js'; import type {DependencyRequestPatterns} from '../../types.js'; import type Config from '../../config.js'; +import type {LsOptions} from './ls.js'; import Lockfile from '../../lockfile/wrapper.js'; import * as PackageReference from '../../package-reference.js'; import PackageRequest from '../../package-request.js'; @@ -82,7 +83,11 @@ export class Add extends Install { */ async maybeOutputSaveTree(patterns: Array): Promise { - const {trees, count} = await buildTree(this.resolver, this.linker, patterns, true, true); + // don't limit the shown tree depth + const opts: LsOptions = { + reqDepth: 0, + }; + const {trees, count} = await buildTree(this.resolver, this.linker, patterns, opts, true, true); this.reporter.success( count === 1 ? this.reporter.lang('savedNewDependency') diff --git a/src/cli/commands/ls.js b/src/cli/commands/ls.js index 0406cf03b2..d66f179149 100644 --- a/src/cli/commands/ls.js +++ b/src/cli/commands/ls.js @@ -13,6 +13,10 @@ const invariant = require('invariant'); export const requireLockfile = true; +export type LsOptions = { + reqDepth?: ?number, +}; + function buildCount(trees: ?Trees): number { if (!trees || !trees.length) { return 0; @@ -36,6 +40,7 @@ export async function buildTree( resolver: PackageResolver, linker: PackageLinker, patterns: Array, + opts: LsOptions, onlyFresh?: boolean, ignoreHoisted?: boolean, ): Promise<{ @@ -54,6 +59,11 @@ export async function buildTree( // build initial trees for (const [, info] of hoisted) { const ref = info.pkg._reference; + const hint = null; + const parent = getParent(info.key, treesByKey); + const children = []; + let depth = 0; + let color = 'bold'; invariant(ref, 'expected reference'); if (onlyFresh) { @@ -69,29 +79,39 @@ export async function buildTree( } } - const hint = null; - let color = 'bold'; - - if (info.originalKey !== info.key) { + if (info.originalKey !== info.key || opts.reqDepth === 0) { // was hoisted color = null; } + // check parent to obtain next depth + if (parent && parent.depth > 0) { + depth = parent.depth + 1; + } else { + depth = 0; + } - const children = []; - treesByKey[info.key] = { - name: `${info.pkg.name}@${info.pkg.version}`, - children, - hint, - color, - }; + const topLevel = opts.reqDepth === 0 && !parent; + const showAll = opts.reqDepth === -1; + const nextDepthIsValid = (depth + 1 <= Number(opts.reqDepth)); + + if (topLevel || nextDepthIsValid || showAll) { + treesByKey[info.key] = { + name: `${info.pkg.name}@${info.pkg.version}`, + children, + hint, + color, + depth, + }; + } // add in dummy children for hoisted dependencies + const nextChildDepthIsValid = (depth + 1 < Number(opts.reqDepth)); invariant(ref, 'expected reference'); - if (!ignoreHoisted) { + if ((!ignoreHoisted && nextDepthIsValid) || showAll) { for (const pattern of resolver.dedupePatterns(ref.dependencies)) { const pkg = resolver.getStrictResolvedPattern(pattern); - if (!hoistedByKey[`${info.key}#${pkg.name}`]) { + if (!hoistedByKey[`${info.key}#${pkg.name}`] && (nextChildDepthIsValid || showAll)) { children.push({ name: pattern, color: 'dim', @@ -105,18 +125,16 @@ export async function buildTree( // add children for (const [, info] of hoisted) { const tree = treesByKey[info.key]; + const parent = getParent(info.key, treesByKey); if (!tree) { continue; } - const keyParts = info.key.split('#'); - if (keyParts.length === 1) { + if (info.key.split('#').length === 1) { trees.push(tree); continue; } - const parentKey = keyParts.slice(0, -1).join('#'); - const parent = treesByKey[parentKey]; if (parent) { parent.children.push(tree); } @@ -125,17 +143,35 @@ export async function buildTree( return {trees, count: buildCount(trees)}; } +export function getParent(key: string, treesByKey: Object) : Object { + const parentKey = key.split('#').slice(0, -1).join('#'); + return treesByKey[parentKey]; +} + +export function setFlags(commander: Object) { + commander.option('--depth [depth]', 'Limit the depth of the shown dependencies'); +} + +export function getReqDepth(inputDepth: string) : number { + return inputDepth && /^\d+$/.test(inputDepth) ? Number(inputDepth) : -1; +} + export async function run( config: Config, reporter: Reporter, flags: Object, args: Array, ): Promise { + const lockfile = await Lockfile.fromDirectory(config.cwd, reporter); const install = new Install(flags, config, reporter, lockfile); const [depRequests, patterns] = await install.fetchRequestFromCwd(); await install.resolver.init(depRequests, install.flags.flat); + const opts: LsOptions = { + reqDepth: getReqDepth(flags.depth), + }; + let filteredPatterns: Array = []; if (args.length) { @@ -161,6 +197,6 @@ export async function run( filteredPatterns = patterns; } - const {trees} = await buildTree(install.resolver, install.linker, filteredPatterns); + const {trees} = await buildTree(install.resolver, install.linker, filteredPatterns, opts); reporter.tree('ls', trees); } diff --git a/src/reporters/base-reporter.js b/src/reporters/base-reporter.js index 65e10d52c6..4018c193ae 100644 --- a/src/reporters/base-reporter.js +++ b/src/reporters/base-reporter.js @@ -120,7 +120,7 @@ export default class BaseReporter { // TODO list(key: string, items: Array) {} - // TODO + // Outputs basic tree structure to console tree(key: string, obj: Trees) {} // called whenever we begin a step in the CLI. diff --git a/src/reporters/console/console-reporter.js b/src/reporters/console/console-reporter.js index 73f4e01e10..214475f144 100644 --- a/src/reporters/console/console-reporter.js +++ b/src/reporters/console/console-reporter.js @@ -14,6 +14,7 @@ import Progress from './progress-bar.js'; import Spinner from './spinner-progress.js'; import {clearLine} from './util.js'; import {removeSuffix} from '../../util/misc.js'; +import {sortTrees, recurseTree, getFormattedOutput} from './helpers/tree-helper.js'; const {inspect} = require('util'); const readline = require('readline'); @@ -21,12 +22,6 @@ const repeat = require('repeating'); const chalk = require('chalk'); const read = require('read'); -function sortTrees(trees: Trees = []): Trees { - return trees.sort(function(tree1, tree2): number { - return tree1.name.localeCompare(tree2.name); - }); -} - type Row = Array; export default class ConsoleReporter extends BaseReporter { @@ -186,42 +181,19 @@ export default class ConsoleReporter extends BaseReporter { }); }); } - + // handles basic tree output to console tree(key: string, trees: Trees) { - trees = sortTrees(trees); - - const stdout = this.stdout; - + // const output = ({name, children, hint, color}, level, end) => { - children = sortTrees(children); - - let indent = end ? '└' : '├'; - - if (level) { - indent = repeat('│ ', level) + indent; - } - - let suffix = ''; - if (hint) { - suffix += ` (${this.format.dim(hint)})`; - } - if (color) { - name = this.format[color](name); - } - stdout.write(`${indent}─ ${name}${suffix}\n`); + const formatter = this.format; + const out = getFormattedOutput({end, level, hint, color, name, formatter}); + this.stdout.write(out); if (children && children.length) { - for (let i = 0; i < children.length; i++) { - const tree = children[i]; - output(tree, level + 1, i === children.length - 1); - } + recurseTree(sortTrees(children), level, output); } }; - - for (let i = 0; i < trees.length; i++) { - const tree = trees[i]; - output(tree, 0, i === trees.length - 1); - } + recurseTree(sortTrees(trees), -1, output); } activitySet(total: number, workers: number): ReporterSpinnerSet { diff --git a/src/reporters/console/helpers/tree-helper.js b/src/reporters/console/helpers/tree-helper.js new file mode 100644 index 0000000000..06f612c03b --- /dev/null +++ b/src/reporters/console/helpers/tree-helper.js @@ -0,0 +1,57 @@ +/* @flow */ + +const repeat = require('repeating'); + +// types +import type {Trees} from '../../types.js'; + +export type FormattedOutput = { + end: boolean, + level: number, + hint: any, + color: string, + name: string, + formatter: any, +}; + +// public +export const sortTrees = (trees: Trees): Trees => { + return trees.sort(function(tree1, tree2): number { + return tree1.name.localeCompare(tree2.name); + }); +}; + +export const recurseTree = (tree: Trees, level: number, recurseFunc: Function) => { + const treeLen = tree.length; + const treeEnd = treeLen - 1; + for (let i = 0; i < treeLen; i++) { + recurseFunc(tree[i], level + 1, i === treeEnd); + } +}; + +export const getFormattedOutput = (fmt: FormattedOutput): string => { + const item = formatColor(fmt.color, fmt.name, fmt.formatter); + const indent = getIndent(fmt.end, fmt.level); + const suffix = getSuffix(fmt.hint, fmt.formatter); + return `${indent}─ ${item}${suffix}\n`; +}; + +// private +const getIndentChar = (end: boolean) : string => { + return end ? '└' : '├'; +}; + +const getIndent = (end: boolean, level: number) : string => { + const base = repeat('│ ', level); + const indentChar = getIndentChar(end); + const hasLevel = base + indentChar; + return level ? hasLevel : indentChar; +}; + +const getSuffix = (hint: any, formatter: any) : string => { + return hint ? ` (${formatter.grey(hint)})` : ''; +}; + +const formatColor = (color: string, strToFormat: string, formatter: any) : string => { + return color ? formatter[color](strToFormat) : strToFormat; +};