pkg-stats 0.1.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
package/README.md CHANGED
@@ -1,3 +1,16 @@
1
1
  ## PKG Stats
2
2
 
3
- Beautiful package download stats.
3
+ Beautiful NPM package download stats.
4
+
5
+ ```
6
+ npx pkg-stats react
7
+ ```
8
+
9
+ <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" />
11
+ </div>
12
+
13
+ ### Options:
14
+
15
+ - version grouping: `--group major|minor|patch` (alias `-g`, `--major`, `--minor`, `--patch`)
16
+ - top version: `--top <number>` (alias `-t`)
package/bin.js CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  import { pkgStats } from './dist/index.js';
4
4
 
5
- pkgStats(process.argv.slice(2));
5
+ pkgStats(process.argv);
@@ -0,0 +1,7 @@
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
+ });
package/dist/bin.js CHANGED
@@ -1,25 +1,22 @@
1
1
  import chalk from 'chalk';
2
- import minimist from 'minimist';
3
2
  import { renderChart } from './chart.js';
4
- import { gradients } from './colors.js';
3
+ import { parseCliOptions } from './cli-options.js';
4
+ import { getColors } from './colors.js';
5
+ import { fetchNpmLastWeekDownloads } from './npm-api.js';
6
+ import { groupByType, pickTopStats } from './stats.js';
5
7
  import { parseVersion, versionCompare } from './version.js';
6
8
  export async function pkgStats(argv) {
7
9
  const options = parseCliOptions(argv);
8
- if (options.help) {
9
- printHelp();
10
- return;
11
- }
12
10
  let data;
13
11
  try {
14
- const response = await fetch(`https://api.npmjs.org/versions/${encodeURIComponent(options.name)}/last-week`);
15
- data = await response.json();
12
+ data = await fetchNpmLastWeekDownloads(options.packageName);
16
13
  }
17
14
  catch (error) {
18
- console.error(`Failed to fetch data for package "${options.name}"`);
15
+ console.error(`Failed to fetch data for package "${options.packageName}"`, error);
19
16
  return;
20
17
  }
21
18
  if (!Object.keys(data.downloads).length) {
22
- console.error(`No data found for package "${options.name}".\n`);
19
+ console.error(`No data found for package "${options.packageName}".\n`);
23
20
  process.exit(1);
24
21
  }
25
22
  const rawStats = Object.keys(data.downloads)
@@ -31,27 +28,20 @@ export async function pkgStats(argv) {
31
28
  };
32
29
  })
33
30
  .sort(versionCompare);
34
- let groupedStats;
35
- if (options.group === 'patch') {
36
- groupedStats = sumByPatch(rawStats);
37
- }
38
- else if (options.group === 'minor') {
39
- groupedStats = sumByMinor(rawStats);
40
- }
41
- else {
42
- groupedStats = sumByMajor(rawStats);
43
- }
31
+ const groupedStats = groupByType(options.group, rawStats);
44
32
  const totalDownloads = Object.values(groupedStats).reduce((sum, version) => sum + version.downloads, 0);
45
33
  const groupedStatsToDisplay = options.top
46
34
  ? pickTopStats(groupedStats, options.top)
47
35
  : groupedStats;
48
- console.log(chalk.bold(`\nNPM weekly downloads for ${chalk.cyan(options.name)}\n`));
49
- console.log(`Total: ${chalk.cyan(totalDownloads.toLocaleString())}\n`);
36
+ const colors = getColors(groupedStatsToDisplay.length, options.color);
37
+ const primaryColor = chalk.hex(colors[0]);
38
+ console.log(chalk.bold(`\nNPM weekly downloads for ${primaryColor(options.packageName)}\n`));
39
+ console.log(`Total: ${primaryColor(totalDownloads.toLocaleString())} last week\n`);
50
40
  console.log(options.top ? `Top ${options.top} versions:\n` : 'By version:\n');
51
- const colors = gradients.passion(groupedStatsToDisplay.length);
52
41
  const maxDownloads = Math.max(...groupedStats.map((v) => v.downloads));
53
42
  groupedStatsToDisplay.forEach((item, i) => {
54
- const version = options.group != 'patch' ? `${item.versionString}.x` : item.versionString;
43
+ const versionParts = item.versionString.split('.');
44
+ const version = versionParts.length < 3 ? `${item.versionString}.x` : item.versionString;
55
45
  const chart = renderChart(item.downloads / maxDownloads);
56
46
  const downloads = formatDownloads(item.downloads, maxDownloads);
57
47
  const color = chalk.hex(colors[i]);
@@ -59,86 +49,6 @@ export async function pkgStats(argv) {
59
49
  });
60
50
  console.log('');
61
51
  }
