Skip to content

Instantly share code, notes, and snippets.

@aparajita
Last active January 4, 2020 00:07
Show Gist options
  • Save aparajita/3311405ab7a047426c9a9e2c07852880 to your computer and use it in GitHub Desktop.
Save aparajita/3311405ab7a047426c9a9e2c07852880 to your computer and use it in GitHub Desktop.
npmls: A (much) better npm listing tool
#!/usr/bin/env node
"use strict";
const child_process = require("child_process");
const fs = require("fs");
const help = `Usage: npmls.js [-gh|--help] [filter...]
** Requires Node >= 4 **
Displays an alphabetical listing of top level packages.
OPTIONS
-g Display global packages.
-h|--help Display this help.
FILTERING
By default, all of the packages are listed in alphabetical order.
When listing the current directory, the current package is listed
at the top.
If you only want to see specific packages, you can filter the package names
in the same way the 'ls' command filters filenames. The following matching
characters are valid:
* Replaces zero or more characters
? Replaces one character
{one,two[,...]} Selects a comma-delimited list of alternates
Passing more than filter ORs the results of each filter. To avoid shell escaping,
filters that use matching characters should be quote enclosed.
EXAMPLES
Given the packages: acorn, eslint, gulp-eslint, gulp-jscs, jscs
'eslint' => eslint
'*eslint*' => eslint, gulp-eslint
'*eslint*' '*jscs*' => eslint, gulp-eslint, gulp-jscs, jscs
'*{eslint,jscs}*' => eslint, gulp-eslint, gulp-jscs, jscs
'gulp*' => gulp-eslint, gulp-jscs`;
function parseOptions() {
const options = { filters: [] };
// Skip 'node' and this script in argv
for (const arg of process.argv.slice(2)) {
let option;
switch (arg) {
case "-g":
options.global = true;
break;
case "-h":
case "--help":
options.help = true;
break;
default:
if (arg[0] === "-") {
console.log(`unknown option '${arg}'`);
process.exit(1);
} else {
options.filters.push(arg);
}
}
}
return options;
}
function makeFilterRegex(args) {
args = args.map(arg =>
arg
.replace(/\*/g, ".*")
.replace(/\?/g, ".")
.replace(/\{.+?}/g, match => {
const alternates = match
.slice(1, -1)
.split(",")
.map(item => item.trim());
return "(" + alternates.join("|") + ")";
})
);
return new RegExp("^(?:" + args.join("|") + ")$");
}
function parsePackage(pkg) {
const match = /^(.+\s+)?([@/\w.-]+?)@(\S+)/.exec(pkg);
if (match) {
return {
name: match[2],
version: match[3],
warning: match[1]
};
}
}
function ls() {
const options = parseOptions();
if (options.help) {
console.log(help);
process.exit(0);
}
let pkg = {};
if (!options.global) {
try {
pkg = JSON.parse(fs.readFileSync("./package.json", "utf8"));
} catch (e) {
console.error(e.message);
return;
}
}
const dependencies = Object.keys(pkg.dependencies || {});
const devDependencies = Object.keys(pkg.devDependencies || {});
let cmd = "npm ls --depth 0";
if (options.global) {
cmd += " -g";
}
child_process.exec(cmd, (error, stdout, stderr) => {
if (error && !stdout) {
console.error(error.toString());
process.exit(1);
} else {
let lines = stdout.trim().split("\n");
const firstLine = lines.shift();
lines = lines.map(line => line.substr(4));
if (options.global) {
console.log(`\x1b[32m\x1b[1m${firstLine}\x1b[0m`);
} else {
// Start with the current package
const { name, version } = parsePackage(firstLine);
console.log(`\x1b[4m\x1b[32m\x1b[1m${name}\x1b[0m \x1b[37m(${version})\x1b[0m`);
}
let filter;
if (options.filters.length) {
filter = makeFilterRegex(options.filters);
}
const modules = lines.reduce((result, line) => {
const info = parsePackage(line);
if (info) {
result.push(info);
}
return result;
}, []);
for (const module of modules) {
if (filter && !filter.test(module.name)) {
continue;
}
const color = (options.global || dependencies.includes(module.name)) ? '\x1b[33m' : '';
const warning = module.warning ? ` \x1b[31m[${module.warning.trim()}]\x1b[0m` : '';
console.log(`${color}${module.name}\x1b[0m \x1b[37m(${module.version})\x1b[0m${warning}`);
}
}
});
}
ls();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment