pjv
Version:
package.json visualizer
358 lines (332 loc) • 11.7 kB
text/typescript
/*=============================================================================
pjv - package.json visualization tool
- an alternative of cat and bat tools providing formatted view of config files:
.env: aligned table
package.json: especially the "scripts" field
- Installation
$ yarn global add pjv
- Option arguments
s - print scripts only in package.json and .env
https://docs.npmjs.com/cli/v7/configuring-npm/package-json
TODO: When the element of array is an object, formatting is required. [object Object]
- test case: ~/.config/yarn/global/node_modules/yaml/
(C) 2022 SpacetimeQ INC
=============================================================================*/
import fs from 'fs';
import dotenv from 'dotenv';
import path from 'path';
import chalk from 'chalk'; // for TypeScript use version ^4
import semver from 'semver';
import { exec } from 'child_process';
const SEP = chalk.gray(': '); // separator between key and value
const SEP0 = chalk.gray(':');
const BULLET = '▫️ ';
const MIN_PAD_VER = 6; // minimum padding for version strings
const SEMVERSIGN = new Map([ // semverDiff
['major', '❌'],
['minor', '✅'],
['patch', '✔️ '],
]);
type TVMODE = 'all'|'scripts'|'dotenv'|'xoutdated';
// JSON object in typescript, Recursive Type Alias
type JSONValue =
| string
| number
| boolean
| JSONObject
| Array<JSONValue>;
interface JSONObject { [x: string]: JSONValue };
interface TVersions {
current: string;
wanted: string;
latest: string;
};
interface TOutdatedResult extends TVersions {
dependent: string;
location: string;
};
interface TPackages {
dep: string|undefined;
name: string;
dev: boolean; // devDependencies
ver: string; // version in package.json
outd?: TVersions; // outdated
};
const packVers: TPackages[] = [];
let packageName = '';
const dirBase = path.basename(process.cwd());
const isObject = (o: Object) =>
(typeof (o) === 'object') && !Array.isArray(o); // array also is an 'object'
let rootPath = './';
const PACKAGE = 'package.json';
const DOTENV = '.env';
const HELP = [ // key-value pairs with the original insertion order of the keys
['v', "version", "package version"],
['e', "env", ".env only"],
['s', "scripts", "scripts (in package.json) only"],
['x', "xcheck", "skip checking npm outdated packages that takes time"],
['h', "help", "help"],
];
const [,, ...args] = process.argv;
let vmode: TVMODE = 'all';
if (args[0]) {
const arg = args[0];
const { name, version, description } = require('../' + PACKAGE); // own package
const option = (arg[0] === '-') // allow up to two '-' for the option prefix
? (arg[1] === '-')
? arg[2] // arg.substring(2) // long
: arg[1] // short
: '';
if (option) {
switch (option.toLowerCase()[0]) {
case 'e': vmode = 'dotenv'; break;
case 's': vmode = 'scripts'; break;
case 'x': vmode = 'xoutdated'; break;
case 'v':
console.log(version);
// console.log(process.env.npm_package_version);
process.exit(0);
default:
console.log(chalk.yellowBright(name), chalk.greenBright('v' + version), description,
'-', chalk.yellowBright('node'), chalk.greenBright(process.version));
console.log(' ', chalk.gray('by'), chalk.blue("SPACE") + chalk.green('TIME') + chalk.red('Q'));
console.log(chalk.yellow("OPTIONS:"));
let pad=7; // set the longest length in HELP's long option
HELP.forEach(arr => {
if (arr[1].length > pad)
pad = arr[1].length;
});
HELP.forEach(arr =>
console.log(' ',
chalk.green('-' + arr[0]) + ', ' +
chalk.green('--' + arr[1].padEnd(pad)), arr[2])
);
process.exit(0);
}
} else {
rootPath = arg; // TODO: REGEX check required
if (rootPath[rootPath.length-1] !== '/')
rootPath += '/';
}
}
// arg: a | A - all
const SCRIPT_HL = [ // highlighted script
'start',
'serve',
'build',
];
// pad should be the length of the longest key + 1
const maxKeyLen = (obj: JSONObject | Array<string>) => {
let len=0;
(Array.isArray(obj) ? obj : Object.keys(obj)).forEach((key) => {
if (key.length > len)
len = key.length;
});
return len;
}
// log filename
const logFilename = (name: string) =>
console.log(chalk.white.bgGray(` ${name}`.padEnd(80)));
const logField = (field: string) =>
console.log(chalk.black.bgCyan(` ◀︎ ${field} ▶︎ `));
const logNumbering = (n: number) =>
chalk.gray(`${(n+1).toString().padStart(3)}`);
const logSubObj = (obj: JSONObject, key: string) => {
console.log(BULLET, chalk.cyanBright(key));
Object.entries(obj).forEach(([kv, vv], iv) => {
if (isObject(vv)) {
logSubObj(vv as JSONObject, kv); // recursively call
} else {
const pk = ` ${kv.padEnd(maxKeyLen(obj))} `;
console.log(logNumbering(iv), chalk.yellow(pk) + SEP + chalk.green(vv));
}
});
}
// iterate object
const logKeyValueObj = (obj: JSONObject, key: string) => {
const o = obj[key] as JSONObject;
const isScript = key === 'scripts';
const isDep = key.toLowerCase().includes('dependencies');
if (!isScript && vmode === 'scripts')
return;
if (isDep && vmode !== 'xoutdated') {
const dev = key === 'devDependencies';
Object.entries(o).forEach(([name, ver]) =>
packVers.push({ dep: dirBase, name, dev, ver: ver as string })
);
return; // just keep in packVers to print when outdated versions are fetched
}
logField(key);
Object.entries(o).forEach(([k, val], i) => {
if (isObject(val)) {
logSubObj(val as JSONObject, k);
} else {
const pk = ` ${k.padEnd(maxKeyLen(o))} `; // padded key
console.log(
logNumbering(i),
isScript
? SCRIPT_HL.includes(k)
? chalk.black.bgYellow(pk) + SEP + chalk.yellow(val)
: chalk.yellow(pk) + SEP + chalk.white(val)
: (isDep && k.includes('@'))
? chalk.redBright(pk) + SEP + chalk.blue(val)
: chalk.yellow(pk) + SEP + chalk.cyan(val)
);
}
});
}
const logKeyValue = (obj: JSONObject, key: string, pad: number) => {
let content = obj[key];
if (key === 'name' && packageName !== dirBase) {
content += '🚨' + chalk.redBright(' ≠../' + dirBase);
}
console.log(BULLET, chalk.cyanBright(`${key.padEnd(pad)}`), SEP0, content);
}
try {
fs.access(rootPath + DOTENV, fs.constants.F_OK, (err) => {
if (err) {
// console.log("No .env");
return;
}
logFilename('.env');
const env = dotenv.parse(fs.readFileSync(rootPath + DOTENV, 'utf8'));
const pad = maxKeyLen(env);
Object.entries(env).forEach(([key, val], i) => {
console.log(
logNumbering(i),
key.padEnd(pad) + SEP + chalk.greenBright(val)
);
});
});
} catch (err) {
console.error(err);
}
if (vmode !== 'dotenv') {
try {
fs.access(rootPath + PACKAGE, fs.constants.F_OK, (err) => {
if (err) { // file not found
console.log("⛔️ No", rootPath + PACKAGE);
process.exit(1);
}
const json = JSON.parse(fs.readFileSync(rootPath + PACKAGE, 'utf8'));
packageName = json.name;
console.log(
chalk.white.bgGray( ` ${PACKAGE} `) +
chalk.black.bgWhite(` ${packageName} `) +
chalk.black.bgGreen(` ${json.version || 'no version'} `) +
chalk.green.bgBlack(` ${json.description || ''} `)
);
// Partition keys array by its content if it is of pure object type or not
const [ob, no] = Object.keys(json).reduce<[string[], string[]]>
(([ob, no], k) =>
(isObject(json[k])
? [[...ob, k], no]
: [ob, [...no, k]]), [[], []]);
if (vmode !== 'scripts') {
const pad = maxKeyLen(no);
no.forEach(key => logKeyValue(json, key, pad));
}
ob.forEach(key => logKeyValueObj(json, key));
});
} catch (err) {
console.error(err);
}
}
/**
* package versions
* @param pack - package versions object
* @param pd - padding 0: package nanme, 1: package version, 2: Current, 3: Wanted, 4: Latest
*/
const logPackVers = (pack: TPackages[], pd: number[]) => {
if (!pack.length)
return;
const field = (pack[0].dev ? 'devDependencies' : 'dependencies').padEnd(pd[0] + pd[1] + 2);
console.log(
chalk.black.bgCyan(` ◀︎ ${field.padEnd(pd[0]+pd[1]+2)} ▶︎ `),
chalk.black.bgWhite((pd[2] > MIN_PAD_VER ? 'Current' : 'Cur').padStart(pd[2])),
chalk.black.bgGreen( 'Wanted'.padStart(pd[3])),
chalk.black.bgMagenta('Latest'.padStart(pd[4]))
);
pack.forEach((p, i) => {
const nk = ` ${p.name.padEnd(pd[0])} `;
let sn = ''; // sign
if (p.outd) {
const df = semver.diff(p.outd.current || p.outd.wanted, p.outd.latest) || '';
const svsn = df && SEMVERSIGN.get(df);
sn = svsn || df;
if (p.dep !== dirBase)
sn += ' ' + chalk.blueBright(p.dep);
}
console.log(
logNumbering(i),
p.name.includes('@')
? (p.name.includes('@types/')
? chalk.blueBright.italic(nk)
: chalk.redBright(nk) ) + SEP + chalk.blue(p.ver.padStart(pd[1]))
: chalk.yellow(nk) + SEP + chalk.cyan(p.ver.padStart(pd[1])),
p.outd
? chalk.white( (p.outd.current||'-').padStart(pd[2]) ) + ' ' +
chalk.greenBright( (p.outd.wanted ||' ').padStart(pd[3]) ) + ' ' +
chalk.magentaBright( (p.outd.latest ||' ').padStart(pd[4]) ) + ' ' + sn
: '✨'
);
});
}
/**
* assumption that packVers was already built by reading package.json
* depent name from 'npm outdated' is the current folder name
*/
if (
vmode !== 'xoutdated' &&
vmode !== 'dotenv' &&
vmode !== 'scripts'
) {
// --------------------------------------------------------------------------------
// takes time to run the external command
exec('npm outdated --json', (error, stdout) => { // stderr)
// --------------------------------------------------------------------------------
if (stdout) {
// console.log(stdout);
const pd = Array(5).fill(MIN_PAD_VER, 1); // minimum padding
pd[0]=0; // minimum padding for package name
const json: TOutdatedResult[] = JSON.parse(stdout);
Object.entries(json).forEach(([key, val]) => {
const { current, wanted, latest } = val;
if (current?.length > pd[2]) pd[2] = current.length;
if (wanted?.length > pd[3]) pd[3] = wanted.length;
if (latest?.length > pd[4]) pd[4] = latest.length;
if (val.dependent === dirBase) {
const fo = packVers.find(x => x.name === key);
if (fo) {
fo.outd = { current, wanted, latest };
} else {
console.error(key, "not found!");
}
} else { // dependent is another workspace
packVers.push({
dep: val.dependent,
name: key,
dev: false,
ver: '',
outd: { current, wanted, latest }
});
}
});
packVers.forEach(p => {
if (p.name.length > pd[0])
pd[0] = p.name.length;
if (p.ver.length > pd[1])
pd[1] = p.ver.length;
});
// partition array into prod dependencies and devDependencies
const [pro, dev] = packVers.reduce<[TPackages[], TPackages[]]>(([pro, dev], k) =>
(k.dev ? [pro, [...dev, k]] : [[...pro, k], dev]), [[], []]);
logPackVers(pro, pd);
logPackVers(dev, pd);
}
if (!error) {
console.log(packVers.length ? '✨No outdated modules!' : "⚠️ No dependencies.");
}
});
}