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 +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
|
+
}
|