pkg-stats 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,15 +2,34 @@
2
2
 
3
3
  Beautiful NPM package download stats.
4
4
 
5
+ ### Single package
6
+
5
7
  ```
6
8
  npx pkg-stats react
7
9
  ```
8
10
 
9
11
  <div align='center'>
10
- <img src="https://raw.githubusercontent.com/mdjastrzebski/pkg-stats/main/docs/public/example-react.png" alt="pkg-stats" height="210" width="538" />
12
+ <img src="https://raw.githubusercontent.com/mdjastrzebski/pkg-stats/main/docs/public/example-package.png" alt="Display single package stats" style="max-width: 610px; aspect-ratio: 610x374;" />
13
+ </div>
14
+
15
+ #### Options:
16
+
17
+ - `--major`, `--minor`, `--patch` - group by major, minor or patch version
18
+ - `--top <number>` (alias `-t`) - show top N versions
19
+ - `--color <scheme>` (alias `-c`) - specify color scheme
20
+ - available schemes: `atlas`, `cristal`, `fruit`, `insta`, `mind`, `morning`, `passion`, `pastel`, `rainbow`, `retro`, `summer`, `teen`, `vice`
21
+
22
+
23
+ ### Compare packages
24
+
25
+ ```
26
+ npx pkg-stats moment date-fns dayjs luxon @js-joda/core
27
+ ```
28
+
29
+ <div align='center'>
30
+ <img src="https://raw.githubusercontent.com/mdjastrzebski/pkg-stats/main/docs/public/example-compare.png" alt="Compare package stats" style="max-width: 610px; aspect-ratio: 610x374;" />
11
31
  </div>
12
32
 
13
- ### Options:
33
+ #### Options:
14
34
 
