pkg-stats 0.2.0 → 0.3.1
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/bin.js +1 -1
- package/dist/__tests__/chart.test.js +7 -0
- package/dist/bin.js +26 -69
- package/dist/cli-options.js +70 -0
- package/dist/colors.js +19 -9
- package/dist/format.js +9 -0
- package/dist/npm-api.js +14 -0
- package/dist/stats.js +9 -9
- package/package.json +28 -9
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,29 +1,34 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import minimist from 'minimist';
|
|
3
2
|
import { renderChart } from './chart.js';
|
|
3
|
+
import { parseCliOptions, showHelp } from './cli-options.js';
|
|
4
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';
|
|
5
8
|
import { parseVersion, versionCompare } from './version.js';
|
|
6
|
-
import { groupByType, pickTopStats } from './stats.js';
|
|
7
9
|
export async function pkgStats(argv) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
let options;
|
|
11
|
+
try {
|
|
12
|
+
options = parseCliOptions(argv);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
showHelp();
|
|
16
|
+
console.error(chalk.red(`Error parsing CLI options: ${error instanceof Error ? error.message : error}`));
|
|
17
|
+
process.exit(2);
|
|
12
18
|
}
|
|
13
19
|
let data;
|
|
14
20
|
try {
|
|
15
|
-
|
|
16
|
-
data = await response.json();
|
|
21
|
+
data = await fetchNpmLastWeekDownloads(options.packageName);
|
|
17
22
|
}
|
|
18
23
|
catch (error) {
|
|
19
|
-
console.error(`Failed to fetch data for package "${options.
|
|
24
|
+
console.error(`Failed to fetch data for package "${options.packageName}"`, error);
|
|
20
25
|
return;
|
|
21
26
|
}
|
|
22
27
|
if (!Object.keys(data.downloads).length) {
|
|
23
|
-
console.error(`No data found for package "${options.
|
|
28
|
+
console.error(`No data found for package "${options.packageName}".\n`);
|
|
24
29
|
process.exit(1);
|
|
25
30
|
}
|
|
26
|
-
const
|
|
31
|
+
const npmStats = Object.keys(data.downloads)
|
|
27
32
|
.map((versionString) => {
|
|
28
33
|
const version = parseVersion(versionString);
|
|
29
34
|
return {
|
|
@@ -32,70 +37,22 @@ export async function pkgStats(argv) {
|
|
|
32
37
|
};
|
|
33
38
|
})
|
|
34
39
|
.sort(versionCompare);
|
|
35
|
-
|
|
36
|
-
const totalDownloads = Object.values(
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
: groupedStats;
|
|
40
|
-
const colors = getColors(groupedStatsToDisplay.length);
|
|
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);
|
|
41
44
|
const primaryColor = chalk.hex(colors[0]);
|
|
42
|
-
console.log(chalk.bold(`\nNPM weekly downloads for ${primaryColor(options.
|
|
45
|
+
console.log(chalk.bold(`\nNPM weekly downloads for ${primaryColor(options.packageName)}\n`));
|
|
43
46
|
console.log(`Total: ${primaryColor(totalDownloads.toLocaleString())} last week\n`);
|
|
44
|
-
console.log(options.top ? `Top ${options.top} versions:\n` :
|
|
45
|
-
const maxDownloads = Math.max(...
|
|
46
|
-
|
|
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) => {
|
|
47
50
|
const versionParts = item.versionString.split('.');
|
|
48
|
-
const
|
|
51
|
+
const versionString = versionParts.length < 3 ? `${item.versionString}.x` : item.versionString;
|
|
49
52
|
const chart = renderChart(item.downloads / maxDownloads);
|
|
50
53
|
const downloads = formatDownloads(item.downloads, maxDownloads);
|
|
51
54
|
const color = chalk.hex(colors[i]);
|
|
52
|
-
console.log(`${
|
|
55
|
+
console.log(`${versionString.padStart(8)} ${color(chart)} ${color(downloads.padStart(6))}`);
|
|
53
56
|
});
|
|
54
57
|
console.log('');
|
|
55
58
|
}
|
|
56
|
-
function parseCliOptions(argv) {
|
|
57
|
-
const options = minimist(argv, {
|
|
58
|
-
string: ['group', 'top'],
|
|
59
|
-
boolean: ['help'],
|
|
60
|
-
alias: { g: 'group', h: 'help', t: 'top' },
|
|
61
|
-
});
|
|
62
|
-
let group;
|
|
63
|
-
if (options.group === 'major' || options.major) {
|
|
64
|
-
group = 'major';
|
|
65
|
-
}
|
|
66
|
-
else if (options.group === 'minor' || options.minor) {
|
|
67
|
-
group = 'minor';
|
|
68
|
-
}
|
|
69
|
-
else if (options.group === 'patch' || options.patch) {
|
|
70
|
-
group = 'patch';
|
|
71
|
-
}
|
|
72
|
-
const top = options.top ? parseInt(options.top) : undefined;
|
|
73
|
-
if (!options._[0]) {
|
|
74
|
-
console.error('Package name is required');
|
|
75
|
-
process.exit(1);
|
|
76
|
-
}
|
|
77
|
-
return { name: options._[0], group, help: options.help, top };
|
|
78
|
-
}
|
|
79
|
-
function printHelp() {
|
|
80
|
-
console.log(`
|
|
81
|
-
Usage:
|
|
82
|
-
pkg-stats [options] <package-name>
|
|
83
|
-
|
|
84
|
-
Options:
|
|
85
|
-
-h, --help Show help
|
|
86
|
-
--group <group> Group by major, minor, or patch (default: major)
|
|
87
|
-
--major Group by major
|
|
88
|
-
--minor Group by minor
|
|
89
|
-
--patch Group by patch
|
|
90
|
-
--top <number> Show top <number> versions
|
|
91
|
-
`);
|
|
92
|
-
}
|
|
93
|
-
function formatDownloads(downloads, maxDownloads) {
|
|
94
|
-
if (maxDownloads > 1000000) {
|
|
95
|
-
return `${(downloads / 1000000).toFixed(1)}M`;
|
|
96
|
-
}
|
|
97
|
-
if (maxDownloads > 1000) {
|
|
98
|
-
return `${(downloads / 1000).toFixed(1)}K`;
|
|
99
|
-
}
|
|
100
|
-
return downloads.toString();
|
|
101
|
-
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import meow from 'meow';
|
|
2
|
+
import redent from 'redent';
|
|
3
|
+
import { COLOR_SCHEMES } from './colors.js';
|
|
4
|
+
const HELP = `
|
|
5
|
+
pkg-stats <package-name>
|
|
6
|
+
|
|
7
|
+
Show NPM weekly downloads stats for a package
|
|
8
|
+
|
|
9
|
+
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(', ')}
|
|
16
|
+
`;
|
|
17
|
+
export function showHelp() {
|
|
18
|
+
console.log(redent(HELP, 2));
|
|
19
|
+
}
|
|
20
|
+
export function parseCliOptions(argv) {
|
|
21
|
+
const cli = meow(HELP, {
|
|
22
|
+
argv: argv.slice(2),
|
|
23
|
+
autoHelp: true,
|
|
24
|
+
description: 'Show NPM weekly downloads stats for a package',
|
|
25
|
+
importMeta: import.meta,
|
|
26
|
+
flags: {
|
|
27
|
+
help: {
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
shortFlag: 'h',
|
|
30
|
+
},
|
|
31
|
+
major: {
|
|
32
|
+
type: 'boolean',
|
|
33
|
+
shortFlag: 'm',
|
|
34
|
+
},
|
|
35
|
+
minor: {
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
},
|
|
38
|
+
patch: {
|
|
39
|
+
type: 'boolean',
|
|
40
|
+
},
|
|
41
|
+
top: {
|
|
42
|
+
shortFlag: 't',
|
|
43
|
+
type: 'number',
|
|
44
|
+
},
|
|
45
|
+
color: {
|
|
46
|
+
shortFlag: 'c',
|
|
47
|
+
type: 'string',
|
|
48
|
+
choices: COLOR_SCHEMES,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
if (cli.flags.help) {
|
|
53
|
+
cli.showHelp();
|
|
54
|
+
}
|
|
55
|
+
if (!cli.input[0]) {
|
|
56
|
+
throw new Error('<package-name> is required');
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
packageName: cli.input[0],
|
|
60
|
+
group: cli.flags.major
|
|
61
|
+
? 'major'
|
|
62
|
+
: cli.flags.minor
|
|
63
|
+
? 'minor'
|
|
64
|
+
: cli.flags.patch
|
|
65
|
+
? 'patch'
|
|
66
|
+
: undefined,
|
|
67
|
+
top: cli.flags.top,
|
|
68
|
+
color: cli.flags.color,
|
|
69
|
+
};
|
|
70
|
+
}
|
package/dist/colors.js
CHANGED
|
@@ -9,7 +9,7 @@ const gradients = {
|
|
|
9
9
|
vice: { colors: ['#5ee7df', '#b490ca'], options: { interpolation: 'hsv' } },
|
|
10
10
|
passion: { colors: ['#f43b47', '#453a94'], options: {} },
|
|
11
11
|
fruit: { colors: ['#ff4e50', '#f9d423'], options: {} },
|
|
12
|
-
|
|
12
|
+
insta: { colors: ['#833ab4', '#fd1d1d', '#fcb045'], options: {} },
|
|
13
13
|
retro: {
|
|
14
14
|
colors: [
|
|
15
15
|
'#3f51b1',
|
|
@@ -25,20 +25,30 @@ const gradients = {
|
|
|
25
25
|
options: {},
|
|
26
26
|
},
|
|
27
27
|
summer: { colors: ['#fdbb2d', '#22c1c3'], options: {} },
|
|
28
|
-
rainbow: {
|
|
29
|
-
|
|
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
|
+
},
|
|
30
36
|
};
|
|
37
|
+
export const COLOR_SCHEMES = Object.keys(gradients);
|
|
31
38
|
export function getColors(count, colorScheme) {
|
|
32
|
-
const { colors, options } = gradients[colorScheme ??
|
|
33
|
-
|
|
39
|
+
const { colors, options } = gradients[colorScheme ?? getColorOfDay()];
|
|
40
|
+
const paddedCount = count + (options.padEnd ? Math.ceil(count * options.padEnd) : 0);
|
|
41
|
+
if (paddedCount < colors.length) {
|
|
34
42
|
return colors;
|
|
35
43
|
}
|
|
36
44
|
const gradient = tinygradient(colors);
|
|
37
45
|
const tinyColors = options.interpolation === 'hsv'
|
|
38
|
-
? gradient.hsv(
|
|
39
|
-
: gradient.rgb(
|
|
46
|
+
? gradient.hsv(paddedCount, options.hsvSpin ?? false)
|
|
47
|
+
: gradient.rgb(paddedCount);
|
|
40
48
|
return tinyColors.map((c) => c.toHexString());
|
|
41
49
|
}
|
|
42
|
-
function
|
|
43
|
-
|
|
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];
|
|
44
54
|
}
|
package/dist/format.js
ADDED
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
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import { versionCompare } from './version.js';
|
|
2
|
-
export function
|
|
2
|
+
export function groupStats(stats, type) {
|
|
3
3
|
if (type === 'major') {
|
|
4
|
-
return groupByMajor(stats);
|
|
4
|
+
return { type: 'major', stats: groupByMajor(stats) };
|
|
5
5
|
}
|
|
6
6
|
if (type === 'minor') {
|
|
7
|
-
return groupByMinor(stats);
|
|
7
|
+
return { type: 'minor', stats: groupByMinor(stats) };
|
|
8
8
|
}
|
|
9
9
|
if (type === 'patch') {
|
|
10
|
-
return groupByPatch(stats);
|
|
10
|
+
return { type: 'patch', stats: groupByPatch(stats) };
|
|
11
11
|
}
|
|
12
12
|
const groupedByMajor = groupByMajor(stats);
|
|
13
|
-
if (groupedByMajor.length
|
|
14
|
-
return groupedByMajor;
|
|
13
|
+
if (groupedByMajor.length >= 3) {
|
|
14
|
+
return { type: 'major', stats: groupedByMajor };
|
|
15
15
|
}
|
|
16
16
|
const groupedByMinor = groupByMinor(stats);
|
|
17
|
-
if (groupedByMinor.length
|
|
18
|
-
return groupedByMinor;
|
|
17
|
+
if (groupedByMinor.length >= 3) {
|
|
18
|
+
return { type: 'minor', stats: groupedByMinor };
|
|
19
19
|
}
|
|
20
|
-
return groupByPatch(stats);
|
|
20
|
+
return { type: 'patch', stats: groupByPatch(stats) };
|
|
21
21
|
}
|
|
22
22
|
function groupByMajor(stats) {
|
|
23
23
|
const result = {};
|
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pkg-stats",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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,31 @@
|
|
|
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
|
+
"meow": "^13.2.0",
|
|
38
|
+
"redent": "^4.0.0",
|
|
25
39
|
"tinygradient": "^1.1.5"
|
|
26
40
|
},
|
|
27
41
|
"devDependencies": {
|
|
28
|
-
"@
|
|
42
|
+
"@eslint/js": "^9.18.0",
|
|
43
|
+
"@release-it/conventional-changelog": "^10.0.0",
|
|
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
|
+
"release-it": "^18.1.1",
|
|
49
|
+
"typescript": "^5.7.3",
|
|
50
|
+
"typescript-eslint": "^8.19.1",
|
|
51
|
+
"vitest": "^2.1.8"
|
|
31
52
|
},
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
}
|
|
53
|
+
"packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a"
|
|
54
|
+
}
|