pkg-stats 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,7 @@
1
1
  import { expect, test } from 'vitest';
2
2
  import { renderChart } from '../chart.js';
3
3
  test('renderChart basic tests', () => {
4
- expect(renderChart(0.0, { length: 10 })).toMatchInlineSnapshot(`" "`);
4
+ expect(renderChart(0.0, { length: 10 })).toMatchInlineSnapshot(`""`);
5
5
  expect(renderChart(0.5, { length: 10 })).toMatchInlineSnapshot(`"█████ "`);
6
6
  expect(renderChart(1.0, { length: 10 })).toMatchInlineSnapshot(`"██████████"`);
7
7
  });
package/dist/bin.js CHANGED
@@ -1,60 +1,22 @@
1
1
  import chalk from 'chalk';
2
- import { renderChart } from './chart.js';
3
- import { parseCliOptions } from './cli-options.js';
4
- import { getColors } from './colors.js';
5
- import { fetchNpmLastWeekDownloads } from './npm-api.js';
6
- import { groupByType, pickTopStats } from './stats.js';
7
- import { parseVersion, versionCompare } from './version.js';
2
+ import { parseCliOptions, showHelp } from './cli-options.js';
3
+ import { comparePackages } from './mode/compare-packages.js';
4
+ import { printPackageStats } from './mode/package-stats.js';
8
5
  export async function pkgStats(argv) {
9
- const options = parseCliOptions(argv);
10
- let data;
6
+ let options;
11
7
  try {
12
- data = await fetchNpmLastWeekDownloads(options.packageName);
8
+ options = parseCliOptions(argv);
13
9
  }
14
10
  catch (error) {
15
- console.error(`Failed to fetch data for package "${options.packageName}"`, error);
16
- return;
11
+ showHelp();
12
+ console.error(chalk.red(`Error parsing CLI options: ${error instanceof Error ? error.message : error}`));
13
+ process.exit(2);
17
14
  }
18
- if (!Object.keys(data.downloads).length) {
19
- console.error(`No data found for package "${options.packageName}".\n`);
20
- process.exit(1);
15
+ if (options.packageNames.length === 1) {
16
+ await printPackageStats(options.packageNames[0], options);
21
17
  }
22
- const rawStats = Object.keys(data.downloads)
23
- .map((versionString) => {
24
- const version = parseVersion(versionString);
25
- return {
26
- ...version,
27
- downloads: data.downloads[versionString],
28
- };
29
- })
30
- .sort(versionCompare);
31
- const groupedStats = groupByType(options.group, rawStats);
32
- const totalDownloads = Object.values(groupedStats).reduce((sum, version) => sum + version.downloads, 0);
33
- const groupedStatsToDisplay = options.top
34
- ? pickTopStats(groupedStats, options.top)
35
- : groupedStats;
36
- const colors = getColors(groupedStatsToDisplay.length, options.color);
37
- const primaryColor = chalk.hex(colors[0]);
38
- console.log(chalk.bold(`\nNPM weekly downloads for ${primaryColor(options.packageName)}\n`));
39
- console.log(`Total: ${primaryColor(totalDownloads.toLocaleString())} last week\n`);
40
- console.log(options.top ? `Top ${options.top} versions:\n` : 'By version:\n');
41
- const maxDownloads = Math.max(...groupedStats.map((v) => v.downloads));
42
- groupedStatsToDisplay.forEach((item, i) => {
43
- const versionParts = item.versionString.split('.');
44
- const version = versionParts.length < 3 ? `${item.versionString}.x` : item.versionString;
45
- const chart = renderChart(item.downloads / maxDownloads);
46
- const downloads = formatDownloads(item.downloads, maxDownloads);
47
- const color = chalk.hex(colors[i]);
48
- console.log(`${version.padStart(8)} ${color(chart)} ${color(downloads.padStart(6))}`);
49
- });
50
- console.log('');
51
- }
52
- function formatDownloads(downloads, maxDownloads) {
53
- if (maxDownloads > 1000000) {
54
- return `${(downloads / 1000000).toFixed(1)}M`;
18
+ else {
19
+ await comparePackages(options.packageNames, options);
55
20
  }
56
- if (maxDownloads > 1000) {
57
- return `${(downloads / 1000).toFixed(1)}K`;
58
- }
59
- return downloads.toString();
21
+ console.log('');
60
22
  }
package/dist/chart.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export function renderChart(value, { length = 50 } = {}) {
2
2
  const filledChars = Math.round(value * length);
3
- const emptyChars = length - filledChars;
4
- return ''.repeat(filledChars) + ' '.repeat(emptyChars);
3
+ if (filledChars === 0) {
4
+ return '' + ' '.repeat(length - 1);
5
+ }
6
+ return '█'.repeat(filledChars) + ' '.repeat(length - filledChars);
5
7
  }
@@ -1,24 +1,106 @@
1
- import { program } from 'commander';
1
+ import chalk from 'chalk';
2
+ import meow from 'meow';
3
+ import redent from 'redent';
2
4
  import { COLOR_SCHEMES } from './colors.js';
5
+ const colorCommand = chalk.hex('#22c1c3');
6
+ const colorOption = chalk.hex('#fdbb2d');
7
+ const HELP = `
8
+ ${colorCommand('pkg-stats')} <package> - Show version stats
9
+ ${colorCommand('pkg-stats')} <package-1> <package-2>... - Compare between packages
10
+
11
+ Options:
12
+ ${colorOption('--major')} Group by major version
13
+ ${colorOption('--minor')} Group by minor version
14
+ ${colorOption('--patch')} Group by patch version
15
+ ${colorOption('-t, --top')} <number> Show top <number> most downloaded versions
16
+ ${colorOption('-a, --all')} Include all versions in output, even those with minimal downloads
17
+ ${colorOption('-c, --color')} <scheme> ${wrapOption(`Choose color scheme from: ${COLOR_SCHEMES.sort().join(', ')}`, 50, 24)}
18
+
19
+ Examples:
20
+ ${chalk.dim('# Show stats for react')}
21
+ ${colorCommand('pkg-stats')} react
22
+
23
+ ${chalk.dim('# Compare react, vue, angular and svelte')}
24
+ ${colorCommand('pkg-stats')} react vue @angular/core svelte
25
+
26
+ ${chalk.dim('# Show top 10 major versions of lodash')}
27
+ ${colorCommand('pkg-stats')} lodash ${colorOption('--major -t 10')}
28
+ `;
29
+ function wrapOption(text, maxLength, indent) {
30
+ const words = text.split(' ');
31
+ let result = '';
32
+ let currentLine = words[0];
33
+ for (let i = 1; i < words.length; i++) {
34
+ const nextCurrentLine = currentLine + ' ' + words[i];
35
+ if (nextCurrentLine.length <= maxLength) {
36
+ currentLine = nextCurrentLine;
37
+ }
38
+ else {
39
+ result += `\n${currentLine}`;
40
+ currentLine = words[i];
41
+ }
42
+ }
43
+ if (currentLine) {
44
+ result += `\n${currentLine}`;
45
+ }
46
+ return redent(result.trim(), indent).trim();
47
+ }
48
+ export function showHelp() {
49
+ console.log(redent(HELP, 2));
50
+ }
3
51
  export function parseCliOptions(argv) {
4
- program
5
- .name('pkg-stats')
6
- .description('Show NPM weekly downloads stats for a package')
7
- .argument('<package-name>', 'Package name')
8
- .option('--major', 'Group by major version')
9
- .option('--minor', 'Group by minor version')
10
- .option('--patch', 'Group by patch version')
11
- .option('-t, --top <number>', 'Show top <number> versions')
12
- .option('-c, --color <color>', 'Color scheme: ' + COLOR_SCHEMES.join(', '))
13
- .parse(argv);
14
- const args = program.args;
15
- const options = program.opts();
52
+ const cli = meow(HELP, {
53
+ argv: argv.slice(2),
54
+ autoHelp: true,
55
+ description: 'Show NPM weekly downloads stats:',
56
+ importMeta: import.meta,
57
+ flags: {
58
+ help: {
59
+ type: 'boolean',
60
+ shortFlag: 'h',
61
+ },
62
+ major: {
63
+ type: 'boolean',
64
+ shortFlag: 'm',
65
+ },
66
+ minor: {
67
+ type: 'boolean',
68
+ },
69
+ patch: {
70
+ type: 'boolean',
71
+ },
72
+ top: {
73
+ shortFlag: 't',
74
+ type: 'number',
75
+ },
76
+ all: {
77
+ type: 'boolean',
78
+ shortFlag: 'a',
79
+ },
80
+ color: {
81
+ shortFlag: 'c',
82
+ type: 'string',
83
+ choices: COLOR_SCHEMES,
84
+ },
85
+ },
86
+ });
87
+ if (cli.flags.help) {
88
+ cli.showHelp();
89
+ }
90
+ if (!cli.input.length) {
91
+ throw new Error('At least one <package-name> is required');
92
+ }
16
93
  return {
17
- packageName: args[0],
18
- group: options.major ? 'major' : options.minor ? 'minor' : options.patch ? 'patch' : undefined,
19
- top: options.top !== undefined ? parseInt(options.top) : undefined,
20
- color: options.color && COLOR_SCHEMES.includes(options.color)
21
- ? options.color
22
- : undefined,
94
+ packageNames: cli.input,
95
+ group: cli.flags.major
96
+ ? 'major'
97
+ : cli.flags.minor
98
+ ? 'minor'
99
+ : cli.flags.patch
100
+ ? 'patch'
101
+ : undefined,
102
+ top: cli.flags.top,
103
+ all: cli.flags.all,
104
+ color: cli.flags.color,
23
105
  };
24
106
  }
package/dist/format.js ADDED
@@ -0,0 +1,9 @@
1
+ export function formatDownloads(downloads, maxDownloads) {
2
+ if (maxDownloads > 1000000) {
3
+ return `${(downloads / 1000000).toFixed(1)}M`;
4
+ }
5
+ if (maxDownloads > 1000) {
6
+ return `${(downloads / 1000).toFixed(1)}K`;
7
+ }
8
+ return downloads.toString();
9
+ }
@@ -0,0 +1,48 @@
1
+ import chalk from 'chalk';
2
+ import { renderChart } from '../chart.js';
3
+ import { getColors } from '../colors.js';
4
+ import { formatDownloads } from '../format.js';
5
+ import { fetchNpmLastWeekDownloads } from '../npm-api.js';
6
+ export async function comparePackages(packageNames, options) {
7
+ const rawPackages = await Promise.all(packageNames.map((packageName) => fetchPackageData(packageName)));
8
+ const packagesToDisplay = rawPackages
9
+ .filter((pkg) => pkg !== undefined)
10
+ .sort((a, b) => b.downloads - a.downloads);
11
+ if (packagesToDisplay.length === 0) {
12
+ console.error(chalk.red('\nNo packages found.\n'));
13
+ process.exit(1);
14
+ }
15
+ console.log(chalk.bold(`\nNPM weekly downloads\n`));
16
+ const maxDownloads = Math.max(...packagesToDisplay.map((v) => v.downloads));
17
+ const displayData = packagesToDisplay.map((item) => {
18
+ return {
19
+ name: item.packageName,
20
+ chart: renderChart(item.downloads / maxDownloads),
21
+ downloads: formatDownloads(item.downloads, maxDownloads),
22
+ };
23
+ });
24
+ const maxNameLength = Math.max(...displayData.map((item) => item.name.length));
25
+ const maxDownloadsLength = Math.max(...displayData.map((item) => item.downloads.length));
26
+ const colors = getColors(packagesToDisplay.length, options.color);
27
+ displayData.forEach((item, i) => {
28
+ const color = chalk.hex(colors[i]);
29
+ console.log(`${item.name.padStart(2 + maxNameLength)} ${color(item.chart)} ${color(item.downloads.padStart(maxDownloadsLength))}`);
30
+ });
31
+ }
32
+ async function fetchPackageData(packageName) {
33
+ try {
34
+ const data = await fetchNpmLastWeekDownloads(packageName);
35
+ if (!Object.keys(data.downloads).length) {
36
+ console.warn(chalk.yellow(`No data found for package "${packageName}".`));
37
+ return undefined;
38
+ }
39
+ return {
40
+ packageName,
41
+ downloads: Object.values(data.downloads).reduce((sum, downloads) => sum + downloads, 0),
42
+ };
43
+ }
44
+ catch (error) {
45
+ console.warn(chalk.yellow(`Failed to fetch data for package "${packageName}"`, error));
46
+ return undefined;
47
+ }
48
+ }
@@ -0,0 +1,57 @@
1
+ import chalk from 'chalk';
2
+ import { renderChart } from '../chart.js';
3
+ import { getColors } from '../colors.js';
4
+ import { formatDownloads } from '../format.js';
5
+ import { fetchNpmLastWeekDownloads } from '../npm-api.js';
6
+ import { filterStats, groupStats } from '../stats.js';
7
+ import { parseVersion, versionCompare } from '../version.js';
8
+ export async function packageDetails(packageName, options) {
9
+ let data;
10
+ try {
11
+ data = await fetchNpmLastWeekDownloads(packageName);
12
+ }
13
+ catch (error) {
14
+ console.error(`Failed to fetch data for package "${packageName}"`, error);
15
+ return;
16
+ }
17
+ if (!Object.keys(data.downloads).length) {
18
+ console.error(`No data found for package "${packageName}".\n`);
19
+ process.exit(1);
20
+ }
21
+ const npmStats = Object.keys(data.downloads)
22
+ .map((versionString) => {
23
+ const version = parseVersion(versionString);
24
+ return {
25
+ ...version,
26
+ downloads: data.downloads[versionString],
27
+ };
28
+ })
29
+ .sort(versionCompare);
30
+ const { type, stats } = groupStats(npmStats, options.group);
31
+ const totalDownloads = Object.values(stats).reduce((sum, version) => sum + version.downloads, 0);
32
+ const statsToDisplay = filterStats(stats, {
33
+ totalDownloads,
34
+ all: options.all,
35
+ top: options.top,
36
+ });
37
+ const colors = getColors(statsToDisplay.length, options.color);
38
+ const primaryColor = chalk.hex(colors[0]);
39
+ console.log(chalk.bold(`\nNPM weekly downloads for ${primaryColor(packageName)}\n`));
40
+ console.log(`Total: ${primaryColor(totalDownloads.toLocaleString())} last week\n`);
41
+ console.log(options.top ? `Top ${options.top} ${type} versions:\n` : `By ${type} version:\n`);
42
+ const maxDownloads = Math.max(...stats.map((v) => v.downloads));
43
+ const displayData = statsToDisplay.map((item) => {
44
+ const versionParts = item.versionString.split('.');
45
+ return {
46
+ version: versionParts.length < 3 ? `${item.versionString}.x` : item.versionString,
47
+ chart: renderChart(item.downloads / maxDownloads),
48
+ downloads: formatDownloads(item.downloads, maxDownloads),
49
+ };
50
+ });
51
+ const maxVersionLength = Math.max(...displayData.map((item) => item.version.length));
52
+ const maxDownloadsLength = Math.max(...displayData.map((item) => item.downloads.length));
53
+ displayData.forEach((item, i) => {
54
+ const color = chalk.hex(colors[i]);
55
+ console.log(`${item.version.padStart(2 + maxVersionLength)} ${color(item.chart)} ${color(item.downloads.padStart(maxDownloadsLength))}`);
56
+ });
57
+ }
@@ -0,0 +1,57 @@
1
+ import chalk from 'chalk';
2
+ import { renderChart } from '../chart.js';
3
+ import { getColors } from '../colors.js';
4
+ import { formatDownloads } from '../format.js';
5
+ import { fetchNpmLastWeekDownloads } from '../npm-api.js';
6
+ import { filterStats, groupStats } from '../stats.js';
7
+ import { parseVersion, versionCompare } from '../version.js';
8
+ export async function printPackageStats(packageName, options) {
9
+ let data;
10
+ try {
11
+ data = await fetchNpmLastWeekDownloads(packageName);
12
+ }
13
+ catch (error) {
14
+ console.error(`Failed to fetch data for package "${packageName}"`, error);
15
+ return;
16
+ }
17
+ if (!Object.keys(data.downloads).length) {
18
+ console.error(`No data found for package "${packageName}".\n`);
19
+ process.exit(1);
20
+ }
21
+ const npmStats = Object.keys(data.downloads)
22
+ .map((versionString) => {
23
+ const version = parseVersion(versionString);
24
+ return {
25
+ ...version,
26
+ downloads: data.downloads[versionString],
27
+ };
28
+ })
29
+ .sort(versionCompare);
30
+ const { type, stats } = groupStats(npmStats, options.group);
31
+ const totalDownloads = Object.values(stats).reduce((sum, version) => sum + version.downloads, 0);
32
+ const statsToDisplay = filterStats(stats, {
33
+ totalDownloads,
34
+ all: options.all,
35
+ top: options.top,
36
+ });
37
+ const colors = getColors(statsToDisplay.length, options.color);
38
+ const primaryColor = chalk.hex(colors[0]);
39
+ console.log(chalk.bold(`\nNPM weekly downloads for ${primaryColor(packageName)}\n`));
40
+ console.log(`Total: ${primaryColor(totalDownloads.toLocaleString())} last week\n`);
41
+ console.log(options.top ? `Top ${options.top} ${type} versions:\n` : `By ${type} version:\n`);
42
+ const maxDownloads = Math.max(...stats.map((v) => v.downloads));
43
+ const displayData = statsToDisplay.map((item) => {
44
+ const versionParts = item.versionString.split('.');
45
+ return {
46
+ version: versionParts.length < 3 ? `${item.versionString}.x` : item.versionString,
47
+ chart: renderChart(item.downloads / maxDownloads),
48
+ downloads: formatDownloads(item.downloads, maxDownloads),
49
+ };
50
+ });
51
+ const maxVersionLength = Math.max(...displayData.map((item) => item.version.length));
52
+ const maxDownloadsLength = Math.max(...displayData.map((item) => item.downloads.length));
53
+ displayData.forEach((item, i) => {
54
+ const color = chalk.hex(colors[i]);
55
+ console.log(`${item.version.padStart(2 + maxVersionLength)} ${color(item.chart)} ${color(item.downloads.padStart(maxDownloadsLength))}`);
56
+ });
57
+ }
@@ -0,0 +1,58 @@
1
+ import chalk from 'chalk';
2
+ import { renderChart } from '../chart.js';
3
+ import { getColors } from '../colors.js';
4
+ import { formatDownloads } from '../format.js';
5
+ import { fetchNpmLastWeekDownloads } from '../npm-api.js';
6
+ import { filterStats, groupStats } from '../stats.js';
7
+ import { parseVersion, versionCompare } from '../version.js';
8
+ export async function singlePackage(packageName, options) {
9
+ let data;
10
+ try {
11
+ data = await fetchNpmLastWeekDownloads(packageName);
12
+ }
13
+ catch (error) {
14
+ console.error(`Failed to fetch data for package "${packageName}"`, error);
15
+ return;
16
+ }
17
+ if (!Object.keys(data.downloads).length) {
18
+ console.error(`No data found for package "${packageName}".\n`);
19
+ process.exit(1);
20
+ }
21
+ const npmStats = Object.keys(data.downloads)
22
+ .map((versionString) => {
23
+ const version = parseVersion(versionString);
24
+ return {
25
+ ...version,
26
+ downloads: data.downloads[versionString],
27
+ };
28
+ })
29
+ .sort(versionCompare);
30
+ const { type, stats } = groupStats(npmStats, options.group);
31
+ const totalDownloads = Object.values(stats).reduce((sum, version) => sum + version.downloads, 0);
32
+ const statsToDisplay = filterStats(stats, {
33
+ totalDownloads,
34
+ all: options.all,
35
+ top: options.top,
36
+ });
37
+ const colors = getColors(statsToDisplay.length, options.color);
38
+ const primaryColor = chalk.hex(colors[0]);
39
+ console.log(chalk.bold(`\nNPM weekly downloads for ${primaryColor(packageName)}\n`));
40
+ console.log(`Total: ${primaryColor(totalDownloads.toLocaleString())} last week\n`);
41
+ console.log(options.top ? `Top ${options.top} ${type} versions:\n` : `By ${type} version:\n`);
42
+ const maxDownloads = Math.max(...stats.map((v) => v.downloads));
43
+ const displayData = statsToDisplay.map((item) => {
44
+ const versionParts = item.versionString.split('.');
45
+ return {
46
+ version: versionParts.length < 3 ? `${item.versionString}.x` : item.versionString,
47
+ chart: renderChart(item.downloads / maxDownloads),
48
+ downloads: formatDownloads(item.downloads, maxDownloads),
49
+ };
50
+ });
51
+ const maxVersionLength = Math.max(...displayData.map((item) => item.version.length));
52
+ const maxDownloadsLength = Math.max(...displayData.map((item) => item.downloads.length));
53
+ displayData.forEach((item, i) => {
54
+ const color = chalk.hex(colors[i]);
55
+ console.log(`${item.version.padStart(2 + maxVersionLength)} ${color(item.chart)} ${color(item.downloads.padStart(maxDownloadsLength))}`);
56
+ });
57
+ console.log('');
58
+ }
File without changes
package/dist/stats.js CHANGED
@@ -1,23 +1,23 @@
1
1
  import { versionCompare } from './version.js';
2
- export function groupByType(type, stats) {
2
+ export function groupStats(stats, type) {
3
3
  if (type === 'major') {
4
- return groupByMajor(stats);
4
+ return { type: 'major', stats: groupByMajor(stats) };
5
5
  }
6
6
  if (type === 'minor') {
7
- return groupByMinor(stats);
7
+ return { type: 'minor', stats: groupByMinor(stats) };
8
8
  }
9
9
  if (type === 'patch') {
10
- return groupByPatch(stats);
10
+ return { type: 'patch', stats: groupByPatch(stats) };
11
11
  }
12
12
  const groupedByMajor = groupByMajor(stats);
13
- if (groupedByMajor.length > 1) {
14
- return groupedByMajor;
13
+ if (groupedByMajor.length >= 3) {
14
+ return { type: 'major', stats: groupedByMajor };
15
15
  }
16
16
  const groupedByMinor = groupByMinor(stats);
17
- if (groupedByMinor.length > 1) {
18
- return groupedByMinor;
17
+ if (groupedByMinor.length >= 3) {
18
+ return { type: 'minor', stats: groupedByMinor };
19
19
  }
20
- return groupByPatch(stats);
20
+ return { type: 'patch', stats: groupByPatch(stats) };
21
21
  }
22
22
  function groupByMajor(stats) {
23
23
  const result = {};
@@ -65,7 +65,17 @@ function groupByPatch(stats) {
65
65
  }
66
66
  return Object.values(result).sort((a, b) => versionCompare(a.version, b.version));
67
67
  }
68
- export function pickTopStats(stats, top) {
68
+ export function filterStats(stats, options) {
69
+ if (options.all) {
70
+ return stats;
71
+ }
72
+ if (options.top) {
73
+ return pickTopStats(stats, options.top);
74
+ }
75
+ const downloadThreshold = 0.005 * options.totalDownloads; // 0.5%
76
+ return stats.filter((stat) => stat.downloads >= downloadThreshold);
77
+ }
78
+ function pickTopStats(stats, top) {
69
79
  const sortedStats = stats.sort((a, b) => b.downloads - a.downloads);
70
80
  const topStats = sortedStats.slice(0, top);
71
81
  return topStats.sort((a, b) => versionCompare(a.version, b.version));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pkg-stats",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Beautiful NPM package download stats",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,18 +34,17 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "chalk": "^5.4.1",
37
- "commander": "^13.0.0",
37
+ "meow": "^13.2.0",
38
+ "redent": "^4.0.0",
38
39
  "tinygradient": "^1.1.5"
39
40
  },
40
41
  "devDependencies": {
41
42
  "@eslint/js": "^9.18.0",
42
43
  "@release-it/conventional-changelog": "^10.0.0",
43
- "@types/minimist": "^1.2.5",
44
44
  "@types/node": "^22.10.5",
45
45
  "eslint": "^9.18.0",
46
46
  "eslint-plugin-simple-import-sort": "^12.1.1",
47
47
  "globals": "^15.14.0",
48
- "redent": "^4.0.0",
49
48
  "release-it": "^18.1.1",
50
49
  "typescript": "^5.7.3",
51
50
  "typescript-eslint": "^8.19.1",