pkg-stats 0.3.1 → 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,11 +1,7 @@
1
1
  import chalk from 'chalk';
2
- import { renderChart } from './chart.js';
3
2
  import { parseCliOptions, showHelp } from './cli-options.js';
4
- import { getColors } from './colors.js';
5
- import { formatDownloads } from './format.js';
6
- import { fetchNpmLastWeekDownloads } from './npm-api.js';
7
- import { groupStats, pickTopStats } from './stats.js';
8
- import { parseVersion, versionCompare } from './version.js';
3
+ import { comparePackages } from './mode/compare-packages.js';
4
+ import { printPackageStats } from './mode/package-stats.js';
9
5
  export async function pkgStats(argv) {
10
6
  let options;
11
7
  try {
@@ -16,43 +12,11 @@ export async function pkgStats(argv) {
16
12
  console.error(chalk.red(`Error parsing CLI options: ${error instanceof Error ? error.message : error}`));
17
13
  process.exit(2);
18
14
  }
19
- let data;
20
- try {
21
- data = await fetchNpmLastWeekDownloads(options.packageName);
22
- }
23
- catch (error) {
24
- console.error(`Failed to fetch data for package "${options.packageName}"`, error);
25
- return;
15
+ if (options.packageNames.length === 1) {
16
+ await printPackageStats(options.packageNames[0], options);
26
17
  }
27
- if (!Object.keys(data.downloads).length) {
28
- console.error(`No data found for package "${options.packageName}".\n`);
29
- process.exit(1);
18
+ else {
19
+ await comparePackages(options.packageNames, options);
30
20
  }
31
- const npmStats = Object.keys(data.downloads)
32
- .map((versionString) => {
33
- const version = parseVersion(versionString);
34
- return {
35
- ...version,
36
- downloads: data.downloads[versionString],
37
- };
38
- })
39
- .sort(versionCompare);
40
- const { type, stats } = groupStats(npmStats, options.group);
41
- const totalDownloads = Object.values(stats).reduce((sum, version) => sum + version.downloads, 0);
42
- const statsToDisplay = options.top ? pickTopStats(stats, options.top) : stats;
43
- const colors = getColors(statsToDisplay.length, options.color);
44
- const primaryColor = chalk.hex(colors[0]);
45
- console.log(chalk.bold(`\nNPM weekly downloads for ${primaryColor(options.packageName)}\n`));
46
- console.log(`Total: ${primaryColor(totalDownloads.toLocaleString())} last week\n`);
47
- console.log(options.top ? `Top ${options.top} ${type} versions:\n` : `By ${type} version:\n`);
48
- const maxDownloads = Math.max(...stats.map((v) => v.downloads));
49
- statsToDisplay.forEach((item, i) => {
50
- const versionParts = item.versionString.split('.');
51
- const versionString = versionParts.length < 3 ? `${item.versionString}.x` : item.versionString;
52
- const chart = renderChart(item.downloads / maxDownloads);
53
- const downloads = formatDownloads(item.downloads, maxDownloads);
54
- const color = chalk.hex(colors[i]);
55
- console.log(`${versionString.padStart(8)} ${color(chart)} ${color(downloads.padStart(6))}`);
56
- });
57
21
  console.log('');
58
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,19 +1,50 @@
1
+ import chalk from 'chalk';
1
2
  import meow from 'meow';
2
3
  import redent from 'redent';
3
4
  import { COLOR_SCHEMES } from './colors.js';
5
+ const colorCommand = chalk.hex('#22c1c3');
6
+ const colorOption = chalk.hex('#fdbb2d');
4
7
  const HELP = `
5
- pkg-stats <package-name>
6
-
7
- Show NPM weekly downloads stats for a package
8
+ ${colorCommand('pkg-stats')} <package> - Show version stats
9
+ ${colorCommand('pkg-stats')} <package-1> <package-2>... - Compare between packages
8
10
 
9
11
  Options:
10
- -h, --help Show help
11
- --major Group by major version
12
- --minor Group by minor version
13
- --patch Group by patch version
14
- -t, --top <number> Show top <number> versions
15
- -c, --color <color> Color scheme: ${COLOR_SCHEMES.sort().join(', ')}
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')}
16
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
+ }
17
48
  export function showHelp() {
18
49
  console.log(redent(HELP, 2));
19
50
  }
@@ -21,7 +52,7 @@ export function parseCliOptions(argv) {
21
52
  const cli = meow(HELP, {
22
53
  argv: argv.slice(2),
23
54
  autoHelp: true,
24
- description: 'Show NPM weekly downloads stats for a package',
55
+ description: 'Show NPM weekly downloads stats:',
25
56
  importMeta: import.meta,
26
57
  flags: {
27
58
  help: {
@@ -42,6 +73,10 @@ export function parseCliOptions(argv) {
42
73
  shortFlag: 't',
43
74
  type: 'number',
44
75
  },
76
+ all: {
77
+ type: 'boolean',
78
+ shortFlag: 'a',
79
+ },
45
80
  color: {
46
81
  shortFlag: 'c',
47
82
  type: 'string',
@@ -52,11 +87,11 @@ export function parseCliOptions(argv) {
52
87
  if (cli.flags.help) {
53
88
  cli.showHelp();
54
89
  }
55
- if (!cli.input[0]) {
56
- throw new Error('<package-name> is required');
90
+ if (!cli.input.length) {
91
+ throw new Error('At least one <package-name> is required');
57
92
  }
58
93
  return {
59
- packageName: cli.input[0],
94
+ packageNames: cli.input,
60
95
  group: cli.flags.major
61
96
  ? 'major'
62
97
  : cli.flags.minor
@@ -65,6 +100,7 @@ export function parseCliOptions(argv) {
65
100
  ? 'patch'
66
101
  : undefined,
67
102
  top: cli.flags.top,
103
+ all: cli.flags.all,
68
104
  color: cli.flags.color,
69
105
  };
70
106
  }
@@ -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
@@ -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.1",
3
+ "version": "0.4.0",
4
4
  "description": "Beautiful NPM package download stats",
5
5
  "license": "MIT",
6
6
  "repository": {