From bf56018f3bcf4db5b8ecc899c3afa32ec8bb2b17 Mon Sep 17 00:00:00 2001 From: Daniel Stockman Date: Mon, 30 Jul 2018 13:20:53 -0700 Subject: [PATCH] feat(list): Extract @lerna/listable utility --- commands/list/command.js | 27 +-- commands/list/index.js | 118 ++----------- commands/list/package.json | 5 +- integration/lerna-ls.test.js | 21 +-- package-lock.json | 8 +- utils/listable/README.md | 25 +++ .../__tests__/listable-format.test.js | 164 ++++++++++++++++++ .../__tests__/listable-options.test.js | 36 ++++ utils/listable/index.js | 4 + utils/listable/lib/listable-format.js | 129 ++++++++++++++ utils/listable/lib/listable-options.js | 31 ++++ utils/listable/package.json | 34 ++++ 12 files changed, 453 insertions(+), 149 deletions(-) create mode 100644 utils/listable/README.md create mode 100644 utils/listable/__tests__/listable-format.test.js create mode 100644 utils/listable/__tests__/listable-options.test.js create mode 100644 utils/listable/index.js create mode 100644 utils/listable/lib/listable-format.js create mode 100644 utils/listable/lib/listable-options.js create mode 100644 utils/listable/package.json diff --git a/commands/list/command.js b/commands/list/command.js index 0ccfc757d2..10396bace4 100644 --- a/commands/list/command.js +++ b/commands/list/command.js @@ -1,6 +1,7 @@ "use strict"; const filterable = require("@lerna/filter-options"); +const listable = require("@lerna/listable"); /** * @see https://github.com/yargs/yargs/blob/master/docs/advanced.md#providing-a-command-module @@ -12,31 +13,7 @@ exports.aliases = ["ls", "la", "ll"]; exports.describe = "List local packages"; exports.builder = yargs => { - yargs.options({ - json: { - group: "Command Options:", - describe: "Show information as a JSON array", - type: "boolean", - }, - a: { - group: "Command Options:", - describe: "Show private packages that are normally hidden", - type: "boolean", - alias: "all", - }, - l: { - group: "Command Options:", - describe: "Show extended information", - type: "boolean", - alias: "long", - }, - p: { - group: "Command Options:", - describe: "Show parseable output instead of columnified view", - type: "boolean", - alias: "parseable", - }, - }); + listable.options(yargs); return filterable(yargs); }; diff --git a/commands/list/index.js b/commands/list/index.js index 766508fbc9..84580a9ce8 100644 --- a/commands/list/index.js +++ b/commands/list/index.js @@ -1,9 +1,7 @@ "use strict"; -const chalk = require("chalk"); -const columnify = require("columnify"); -const path = require("path"); const Command = require("@lerna/command"); +const listable = require("@lerna/listable"); const output = require("@lerna/output"); module.exports = factory; @@ -18,114 +16,18 @@ class ListCommand extends Command { } initialize() { - const alias = this.options._[0]; - - this.showAll = alias === "la" || this.options.all; - this.showLong = alias === "la" || alias === "ll" || this.options.long; - - this.resultList = this.showAll - ? this.filteredPackages - : this.filteredPackages.filter(pkg => !pkg.private); - - // logged after output - this.count = this.resultList.length; + this.result = listable.format(this.filteredPackages, this.options); } execute() { - let result; - - if (this.options.json) { - result = this.formatJSON(); - } else if (this.options.parseable) { - result = this.formatParseable(); - } else { - result = this.formatColumns(); - } - - output(result); - - this.logger.success("found", "%d %s", this.count, this.count === 1 ? "package" : "packages"); - } - - formatJSON() { - // explicit re-mapping exposes non-enumerable properties - const data = this.resultList.map(pkg => ({ - name: pkg.name, - version: pkg.version, - private: pkg.private, - location: pkg.location, - })); - - return JSON.stringify(data, null, 2); - } - - formatParseable() { - const mapper = this.showLong - ? pkg => { - const result = [pkg.location, pkg.name]; - - // sometimes the version is inexplicably missing? - if (pkg.version) { - result.push(pkg.version); - } else { - result.push("MISSING"); - } - - if (pkg.private) { - result.push("PRIVATE"); - } - - return result.join(":"); - } - : pkg => pkg.location; - - return this.resultList.map(mapper).join("\n"); - } - - formatColumns() { - const formattedResults = this.resultList.map(result => { - const formatted = { - name: result.name, - }; - - if (result.version) { - formatted.version = chalk.green(`v${result.version}`); - } else { - formatted.version = chalk.yellow("MISSING"); - } - - if (result.private) { - formatted.private = `(${chalk.red("PRIVATE")})`; - } - - formatted.location = chalk.grey(path.relative(".", result.location)); - - return formatted; - }); - - return columnify(formattedResults, { - showHeaders: false, - columns: this.getColumnOrder(), - config: { - version: { - align: "right", - }, - }, - }); - } - - getColumnOrder() { - const columns = ["name"]; - - if (this.showLong) { - columns.push("version", "location"); - } - - if (this.showAll) { - columns.push("private"); - } - - return columns; + output(this.result.text); + + this.logger.success( + "found", + "%d %s", + this.result.count, + this.result.count === 1 ? "package" : "packages" + ); } } diff --git a/commands/list/package.json b/commands/list/package.json index f2bae63a06..ba88b6e5d8 100644 --- a/commands/list/package.json +++ b/commands/list/package.json @@ -33,8 +33,7 @@ "dependencies": { "@lerna/command": "file:../../core/command", "@lerna/filter-options": "file:../../core/filter-options", - "@lerna/output": "file:../../utils/output", - "chalk": "^2.3.1", - "columnify": "^1.5.4" + "@lerna/listable": "file:../../utils/listable", + "@lerna/output": "file:../../utils/output" } } diff --git a/integration/lerna-ls.test.js b/integration/lerna-ls.test.js index d0382c7834..60b78a20c5 100644 --- a/integration/lerna-ls.test.js +++ b/integration/lerna-ls.test.js @@ -2,7 +2,6 @@ const cliRunner = require("@lerna-test/cli-runner"); const initFixture = require("@lerna-test/init-fixture")(__dirname); -const multiLineTrimRight = require("@lerna-test/multi-line-trim-right"); // normalize temp directory paths in snapshots expect.addSnapshotSerializer(require("@lerna-test/serialize-tempdir")); @@ -12,14 +11,12 @@ let lerna; beforeAll(async () => { const cwd = await initFixture("lerna-ls"); - const run = cliRunner(cwd); - // wrap runner to remove trailing whitespace added by columnify - lerna = (...args) => run(...args).then(result => multiLineTrimRight(result.stdout)); + lerna = cliRunner(cwd); }); test("lerna list", async () => { - const stdout = await lerna("list"); + const { stdout } = await lerna("list"); expect(stdout).toMatchInlineSnapshot(` package-1 @test/package-2 @@ -28,7 +25,7 @@ package-3 }); test("lerna ls", async () => { - const stdout = await lerna("ls"); + const { stdout } = await lerna("ls"); expect(stdout).toMatchInlineSnapshot(` package-1 @test/package-2 @@ -37,7 +34,7 @@ package-3 }); test("lerna ls --all", async () => { - const stdout = await lerna("ls", "--all"); + const { stdout } = await lerna("ls", "--all"); expect(stdout).toMatchInlineSnapshot(` package-1 @test/package-2 @@ -47,7 +44,7 @@ package-4 (PRIVATE) }); test("lerna ls --long", async () => { - const stdout = await lerna("ls", "--long"); + const { stdout } = await lerna("ls", "--long"); expect(stdout).toMatchInlineSnapshot(` package-1 v1.0.0 packages/pkg-1 @test/package-2 v2.0.0 packages/pkg-2 @@ -56,7 +53,7 @@ package-3 MISSING packages/pkg-3 }); test("lerna ls --parseable", async () => { - const stdout = await lerna("ls", "--parseable"); + const { stdout } = await lerna("ls", "--parseable"); expect(stdout).toMatchInlineSnapshot(` /packages/pkg-1 /packages/pkg-2 @@ -65,7 +62,7 @@ test("lerna ls --parseable", async () => { }); test("lerna ls --all --long --parseable", async () => { - const stdout = await lerna("ls", "-alp"); + const { stdout } = await lerna("ls", "-alp"); expect(stdout).toMatchInlineSnapshot(` /packages/pkg-1:package-1:1.0.0 /packages/pkg-2:@test/package-2:2.0.0 @@ -75,7 +72,7 @@ test("lerna ls --all --long --parseable", async () => { }); test("lerna la", async () => { - const stdout = await lerna("la"); + const { stdout } = await lerna("la"); expect(stdout).toMatchInlineSnapshot(` package-1 v1.0.0 packages/pkg-1 @test/package-2 v2.0.0 packages/pkg-2 @@ -85,7 +82,7 @@ package-4 v4.0.0 packages/pkg-4 (PRIVATE) }); test("lerna ll", async () => { - const stdout = await lerna("ll"); + const { stdout } = await lerna("ll"); expect(stdout).toMatchInlineSnapshot(` package-1 v1.0.0 packages/pkg-1 @test/package-2 v2.0.0 packages/pkg-2 diff --git a/package-lock.json b/package-lock.json index fcd399cbea..f6cef1f110 100644 --- a/package-lock.json +++ b/package-lock.json @@ -473,7 +473,13 @@ "requires": { "@lerna/command": "file:core/command", "@lerna/filter-options": "file:core/filter-options", - "@lerna/output": "file:utils/output", + "@lerna/listable": "file:utils/listable", + "@lerna/output": "file:utils/output" + } + }, + "@lerna/listable": { + "version": "file:utils/listable", + "requires": { "chalk": "^2.3.1", "columnify": "^1.5.4" } diff --git a/utils/listable/README.md b/utils/listable/README.md new file mode 100644 index 0000000000..62763a093f --- /dev/null +++ b/utils/listable/README.md @@ -0,0 +1,25 @@ +# `@lerna/listable` + +> Shared logic for listing package information + +This is an internal package for [Lerna](https://github.com/lerna/lerna/#readme), YMMV. + +## Usage + +### `listable.format()` + +```js +const listable = require('@lerna/listable'); + +const { text, count } = listable.format(packages, options); +``` + +### `listable.options()` + +```js +const listable = require('@lerna/listable'); + +exports.builder = yargs => { + listable.options(yargs); +}; +``` diff --git a/utils/listable/__tests__/listable-format.test.js b/utils/listable/__tests__/listable-format.test.js new file mode 100644 index 0000000000..59f0fc3db8 --- /dev/null +++ b/utils/listable/__tests__/listable-format.test.js @@ -0,0 +1,164 @@ +"use strict"; + +const path = require("path"); +const tempy = require("tempy"); +const Package = require("@lerna/package"); +const listable = require(".."); + +// normalize temp directory paths in snapshots +expect.addSnapshotSerializer(require("@lerna-test/serialize-tempdir")); + +describe("listable.format()", () => { + let packages; + + const formatWithOptions = opts => listable.format(packages, Object.assign({ _: ["ls"] }, opts)); + + beforeAll(() => { + const cwd = tempy.directory(); + process.chdir(cwd); + + packages = [ + new Package({ name: "pkg-1", version: "1.0.0" }, path.join(cwd, "/pkgs/pkg-1")), + new Package({ name: "pkg-2" }, path.join(cwd, "/pkgs/pkg-2")), + new Package({ name: "pkg-3", version: "3.0.0", private: true }, path.join(cwd, "/pkgs/pkg-3")), + ]; + }); + + describe("renders", () => { + test("all output", () => { + const { count, text } = formatWithOptions({ all: true }); + + expect(count).toBe(3); + expect(text).toMatchInlineSnapshot(` +"pkg-1 +pkg-2 +pkg-3 (PRIVATE)" +`); + }); + + test("long output", () => { + const { count, text } = formatWithOptions({ long: true }); + + expect(count).toBe(2); + expect(text).toMatchInlineSnapshot(` +"pkg-1 v1.0.0 pkgs/pkg-1 +pkg-2 MISSING pkgs/pkg-2" +`); + }); + + test("all long output", () => { + const { text } = formatWithOptions({ long: true, all: true }); + + expect(text).toMatchInlineSnapshot(` +"pkg-1 v1.0.0 pkgs/pkg-1 +pkg-2 MISSING pkgs/pkg-2 +pkg-3 v3.0.0 pkgs/pkg-3 (PRIVATE)" +`); + }); + + test("JSON output", () => { + const { text } = formatWithOptions({ json: true }); + + expect(text).toMatchInlineSnapshot(` +[ + { + "name": "pkg-1", + "version": "1.0.0", + "private": false, + "location": "/pkgs/pkg-1" + }, + { + "name": "pkg-2", + "private": false, + "location": "/pkgs/pkg-2" + } +] +`); + }); + + test("all JSON output", () => { + const { text } = formatWithOptions({ json: true, all: true }); + + expect(text).toMatchInlineSnapshot(` +[ + { + "name": "pkg-1", + "version": "1.0.0", + "private": false, + "location": "/pkgs/pkg-1" + }, + { + "name": "pkg-2", + "private": false, + "location": "/pkgs/pkg-2" + }, + { + "name": "pkg-3", + "version": "3.0.0", + "private": true, + "location": "/pkgs/pkg-3" + } +] +`); + }); + + test("parseable output", () => { + const { text } = formatWithOptions({ parseable: true }); + + expect(text).toMatchInlineSnapshot(` +/pkgs/pkg-1 +/pkgs/pkg-2 +`); + }); + + test("all parseable output", () => { + const { text } = formatWithOptions({ parseable: true, all: true }); + + expect(text).toMatchInlineSnapshot(` +/pkgs/pkg-1 +/pkgs/pkg-2 +/pkgs/pkg-3 +`); + }); + + test("long parseable output", () => { + const { text } = formatWithOptions({ parseable: true, long: true }); + + expect(text).toMatchInlineSnapshot(` +/pkgs/pkg-1:pkg-1:1.0.0 +/pkgs/pkg-2:pkg-2:MISSING +`); + }); + + test("all long parseable output", () => { + const { text } = formatWithOptions({ parseable: true, all: true, long: true }); + + expect(text).toMatchInlineSnapshot(` +/pkgs/pkg-1:pkg-1:1.0.0 +/pkgs/pkg-2:pkg-2:MISSING +/pkgs/pkg-3:pkg-3:3.0.0:PRIVATE +`); + }); + }); + + describe("aliases", () => { + test("la => ls -la", () => { + const { text } = formatWithOptions({ _: ["la"] }); + + expect(text).toMatchInlineSnapshot(` +"pkg-1 v1.0.0 pkgs/pkg-1 +pkg-2 MISSING pkgs/pkg-2 +pkg-3 v3.0.0 pkgs/pkg-3 (PRIVATE)" +`); + }); + + test("ll => ls -l", () => { + const { text } = formatWithOptions({ _: ["ll"] }); + + expect(text).toMatchInlineSnapshot(` +"pkg-1 v1.0.0 pkgs/pkg-1 +pkg-2 MISSING pkgs/pkg-2" +`); + }); + }); +}); diff --git a/utils/listable/__tests__/listable-options.test.js b/utils/listable/__tests__/listable-options.test.js new file mode 100644 index 0000000000..443c36b8c2 --- /dev/null +++ b/utils/listable/__tests__/listable-options.test.js @@ -0,0 +1,36 @@ +"use strict"; + +const yargs = require("yargs/yargs"); +const listable = require(".."); + +describe("listable.options()", () => { + const parsed = (...args) => listable.options(yargs()).parse(args.join(" ")); + + it("provides --json", () => { + expect(parsed("--json")).toHaveProperty("json", true); + }); + + it("provides --all", () => { + expect(parsed("--all")).toHaveProperty("all", true); + }); + + it("provides --all alias -a", () => { + expect(parsed("-a")).toHaveProperty("all", true); + }); + + it("provides --long", () => { + expect(parsed("--long")).toHaveProperty("long", true); + }); + + it("provides --long alias -l", () => { + expect(parsed("-l")).toHaveProperty("long", true); + }); + + it("provides --parseable", () => { + expect(parsed("--parseable")).toHaveProperty("parseable", true); + }); + + it("provides --parseable alias -p", () => { + expect(parsed("-p")).toHaveProperty("parseable", true); + }); +}); diff --git a/utils/listable/index.js b/utils/listable/index.js new file mode 100644 index 0000000000..50f6d447a2 --- /dev/null +++ b/utils/listable/index.js @@ -0,0 +1,4 @@ +"use strict"; + +exports.format = require("./lib/listable-format"); +exports.options = require("./lib/listable-options"); diff --git a/utils/listable/lib/listable-format.js b/utils/listable/lib/listable-format.js new file mode 100644 index 0000000000..208539b333 --- /dev/null +++ b/utils/listable/lib/listable-format.js @@ -0,0 +1,129 @@ +"use strict"; + +const chalk = require("chalk"); +const columnify = require("columnify"); +const path = require("path"); + +module.exports = listableFormat; + +function listableFormat(pkgList, options) { + const viewOptions = parseViewOptions(options); + const resultList = filterResultList(pkgList, viewOptions); + const count = resultList.length; + + let text; + + if (viewOptions.showJSON) { + text = formatJSON(resultList); + } else if (viewOptions.showParseable) { + text = formatParseable(resultList, viewOptions); + } else { + text = formatColumns(resultList, viewOptions); + } + + return { text, count }; +} + +function parseViewOptions(options) { + const alias = options._[0]; + + return { + showAll: alias === "la" || options.all, + showLong: alias === "la" || alias === "ll" || options.long, + showJSON: options.json, + showParseable: options.parseable, + }; +} + +function filterResultList(pkgList, viewOptions) { + return viewOptions.showAll ? pkgList.slice() : pkgList.filter(pkg => !pkg.private); +} + +function formatJSON(resultList) { + // explicit re-mapping exposes non-enumerable properties + const data = resultList.map(pkg => ({ + name: pkg.name, + version: pkg.version, + private: pkg.private, + location: pkg.location, + })); + + return JSON.stringify(data, null, 2); +} + +function makeParseable(pkg) { + const result = [pkg.location, pkg.name]; + + // sometimes the version is inexplicably missing? + if (pkg.version) { + result.push(pkg.version); + } else { + result.push("MISSING"); + } + + if (pkg.private) { + result.push("PRIVATE"); + } + + return result.join(":"); +} + +function formatParseable(resultList, viewOptions) { + return resultList.map(viewOptions.showLong ? makeParseable : pkg => pkg.location).join("\n"); +} + +function getColumnOrder(viewOptions) { + const columns = ["name"]; + + if (viewOptions.showLong) { + columns.push("version", "location"); + } + + if (viewOptions.showAll) { + columns.push("private"); + } + + return columns; +} + +function trimmedColumns(formattedResults, viewOptions) { + const str = columnify(formattedResults, { + showHeaders: false, + columns: getColumnOrder(viewOptions), + config: { + version: { + align: "right", + }, + }, + }); + + // columnify leaves a lot of trailing space in the last column, remove that here + return str + .split("\n") + .map(line => line.trimRight()) + .join("\n"); +} + +function formatColumns(resultList, viewOptions) { + const formattedResults = resultList.map(result => { + const formatted = { + name: result.name, + }; + + if (result.version) { + formatted.version = chalk.green(`v${result.version}`); + } else { + formatted.version = chalk.yellow("MISSING"); + } + + if (result.private) { + formatted.private = `(${chalk.red("PRIVATE")})`; + } + + formatted.location = chalk.grey(path.relative(".", result.location)); + + return formatted; + }); + + return trimmedColumns(formattedResults, viewOptions); +} diff --git a/utils/listable/lib/listable-options.js b/utils/listable/lib/listable-options.js new file mode 100644 index 0000000000..46cd442232 --- /dev/null +++ b/utils/listable/lib/listable-options.js @@ -0,0 +1,31 @@ +"use strict"; + +module.exports = listableOptions; + +function listableOptions(yargs) { + return yargs.options({ + json: { + group: "Command Options:", + describe: "Show information as a JSON array", + type: "boolean", + }, + a: { + group: "Command Options:", + describe: "Show private packages that are normally hidden", + type: "boolean", + alias: "all", + }, + l: { + group: "Command Options:", + describe: "Show extended information", + type: "boolean", + alias: "long", + }, + p: { + group: "Command Options:", + describe: "Show parseable output instead of columnified view", + type: "boolean", + alias: "parseable", + }, + }); +} diff --git a/utils/listable/package.json b/utils/listable/package.json new file mode 100644 index 0000000000..0efc396389 --- /dev/null +++ b/utils/listable/package.json @@ -0,0 +1,34 @@ +{ + "name": "@lerna/listable", + "version": "3.0.0-rc.0", + "description": "Shared logic for listing package information", + "keywords": [ + "lerna", + "utils" + ], + "author": "Daniel Stockman ", + "homepage": "https://github.com/lerna/lerna/tree/master/utils/listable#readme", + "license": "MIT", + "main": "index.js", + "files": [ + "index.js", + "lib" + ], + "engines": { + "node": ">= 6.9.0" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/lerna/lerna.git" + }, + "scripts": { + "test": "echo \"Error: run tests from root\" && exit 1" + }, + "dependencies": { + "chalk": "^2.3.1", + "columnify": "^1.5.4" + } +}