pkg-stats 0.1.1 → 0.3.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 +14 -1
- package/bin.js +1 -1
- package/dist/__tests__/chart.test.js +7 -0
- package/dist/bin.js +14 -109
- package/dist/cli-options.js +24 -0
- package/dist/colors.js +50 -7
- package/dist/npm-api.js +14 -0
- package/dist/stats.js +72 -0
- package/package.json +28 -8
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
@@ -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 {
|
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
|
-
|
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.
|
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.
|
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
|
-
|
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
|
-
|
49
|
-
|
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
|
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
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
10
|
-
|
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
|
}
|
package/dist/npm-api.js
ADDED
@@ -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.
|
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
|
-
"
|
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
|
-
"
|
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
|
-
"
|
33
|
-
|
34
|
-
}
|
35
|
-
}
|
54
|
+
"packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a"
|
55
|
+
}
|