pkg-stats 0.3.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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
- });