15
- - version grouping: `--group major|minor|patch` (alias `-g`, `--major`, `--minor`, `--patch`)
16
- - top version: `--top <number>` (alias `-t`)
35
+ - `--color <scheme>` (alias `-c`) - specify color scheme
@@ -0,0 +1,8 @@
1
+ import { expect, test } from 'vitest';
2
+ import { formatBar } from '../output.js';
3
+
4
+ test('renderChart basic tests', () => {
5
+ expect(formatBar(0.0, { length: 10 })).toMatchInlineSnapshot(`"▏ "`);
6
+ expect(formatBar(0.5, { length: 10 })).toMatchInlineSnapshot(`"█████ "`);
7
+ expect(formatBar(1.0, { length: 10 })).toMatchInlineSnapshot(`"██████████"`);
8
+ });
@@ -0,0 +1,7 @@
1
+ import { expect, test } from 'vitest';
2
+ import { formatBar } from '../output.js';
3
+ test('renderChart basic tests', () => {
4
+ expect(formatBar(0.0, { length: 10 })).toMatchInlineSnapshot(`"▏ "`);
5
+ expect(formatBar(0.5, { length: 10 })).toMatchInlineSnapshot(`"█████ "`);
6
+ expect(formatBar(1.0, { length: 10 })).toMatchInlineSnapshot(`"██████████"`);
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,51 @@
1
+ import chalk from 'chalk';
1
2
  import meow from 'meow';
2
3
  import redent from 'redent';
3
- import { COLOR_SCHEMES } from './colors.js';
4
+ import { COLOR_SCHEMES, getColorOfDay } 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('-x, --extended')} Show extended stats
18
+ ${colorOption('-c, --color')} <scheme> ${wrapOption(`Choose color scheme from: ${COLOR_SCHEMES.sort().join(', ')}`, 50, 24)}
19
+
20
+ Examples:
21
+ ${chalk.dim('# Show stats for react')}
22
+ ${colorCommand('pkg-stats')} react
23
+
24
+ ${chalk.dim('# Compare react, vue, angular and svelte')}
25
+ ${colorCommand('pkg-stats')} react vue @angular/core svelte
26
+
27
+ ${chalk.dim('# Show top 10 major versions of lodash')}
28
+ ${colorCommand('pkg-stats')} lodash ${colorOption('--major -t 10')}
16
29
  `;
30
+ function wrapOption(text, maxLength, indent) {
31
+ const words = text.split(' ');
32
+ let result = '';
33
+ let currentLine = words[0];
34
+ for (let i = 1; i < words.length; i++) {
35
+ const nextCurrentLine = currentLine + ' ' + words[i];
36
+ if (nextCurrentLine.length <= maxLength) {
37
+ currentLine = nextCurrentLine;
38
+ }
39
+ else {
40
+ result += `\n${currentLine}`;
41
+ currentLine = words[i];
42
+ }
43
+ }
44
+ if (currentLine) {
45
+ result += `\n${currentLine}`;
46
+ }
47
+ return redent(result.trim(), indent).trim();
48
+ }
17
49
  export function showHelp() {
18
50
  console.log(redent(HELP, 2));
19
51
  }
@@ -21,7 +53,7 @@ export function parseCliOptions(argv) {
21
53
  const cli = meow(HELP, {
22
54
  argv: argv.slice(2),
23
55
  autoHelp: true,
24
- description: 'Show NPM weekly downloads stats for a package',
56
+ description: 'Show NPM weekly downloads stats:',
25
57
  importMeta: import.meta,
26
58
  flags: {
27
59
  help: {
@@ -42,6 +74,14 @@ export function parseCliOptions(argv) {
42
74
  shortFlag: 't',
43
75
  type: 'number',
44
76
  },
77
+ all: {
78
+ type: 'boolean',
79
+ shortFlag: 'a',
80
+ },
81
+ extended: {
82
+ type: 'boolean',
83
+ shortFlag: 'x',
84
+ },
45
85
  color: {
46
86
  shortFlag: 'c',
47
87
  type: 'string',
@@ -52,11 +92,11 @@ export function parseCliOptions(argv) {
52
92
  if (cli.flags.help) {
53
93
  cli.showHelp();
54
94
  }
55
- if (!cli.input[0]) {
56
- throw new Error('<package-name> is required');
95
+ if (!cli.input.length) {
96
+ throw new Error('At least one <package-name> is required');
57
97
  }
58
98
  return {
59
- packageName: cli.input[0],
99
+ packageNames: cli.input,
60
100
  group: cli.flags.major
61
101
  ? 'major'
62
102
  : cli.flags.minor
@@ -65,6 +105,14 @@ export function parseCliOptions(argv) {
65
105
  ? 'patch'
66
106
  : undefined,
67
107
  top: cli.flags.top,
68
- color: cli.flags.color,
108
+ all: cli.flags.all ?? false,
109
+ extended: cli.flags.extended ?? false,
110
+ color: coalesceColor(cli.flags.color) ?? getColorOfDay(),
69
111
  };
70
112
  }
113
+ function coalesceColor(color) {
114
+ if (color && COLOR_SCHEMES.includes(color)) {
115
+ return color;
116
+ }
117
+ return undefined;
118
+ }
package/dist/colors.js CHANGED
@@ -27,17 +27,20 @@ const gradients = {
27
27
  summer: { colors: ['#fdbb2d', '#22c1c3'], options: {} },
28
28
  rainbow: {
29
29
  colors: ['#ff0100', '#ff0000'],
30
- options: { interpolation: 'hsv', hsvSpin: 'long', padEnd: 0.1 },
30
+ options: { interpolation: 'hsv', hsvSpin: 'long', min: 7, extra: 1 },
31
31
  },
32
32
  pastel: {
33
33
  colors: ['#74ebd5', '#74ecd5'],
34
- options: { interpolation: 'hsv', hsvSpin: 'long', padEnd: 0.1 },
34
+ options: { interpolation: 'hsv', hsvSpin: 'long', extra: 1 },
35
35
  },
36
36
  };
37
37
  export const COLOR_SCHEMES = Object.keys(gradients);
38
+ export function getPrimaryColor(colorScheme) {
39
+ return gradients[colorScheme].colors[0];
40
+ }
38
41
  export function getColors(count, colorScheme) {
39
- const { colors, options } = gradients[colorScheme ?? getColorOfDay()];
40
- const paddedCount = count + (options.padEnd ? Math.ceil(count * options.padEnd) : 0);
42
+ const { colors, options } = gradients[colorScheme];
43
+ const paddedCount = Math.max(count + (options.extra ?? 0), options.min ?? 0);
41
44
  if (paddedCount < colors.length) {
42
45
  return colors;
43
46
  }
@@ -47,7 +50,7 @@ export function getColors(count, colorScheme) {
47
50
  : gradient.rgb(paddedCount);
48
51
  return tinyColors.map((c) => c.toHexString());
49
52
  }
50
- function getColorOfDay() {
53
+ export function getColorOfDay() {
51
54
  const date = new Date();
52
55
  const index = date.getDate() + date.getMonth() * 30 + date.getFullYear() * 360;
53
56
  return COLOR_SCHEMES[index % COLOR_SCHEMES.length];
package/dist/format.js CHANGED
@@ -7,3 +7,6 @@ export function formatDownloads(downloads, maxDownloads) {
7
7
  }
8
8
  return downloads.toString();
9
9
  }
10
+ export function formatPercentage(percentage) {
11
+ return `${(percentage * 100).toFixed(1)}%`;
12
+ }
@@ -0,0 +1,42 @@
1
+ import chalk from 'chalk';
2
+ import { formatPercentage } from '../format.js';
3
+ import { fetchNpmLastWeekDownloads } from '../npm-api.js';
4
+ import { printChart } from '../output.js';
5
+ export async function comparePackages(packageNames, options) {
6
+ const rawPackages = await Promise.all(packageNames.map((packageName) => fetchPackageData(packageName)));
7
+ const packagesToDisplay = rawPackages
8
+ .filter((pkg) => pkg !== undefined)
9
+ .sort((a, b) => b.downloads - a.downloads);
10
+ if (packagesToDisplay.length === 0) {
11
+ console.error(chalk.red('\nNo packages found.\n'));
12
+ process.exit(1);
13
+ }
14
+ console.log(chalk.bold(`\nNPM weekly downloads\n`));
15
+ const maxDownloads = Math.max(...packagesToDisplay.map((item) => item.downloads));
16
+ const items = packagesToDisplay.map((item) => ({
17
+ label: item.packageName,
18
+ value: item.downloads,
19
+ extended: options.extended ? formatPercentage(item.downloads / maxDownloads) : undefined,
20
+ }));
21
+ printChart(items, {
22
+ colorScheme: options.color,
23
+ indent: 2,
24
+ });
25
+ }
26
+ async function fetchPackageData(packageName) {
27
+ try {
28
+ const data = await fetchNpmLastWeekDownloads(packageName);
29
+ if (!Object.keys(data.downloads).length) {
30
+ console.warn(chalk.yellow(`No data found for package "${packageName}".`));
31
+ return undefined;
32
+ }
33
+ return {
34
+ packageName,
35
+ downloads: Object.values(data.downloads).reduce((sum, downloads) => sum + downloads, 0),
36
+ };
37
+ }
38
+ catch (error) {
39
+ console.warn(chalk.yellow(`Failed to fetch data for package "${packageName}"`, error));
40
+ return undefined;
41
+ }
42
+ }
@@ -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,64 @@
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 fetchPackageInfo(packageName, options) {
9
+ const data = await fetchNpmLastWeekDownloads(packageName);
10
+ if (!Object.keys(data.downloads).length) {
11
+ throw new Error(`No data found for package "${packageName}".`);
12
+ }
13
+ const npmStats = Object.keys(data.downloads)
14
+ .map((versionString) => {
15
+ const version = parseVersion(versionString);
16
+ return {
17
+ ...version,
18
+ downloads: data.downloads[versionString],
19
+ };
20
+ })
21
+ .sort(versionCompare);
22
+ const { type, stats } = groupStats(npmStats, options.group);
23
+ const totalDownloads = Object.values(stats).reduce((sum, version) => sum + version.downloads, 0);
24
+ return {
25
+ name: packageName,
26
+ totalDownloads,
27
+ groupingType: type,
28
+ stats: stats.map((stat) => ({
29
+ version: stat.versionString,
30
+ downloads: stat.downloads,
31
+ })),
32
+ };
33
+ }
34
+ export function printPackageInfo({ name, stats, totalDownloads, groupingType }, options) {
35
+ const statsToDisplay = filterStats(stats, {
36
+ totalDownloads,
37
+ all: options.all,
38
+ top: options.top,
39
+ });
40
+ const colors = getColors(statsToDisplay.length, options.color);
41
+ const primaryColor = chalk.hex(colors[0]);
42
+ console.log(chalk.bold(`\nNPM weekly downloads for ${primaryColor(name)}\n`));
43
+ console.log(`Total: ${primaryColor(totalDownloads.toLocaleString())} last week\n`);
44
+ console.log(options.top
45
+ ? `Top ${options.top} ${groupingType} versions:\n`
46
+ : `By ${groupingType} version:\n`);
47
+ const maxDownloads = Math.max(...stats.map((v) => v.downloads));
48
+ const displayData = statsToDisplay.map((item) => {
49
+ const versionParts = item.version.split('.');
50
+ return {
51
+ version: versionParts.length < 3 ? `${item.version}.x` : item.version,
52
+ chart: renderChart(item.downloads / maxDownloads),
53
+ downloads: formatDownloads(item.downloads, maxDownloads),
54
+ };
55
+ });
56
+ const maxVersionLength = Math.max(...displayData.map((item) => item.version.length));
57
+ const maxDownloadsLength = Math.max(...displayData.map((item) => item.downloads.length));
58
+ displayData.forEach((item, i) => {
59
+ const color = chalk.hex(colors[i]);
60
+ console.log(`${item.version.padStart(2 + maxVersionLength)} ${color(item.chart)} ${color(item.downloads.padStart(maxDownloadsLength))}`);
61
+ });
62
+ console.log(chalk.bold(`\nNPM weekly downloads for ${chalk.green(name)}\n`));
63
+ console.log(`Total: ${chalk.green(totalDownloads.toLocaleString())} last week\n`);
64
+ }
@@ -0,0 +1,57 @@
1
+ import chalk from 'chalk';
2
+ import { getPrimaryColor } from '../colors.js';
3
+ import { formatPercentage } from '../format.js';
4
+ import { fetchNpmLastWeekDownloads } from '../npm-api.js';
5
+ import { printChart } from '../output.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 downloadToDisplay = statsToDisplay.reduce((sum, version) => sum + version.downloads, 0);
38
+ if (totalDownloads - downloadToDisplay > 0) {
39
+ statsToDisplay.push({
40
+ versionString: 'rest',
41
+ downloads: totalDownloads - downloadToDisplay,
42
+ });
43
+ }
44
+ const primaryColor = chalk.hex(getPrimaryColor(options.color));
45
+ console.log(chalk.bold(`\nNPM weekly downloads for ${primaryColor(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 items = statsToDisplay.map((item) => ({
49
+ label: item.versionString,
50
+ value: item.downloads,
51
+ extended: options.extended ? formatPercentage(item.downloads / totalDownloads) : undefined,
52
+ }));
53
+ printChart(items, {
54
+ colorScheme: options.color,
55
+ indent: 2,
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/output.js ADDED
@@ -0,0 +1,28 @@
1
+ import chalk from 'chalk';
2
+ import { getColors } from './colors.js';
3
+ import { formatDownloads } from './format.js';
4
+ export function printChart(items, options) {
5
+ const maxLabelLength = Math.max(...items.map((item) => item.label.length));
6
+ const maxValue = Math.max(...items.map((item) => item.value));
7
+ const maxValueLength = formatDownloads(maxValue, maxValue).length;
8
+ const maxExtendedLength = Math.max(...items.map((item) => item.extended?.length ?? 0));
9
+ const colors = getColors(items.length, options.colorScheme);
10
+ const indent = options.indent ?? 0;
11
+ items.forEach((item, i) => {
12
+ const color = chalk.hex(colors[i]);
13
+ const label = ' '.repeat(indent) + item.label.padStart(maxLabelLength);
14
+ const bar = formatBar(item.value / maxValue);
15
+ const value = formatDownloads(item.value, maxValue).padStart(maxValueLength);
16
+ const extended = item.extended
17
+ ? chalk.dim(` ${item.extended}`.padStart(maxExtendedLength + 1))
18
+ : '';
19
+ console.log(`${label} ${color(bar)} ${color(value)}${extended}`);
20
+ });
21
+ }
22
+ export function formatBar(value, { length = 50 } = {}) {
23
+ const filledChars = Math.round(value * length);
24
+ if (filledChars === 0) {
25
+ return '▏' + ' '.repeat(length - 1);
26
+ }
27
+ return '█'.repeat(filledChars) + ' '.repeat(length - filledChars);
28
+ }
package/dist/render.js ADDED
@@ -0,0 +1,23 @@
1
+ import { style } from './style.js';
2
+ export function renderChart(value) {
3
+ const filledValues = Math.round(value * style.chart.width * style.chart.pattern.length);
4
+ if (filledValues === 0) {
5
+ return '▏' + (style.rightLabel === 'align-right' ? ' '.repeat(style.chart.width - 1) : '');
6
+ }
7
+ const fullBlocks = Math.floor(filledValues / style.chart.pattern.length);
8
+ const partialBlock = filledValues % style.chart.pattern.length;
9
+ const hasPartialBlock = partialBlock > 0;
10
+ return (style.chart.pattern.at(-1).repeat(fullBlocks) +
11
+ (hasPartialBlock ? style.chart.pattern.at(partialBlock) : '') +
12
+ (style.rightLabel === 'align-right'
13
+ ? ' '.repeat(style.chart.width - fullBlocks - (hasPartialBlock ? 1 : 0))
14
+ : ''));
15
+ }
16
+ // export type RenderChartLineOptions = {
17
+ // value: number;
18
+ // leftLabel: string;
19
+ // rightLabel: string;
20
+ // };
21
+ // export function renderChartLine({ value, leftLabel, rightLabel }: RenderChartLineOptions) {
22
+ // return `${leftLabel}${renderChart(value)}${rightLabel}`;
23
+ // }
package/dist/stats.js CHANGED
@@ -22,7 +22,7 @@ export function groupStats(stats, type) {
22
22
  function groupByMajor(stats) {
23
23
  const result = {};
24
24
  for (const versionStats of stats) {
25
- const key = `${versionStats.major}`;
25
+ const key = `${versionStats.major}.x`;
26
26
  const entry = result[key] ?? {
27
27
  version: { major: versionStats.major },
28
28
  versionString: key,
@@ -36,7 +36,7 @@ function groupByMajor(stats) {
36
36
  function groupByMinor(stats) {
37
37
  const result = {};
38
38
  for (const versionStats of stats) {
39
- const key = `${versionStats.major}.${versionStats.minor}`;
39
+ const key = `${versionStats.major}.${versionStats.minor}.x`;
40
40
  const entry = result[key] ?? {
41
41
  version: { major: versionStats.major, minor: versionStats.minor },
42
42
  versionString: key,
@@ -65,7 +65,22 @@ 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
+ const filtered = stats.filter((stat) => stat.downloads >= downloadThreshold);
77
+ // If we were to skip only a single state, we rather display it than replace it with "rest".
78
+ if (filtered.length + 1 >= stats.length) {
79
+ return stats;
80
+ }
81
+ return filtered;
82
+ }
83
+ function pickTopStats(stats, top) {
69
84
  const sortedStats = stats.sort((a, b) => b.downloads - a.downloads);
70
85
  const topStats = sortedStats.slice(0, top);
71
86
  return topStats.sort((a, b) => versionCompare(a.version, b.version));
package/dist/style.js ADDED
@@ -0,0 +1,16 @@
1
+ export const style = {
2
+ chart: {
3
+ width: 50,
4
+ //pattern: ['⏹'],
5
+ //pattern: ['⏺'],
6
+ //pattern: ['█'],
7
+ //pattern: [' ', '▎', '▌', '▊', '█'],
8
+ //pattern: [' ', '▩'],
9
+ //pattern: [' ', '▰'],
10
+ //pattern: [' ', '∎'],
11
+ pattern: [' ', '█'],
12
+ //pattern: [' ', '▓'],
13
+ zeroPattern: '▏',
14
+ },
15
+ rightLabel: 'align-right',
16
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/version.js CHANGED
@@ -18,8 +18,5 @@ export function versionCompare(a, b) {
18
18
  if (a.patch !== undefined && b.patch !== undefined && a.patch !== b.patch) {
19
19
  return b.patch - a.patch;
20
20
  }
21
- if (a.preRelease !== undefined && b.preRelease !== undefined) {
22
- return a.preRelease.localeCompare(b.preRelease);
23
- }
24
21
  return 0;
25
22
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pkg-stats",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "Beautiful NPM package download stats",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,7 +0,0 @@
1
- import { expect, test } from 'vitest';
2
- import { renderChart } from '../chart.js';
3
- test('renderChart basic tests', () => {
4
- expect(renderChart(0.0, { length: 10 })).toMatchInlineSnapshot(`" "`);
5
- expect(renderChart(0.5, { length: 10 })).toMatchInlineSnapshot(`"█████ "`);
6
- expect(renderChart(1.0, { length: 10 })).toMatchInlineSnapshot(`"██████████"`);
7
- });