62
- function parseCliOptions(argv) {
63
- const options = minimist(argv, {
64
- string: ['group', 'top'],
65
- boolean: ['help'],
66
- alias: { g: 'group', h: 'help', t: 'top' },
67
- });
68
- let group = 'major';
69
- if (options.group === 'minor' || options.minor) {
70
- group = 'minor';
71
- }
72
- else if (options.group === 'patch' || options.patch) {
73
- group = 'patch';
74
- }
75
- const top = options.top ? parseInt(options.top) : undefined;
76
- if (!options._[0]) {
77
- console.error('Package name is required');
78
- process.exit(1);
79
- }
80
- return { name: options._[0], group, help: options.help, top };
81
- }
82
- function printHelp() {
83
- console.log(`
84
- Usage:
85
- pkg-stats [options] <package-name>
86
-
87
- Options:
88
- -h, --help Show help
89
- --group <group> Group by major, minor, or patch (default: major)
90
- --major Group by major
91
- --minor Group by minor
92
- --patch Group by patch
93
- --top <number> Show top <number> versions
94
- `);
95
- }
96
- function sumByMajor(stats) {
97
- const result = {};
98
- for (const versionStats of stats) {
99
- const key = `${versionStats.major}`;
100
- const entry = result[key] ?? {
101
- version: { major: versionStats.major },
102
- versionString: key,
103
- downloads: 0,
104
- };
105
- result[key] = entry;
106
- entry.downloads += versionStats.downloads;
107
- }
108
- return Object.values(result).sort((a, b) => versionCompare(a.version, b.version));
109
- }
110
- function sumByMinor(stats) {
111
- const result = {};
112
- for (const versionStats of stats) {
113
- const key = `${versionStats.major}.${versionStats.minor}`;
114
- const entry = result[key] ?? {
115
- version: { major: versionStats.major, minor: versionStats.minor },
116
- versionString: key,
117
- downloads: 0,
118
- };
119
- result[key] = entry;
120
- entry.downloads += versionStats.downloads;
121
- }
122
- return Object.values(result).sort((a, b) => versionCompare(a.version, b.version));
123
- }
124
- function sumByPatch(stats) {
125
- const result = {};
126
- for (const versionStats of stats) {
127
- const key = `${versionStats.major}.${versionStats.minor}.${versionStats.patch}`;
128
- const entry = result[key] ?? {
129
- version: {
130
- major: versionStats.major,
131
- minor: versionStats.minor,
132
- patch: versionStats.patch,
133
- },
134
- versionString: key,
135
- downloads: 0,
136
- };
137
- result[key] = entry;
138
- entry.downloads += versionStats.downloads;
139
- }
140
- return Object.values(result).sort((a, b) => versionCompare(a.version, b.version));
141
- }
142
52
  function formatDownloads(downloads, maxDownloads) {
143
53
  if (maxDownloads > 1000000) {
144
54
  return `${(downloads / 1000000).toFixed(1)}M`;
@@ -148,8 +58,3 @@ function formatDownloads(downloads, maxDownloads) {
148
58
  }
149
59
  return downloads.toString();
150
60
  }
151
- function pickTopStats(stats, top) {
152
- const sortedStats = stats.sort((a, b) => b.downloads - a.downloads);
153
- const topStats = sortedStats.slice(0, top);
154
- return topStats.sort((a, b) => versionCompare(a.version, b.version));
155
- }
@@ -0,0 +1,24 @@
1
+ import { program } from 'commander';
2
+ import { COLOR_SCHEMES } from './colors.js';
3
+ export function parseCliOptions(argv) {
4
+ program
5
+ .name('pkg-stats')
6
+ .description('Show NPM weekly downloads stats for a package')
7
+ .argument('<package-name>', 'Package name')
8
+ .option('--major', 'Group by major version')
9
+ .option('--minor', 'Group by minor version')
10
+ .option('--patch', 'Group by patch version')
11
+ .option('-t, --top <number>', 'Show top <number> versions')
12
+ .option('-c, --color <color>', 'Color scheme: ' + COLOR_SCHEMES.join(', '))
13
+ .parse(argv);
14
+ const args = program.args;
15
+ const options = program.opts();
16
+ return {
17
+ packageName: args[0],
18
+ group: options.major ? 'major' : options.minor ? 'minor' : options.patch ? 'patch' : undefined,
19
+ top: options.top !== undefined ? parseInt(options.top) : undefined,
20
+ color: options.color && COLOR_SCHEMES.includes(options.color)
21
+ ? options.color
22
+ : undefined,
23
+ };
24
+ }
package/dist/colors.js CHANGED
@@ -1,11 +1,54 @@
1
1
  import tinygradient from 'tinygradient';
2
2
  // See: https://github.com/bokub/gradient-string/blob/465e86c8499a7f427c45afb1861f1444a2db74b9/src/index.ts#L166
3
- export const gradients = {
4
- mind: (count) => toColors(tinygradient(['#473b7b', '#3584a7', '#30d2be']).rgb(count)),
5
- pastel: (count) => toColors(tinygradient(['#74ebd5', '#74ecd5']).hsv(count, 'long')),
6
- passion: (count) => toColors(tinygradient(['#f43b47', '#453a94']).rgb(count)),
7
- retro: (count) => toColors(tinygradient(['#4150AB', '#AE6F97', '#EFCB84']).rgb(count)),
3
+ const gradients = {
4
+ atlas: { colors: ['#feac5e', '#c779d0', '#4bc0c8'], options: {} },
5
+ cristal: { colors: ['#bdfff3', '#4ac29a'], options: {} },
6
+ teen: { colors: ['#77a1d3', '#79cbca', '#e684ae'], options: {} },
7
+ mind: { colors: ['#473b7b', '#3584a7', '#30d2be'], options: {} },
8
+ morning: { colors: ['#ff5f6d', '#ffc371'], options: { interpolation: 'hsv' } },
9
+ vice: { colors: ['#5ee7df', '#b490ca'], options: { interpolation: 'hsv' } },
10
+ passion: { colors: ['#f43b47', '#453a94'], options: {} },
11
+ fruit: { colors: ['#ff4e50', '#f9d423'], options: {} },
12
+ insta: { colors: ['#833ab4', '#fd1d1d', '#fcb045'], options: {} },
13
+ retro: {
14
+ colors: [
15
+ '#3f51b1',
16
+ '#5a55ae',
17
+ '#7b5fac',
18
+ '#8f6aae',
19
+ '#a86aa4',
20
+ '#cc6b8e',
21
+ '#f18271',
22
+ '#f3a469',
23
+ '#f7c978',
24
+ ],
25
+ options: {},
26
+ },
27
+ summer: { colors: ['#fdbb2d', '#22c1c3'], options: {} },
28
+ rainbow: {
29
+ colors: ['#ff0100', '#ff0000'],
30
+ options: { interpolation: 'hsv', hsvSpin: 'long', padEnd: 0.1 },
31
+ },
32
+ pastel: {
33
+ colors: ['#74ebd5', '#74ecd5'],
34
+ options: { interpolation: 'hsv', hsvSpin: 'long', padEnd: 0.1 },
35
+ },
8
36
  };
9
- function toColors(colors) {
10
- return colors.map((c) => c.toHexString());
37
+ export const COLOR_SCHEMES = Object.keys(gradients);
38
+ export function getColors(count, colorScheme) {
39
+ const { colors, options } = gradients[colorScheme ?? getColorOfDay()];
40
+ const paddedCount = count + (options.padEnd ? Math.ceil(count * options.padEnd) : 0);
41
+ if (paddedCount < colors.length) {
42
+ return colors;
43
+ }
44
+ const gradient = tinygradient(colors);
45
+ const tinyColors = options.interpolation === 'hsv'
46
+ ? gradient.hsv(paddedCount, options.hsvSpin ?? false)
47
+ : gradient.rgb(paddedCount);
48
+ return tinyColors.map((c) => c.toHexString());
49
+ }
50
+ function getColorOfDay() {
51
+ const date = new Date();
52
+ const index = date.getDate() + date.getMonth() * 30 + date.getFullYear() * 360;
53
+ return COLOR_SCHEMES[index % COLOR_SCHEMES.length];
11
54
  }
@@ -0,0 +1,14 @@
1
+ export async function fetchNpmLastWeekDownloads(packageName) {
2
+ const response = await fetch(`https://api.npmjs.org/versions/${encodeURIComponent(packageName)}/last-week`);
3
+ if (!response.ok) {
4
+ throw new Error(`Failed to fetch data for package "${packageName}. Status: ${response.status}`);
5
+ }
6
+ const json = await response.json();
7
+ if (!json.downloads) {
8
+ throw new Error('No downloads found');
9
+ }
10
+ return {
11
+ package: packageName,
12
+ downloads: json.downloads,
13
+ };
14
+ }
package/dist/stats.js ADDED
@@ -0,0 +1,72 @@
1
+ import { versionCompare } from './version.js';
2
+ export function groupByType(type, stats) {
3
+ if (type === 'major') {
4
+ return groupByMajor(stats);
5
+ }
6
+ if (type === 'minor') {
7
+ return groupByMinor(stats);
8
+ }
9
+ if (type === 'patch') {
10
+ return groupByPatch(stats);
11
+ }
12
+ const groupedByMajor = groupByMajor(stats);
13
+ if (groupedByMajor.length > 1) {
14
+ return groupedByMajor;
15
+ }
16
+ const groupedByMinor = groupByMinor(stats);
17
+ if (groupedByMinor.length > 1) {
18
+ return groupedByMinor;
19
+ }
20
+ return groupByPatch(stats);
21
+ }
22
+ function groupByMajor(stats) {
23
+ const result = {};
24
+ for (const versionStats of stats) {
25
+ const key = `${versionStats.major}`;
26
+ const entry = result[key] ?? {
27
+ version: { major: versionStats.major },
28
+ versionString: key,
29
+ downloads: 0,
30
+ };
31
+ result[key] = entry;
32
+ entry.downloads += versionStats.downloads;
33
+ }
34
+ return Object.values(result).sort((a, b) => versionCompare(a.version, b.version));
35
+ }
36
+ function groupByMinor(stats) {
37
+ const result = {};
38
+ for (const versionStats of stats) {
39
+ const key = `${versionStats.major}.${versionStats.minor}`;
40
+ const entry = result[key] ?? {
41
+ version: { major: versionStats.major, minor: versionStats.minor },
42
+ versionString: key,
43
+ downloads: 0,
44
+ };
45
+ result[key] = entry;
46
+ entry.downloads += versionStats.downloads;
47
+ }
48
+ return Object.values(result).sort((a, b) => versionCompare(a.version, b.version));
49
+ }
50
+ function groupByPatch(stats) {
51
+ const result = {};
52
+ for (const versionStats of stats) {
53
+ const key = `${versionStats.major}.${versionStats.minor}.${versionStats.patch}`;
54
+ const entry = result[key] ?? {
55
+ version: {
56
+ major: versionStats.major,
57
+ minor: versionStats.minor,
58
+ patch: versionStats.patch,
59
+ },
60
+ versionString: key,
61
+ downloads: 0,
62
+ };
63
+ result[key] = entry;
64
+ entry.downloads += versionStats.downloads;
65
+ }
66
+ return Object.values(result).sort((a, b) => versionCompare(a.version, b.version));
67
+ }
68
+ export function pickTopStats(stats, top) {
69
+ const sortedStats = stats.sort((a, b) => b.downloads - a.downloads);
70
+ const topStats = sortedStats.slice(0, top);
71
+ return topStats.sort((a, b) => versionCompare(a.version, b.version));
72
+ }
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "pkg-stats",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Beautiful NPM package download stats",
5
- "author": "Maciej Jastrzębski <mdjastrzebski@gmail.com> (https://github.com/mdjastrzebski)",
6
5
  "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/mdjastrzebski/pkg-stats"
9
+ },
10
+ "homepage": "https://github.com/mdjastrzebski/pkg-stats",
11
+ "author": "Maciej Jastrzębski <mdjastrzebski@gmail.com> (https://github.com/mdjastrzebski)",
7
12
  "keywords": [
8
13
  "npm",
9
14
  "package",
@@ -19,17 +24,32 @@
19
24
  "bin": {
20
25
  "pkg-stats": "./bin.js"
21
26
  },
27
+ "scripts": {
28
+ "build": "tsc",
29
+ "test": "vitest",
30
+ "typecheck": "tsc --noEmit",
31
+ "lint": "eslint",
32
+ "release": "release-it",
33
+ "validate": "pnpm lint && pnpm typecheck && pnpm test -- --no-watch"
34
+ },
22
35
  "dependencies": {
23
36
  "chalk": "^5.4.1",
24
- "minimist": "^1.2.8",
37
+ "commander": "^13.0.0",
25
38
  "tinygradient": "^1.1.5"
26
39
  },
27
40
  "devDependencies": {
41
+ "@eslint/js": "^9.18.0",
42
+ "@release-it/conventional-changelog": "^10.0.0",
28
43
  "@types/minimist": "^1.2.5",
29
44
  "@types/node": "^22.10.5",
30
- "typescript": "^5.7.3"
45
+ "eslint": "^9.18.0",
46
+ "eslint-plugin-simple-import-sort": "^12.1.1",
47
+ "globals": "^15.14.0",
48
+ "redent": "^4.0.0",
49
+ "release-it": "^18.1.1",
50
+ "typescript": "^5.7.3",
51
+ "typescript-eslint": "^8.19.1",
52
+ "vitest": "^2.1.8"
31
53
  },
32
- "scripts": {
33
- "build": "tsc"
34
- }
35
- }
54
+ "packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a"
55
+ }