pkg-stats 0.4.0 → 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
+ });
@@ -1,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import meow from 'meow';
3
3
  import redent from 'redent';
4
- import { COLOR_SCHEMES } from './colors.js';
4
+ import { COLOR_SCHEMES, getColorOfDay } from './colors.js';
5
5
  const colorCommand = chalk.hex('#22c1c3');
6
6
  const colorOption = chalk.hex('#fdbb2d');
7
7
  const HELP = `
@@ -14,6 +14,7 @@ Options:
14
14
  ${colorOption('--patch')} Group by patch version
15
15
  ${colorOption('-t, --top')} <number> Show top <number> most downloaded versions
16
16
  ${colorOption('-a, --all')} Include all versions in output, even those with minimal downloads
17
+ ${colorOption('-x, --extended')} Show extended stats
17
18
  ${colorOption('-c, --color')} <scheme> ${wrapOption(`Choose color scheme from: ${COLOR_SCHEMES.sort().join(', ')}`, 50, 24)}
18
19
 
19
20
  Examples:
@@ -77,6 +78,10 @@ export function parseCliOptions(argv) {
77
78
  type: 'boolean',
78
79
  shortFlag: 'a',
79
80
  },
81
+ extended: {
82
+ type: 'boolean',
83
+ shortFlag: 'x',
84
+ },
80
85
  color: {
81
86
  shortFlag: 'c',
82
87
  type: 'string',
@@ -100,7 +105,14 @@ export function parseCliOptions(argv) {
100
105
  ? 'patch'
101
106
  : undefined,
102
107
  top: cli.flags.top,
103
- all: cli.flags.all,
104
- color: cli.flags.color,
108
+ all: cli.flags.all ?? false,
109
+ extended: cli.flags.extended ?? false,
110
+ color: coalesceColor(cli.flags.color) ?? getColorOfDay(),
105
111
  };
106
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
+ }
@@ -1,8 +1,7 @@
1
1
  import chalk from 'chalk';
2
- import { renderChart } from '../chart.js';
3
- import { getColors } from '../colors.js';
4
- import { formatDownloads } from '../format.js';
2
+ import { formatPercentage } from '../format.js';
5
3
  import { fetchNpmLastWeekDownloads } from '../npm-api.js';
4
+ import { printChart } from '../output.js';
6
5
  export async function comparePackages(packageNames, options) {
7
6
  const rawPackages = await Promise.all(packageNames.map((packageName) => fetchPackageData(packageName)));
8
7
  const packagesToDisplay = rawPackages
@@ -13,20 +12,15 @@ export async function comparePackages(packageNames, options) {
13
12
  process.exit(1);
14
13
  }
15
14
  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))}`);
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,
30
24
  });
31
25
  }
32
26
  async function fetchPackageData(packageName) {
@@ -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
+ }
@@ -1,8 +1,8 @@
1
1
  import chalk from 'chalk';
2
- import { renderChart } from '../chart.js';
3
- import { getColors } from '../colors.js';
4
- import { formatDownloads } from '../format.js';
2
+ import { getPrimaryColor } from '../colors.js';
3
+ import { formatPercentage } from '../format.js';
5
4
  import { fetchNpmLastWeekDownloads } from '../npm-api.js';
5
+ import { printChart } from '../output.js';
6
6
  import { filterStats, groupStats } from '../stats.js';
7
7
  import { parseVersion, versionCompare } from '../version.js';
8
8
  export async function printPackageStats(packageName, options) {
@@ -34,24 +34,24 @@ export async function printPackageStats(packageName, options) {
34
34
  all: options.all,
35
35
  top: options.top,
36
36
  });
37
- const colors = getColors(statsToDisplay.length, options.color);
38
- const primaryColor = chalk.hex(colors[0]);
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));
39
45
  console.log(chalk.bold(`\nNPM weekly downloads for ${primaryColor(packageName)}\n`));
40
46
  console.log(`Total: ${primaryColor(totalDownloads.toLocaleString())} last week\n`);
41
47
  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))}`);
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
56
  });
57
57
  }
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,
@@ -73,7 +73,12 @@ export function filterStats(stats, options) {
73
73
  return pickTopStats(stats, options.top);
74
74
  }
75
75
  const downloadThreshold = 0.005 * options.totalDownloads; // 0.5%
76
- return stats.filter((stat) => stat.downloads >= downloadThreshold);
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;
77
82
  }
78
83
  function pickTopStats(stats, top) {
79
84
  const sortedStats = stats.sort((a, b) => b.downloads - a.downloads);
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.4.0",
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
- });