pkg-stats 0.3.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +23 -4
- package/dist/__tests__/output.js +8 -0
- package/dist/__tests__/output.test.js +7 -0
- package/dist/bin.js +6 -42
- package/dist/chart.js +4 -2
- package/dist/cli-options.js +63 -15
- package/dist/colors.js +8 -5
- package/dist/format.js +3 -0
- package/dist/mode/compare-packages.js +42 -0
- package/dist/mode/package-details.js +57 -0
- package/dist/mode/package-info.js +64 -0
- package/dist/mode/package-stats.js +57 -0
- package/dist/mode/single-package.js +58 -0
- package/dist/mode/single-package.ts +0 -0
- package/dist/output.js +28 -0
- package/dist/render.js +23 -0
- package/dist/stats.js +18 -3
- package/dist/style.js +16 -0
- package/dist/types.js +1 -0
- package/dist/version.js +0 -3
- package/package.json +1 -1
- package/dist/__tests__/chart.test.js +0 -7
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-
|
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
|
-
|
33
|
+
#### Options:
|
14
34
|
|
15
|
-
-
|
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
|
+
});
|
package/dist/bin.js
CHANGED
@@ -1,11 +1,7 @@
|
|
1
1
|
import chalk from 'chalk';
|
2
|
-
import { renderChart } from './chart.js';
|
3
2
|
import { parseCliOptions, showHelp } from './cli-options.js';
|
4
|
-
import {
|
5
|
-
import {
|
6
|
-
import { fetchNpmLastWeekDownloads } from './npm-api.js';
|
7
|
-
import { groupStats, pickTopStats } from './stats.js';
|
8
|
-
import { parseVersion, versionCompare } from './version.js';
|
3
|
+
import { comparePackages } from './mode/compare-packages.js';
|
4
|
+
import { printPackageStats } from './mode/package-stats.js';
|
9
5
|
export async function pkgStats(argv) {
|
10
6
|
let options;
|
11
7
|
try {
|
@@ -16,43 +12,11 @@ export async function pkgStats(argv) {
|
|
16
12
|
console.error(chalk.red(`Error parsing CLI options: ${error instanceof Error ? error.message : error}`));
|
17
13
|
process.exit(2);
|
18
14
|
}
|
19
|
-
|
20
|
-
|
21
|
-
data = await fetchNpmLastWeekDownloads(options.packageName);
|
22
|
-
}
|
23
|
-
catch (error) {
|
24
|
-
console.error(`Failed to fetch data for package "${options.packageName}"`, error);
|
25
|
-
return;
|
15
|
+
if (options.packageNames.length === 1) {
|
16
|
+
await printPackageStats(options.packageNames[0], options);
|
26
17
|
}
|
27
|
-
|
28
|
-
|
29
|
-
process.exit(1);
|
18
|
+
else {
|
19
|
+
await comparePackages(options.packageNames, options);
|
30
20
|
}
|
31
|
-
const npmStats = Object.keys(data.downloads)
|
32
|
-
.map((versionString) => {
|
33
|
-
const version = parseVersion(versionString);
|
34
|
-
return {
|
35
|
-
...version,
|
36
|
-
downloads: data.downloads[versionString],
|
37
|
-
};
|
38
|
-
})
|
39
|
-
.sort(versionCompare);
|
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);
|
44
|
-
const primaryColor = chalk.hex(colors[0]);
|
45
|
-
console.log(chalk.bold(`\nNPM weekly downloads for ${primaryColor(options.packageName)}\n`));
|
46
|
-
console.log(`Total: ${primaryColor(totalDownloads.toLocaleString())} last week\n`);
|
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) => {
|
50
|
-
const versionParts = item.versionString.split('.');
|
51
|
-
const versionString = versionParts.length < 3 ? `${item.versionString}.x` : item.versionString;
|
52
|
-
const chart = renderChart(item.downloads / maxDownloads);
|
53
|
-
const downloads = formatDownloads(item.downloads, maxDownloads);
|
54
|
-
const color = chalk.hex(colors[i]);
|
55
|
-
console.log(`${versionString.padStart(8)} ${color(chart)} ${color(downloads.padStart(6))}`);
|
56
|
-
});
|
57
21
|
console.log('');
|
58
22
|
}
|
package/dist/chart.js
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
export function renderChart(value, { length = 50 } = {}) {
|
2
2
|
const filledChars = Math.round(value * length);
|
3
|
-
|
4
|
-
|
3
|
+
if (filledChars === 0) {
|
4
|
+
return '▏' + ' '.repeat(length - 1);
|
5
|
+
}
|
6
|
+
return '█'.repeat(filledChars) + ' '.repeat(length - filledChars);
|
5
7
|
}
|
package/dist/cli-options.js
CHANGED
@@ -1,19 +1,51 @@
|
|
1
|
+
import chalk from 'chalk';
|
1
2
|
import meow from 'meow';
|
2
3
|
import redent from 'redent';
|
3
|
-
import { COLOR_SCHEMES } from './colors.js';
|
4
|
+
import { COLOR_SCHEMES, getColorOfDay } from './colors.js';
|
5
|
+
const colorCommand = chalk.hex('#22c1c3');
|
6
|
+
const colorOption = chalk.hex('#fdbb2d');
|
4
7
|
const HELP = `
|
5
|
-
pkg-stats <package-
|
6
|
-
|
7
|
-
Show NPM weekly downloads stats for a package
|
8
|
+
${colorCommand('pkg-stats')} <package> - Show version stats
|
9
|
+
${colorCommand('pkg-stats')} <package-1> <package-2>... - Compare between packages
|
8
10
|
|
9
11
|
Options:
|
10
|
-
|
11
|
-
--
|
12
|
-
--
|
13
|
-
--
|
14
|
-
-
|
15
|
-
|
12
|
+
${colorOption('--major')} Group by major version
|
13
|
+
${colorOption('--minor')} Group by minor version
|
14
|
+
${colorOption('--patch')} Group by patch version
|
15
|
+
${colorOption('-t, --top')} <number> Show top <number> most downloaded versions
|
16
|
+
${colorOption('-a, --all')} Include all versions in output, even those with minimal downloads
|
17
|
+
${colorOption('-x, --extended')} Show extended stats
|
18
|
+
${colorOption('-c, --color')} <scheme> ${wrapOption(`Choose color scheme from: ${COLOR_SCHEMES.sort().join(', ')}`, 50, 24)}
|
19
|
+
|
20
|
+
Examples:
|
21
|
+
${chalk.dim('# Show stats for react')}
|
22
|
+
${colorCommand('pkg-stats')} react
|
23
|
+
|
24
|
+
${chalk.dim('# Compare react, vue, angular and svelte')}
|
25
|
+
${colorCommand('pkg-stats')} react vue @angular/core svelte
|
26
|
+
|
27
|
+
${chalk.dim('# Show top 10 major versions of lodash')}
|
28
|
+
${colorCommand('pkg-stats')} lodash ${colorOption('--major -t 10')}
|
16
29
|
`;
|
30
|
+
function wrapOption(text, maxLength, indent) {
|
31
|
+
const words = text.split(' ');
|
32
|
+
let result = '';
|
33
|
+
let currentLine = words[0];
|
34
|
+
for (let i = 1; i < words.length; i++) {
|
35
|
+
const nextCurrentLine = currentLine + ' ' + words[i];
|
36
|
+
if (nextCurrentLine.length <= maxLength) {
|
37
|
+
currentLine = nextCurrentLine;
|
38
|
+
}
|
39
|
+
else {
|
40
|
+
result += `\n${currentLine}`;
|
41
|
+
currentLine = words[i];
|
42
|
+
}
|
43
|
+
}
|
44
|
+
if (currentLine) {
|
45
|
+
result += `\n${currentLine}`;
|
46
|
+
}
|
47
|
+
return redent(result.trim(), indent).trim();
|
48
|
+
}
|
17
49
|
export function showHelp() {
|
18
50
|
console.log(redent(HELP, 2));
|
19
51
|
}
|
@@ -21,7 +53,7 @@ export function parseCliOptions(argv) {
|
|
21
53
|
const cli = meow(HELP, {
|
22
54
|
argv: argv.slice(2),
|
23
55
|
autoHelp: true,
|
24
|
-
description: 'Show NPM weekly downloads stats
|
56
|
+
description: 'Show NPM weekly downloads stats:',
|
25
57
|
importMeta: import.meta,
|
26
58
|
flags: {
|
27
59
|
help: {
|
@@ -42,6 +74,14 @@ export function parseCliOptions(argv) {
|
|
42
74
|
shortFlag: 't',
|
43
75
|
type: 'number',
|
44
76
|
},
|
77
|
+
all: {
|
78
|
+
type: 'boolean',
|
79
|
+
shortFlag: 'a',
|
80
|
+
},
|
81
|
+
extended: {
|
82
|
+
type: 'boolean',
|
83
|
+
shortFlag: 'x',
|
84
|
+
},
|
45
85
|
color: {
|
46
86
|
shortFlag: 'c',
|
47
87
|
type: 'string',
|
@@ -52,11 +92,11 @@ export function parseCliOptions(argv) {
|
|
52
92
|
if (cli.flags.help) {
|
53
93
|
cli.showHelp();
|
54
94
|
}
|
55
|
-
if (!cli.input
|
56
|
-
throw new Error('<package-name> is required');
|
95
|
+
if (!cli.input.length) {
|
96
|
+
throw new Error('At least one <package-name> is required');
|
57
97
|
}
|
58
98
|
return {
|
59
|
-
|
99
|
+
packageNames: cli.input,
|
60
100
|
group: cli.flags.major
|
61
101
|
? 'major'
|
62
102
|
: cli.flags.minor
|
@@ -65,6 +105,14 @@ export function parseCliOptions(argv) {
|
|
65
105
|
? 'patch'
|
66
106
|
: undefined,
|
67
107
|
top: cli.flags.top,
|
68
|
-
|
108
|
+
all: cli.flags.all ?? false,
|
109
|
+
extended: cli.flags.extended ?? false,
|
110
|
+
color: coalesceColor(cli.flags.color) ?? getColorOfDay(),
|
69
111
|
};
|
70
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',
|
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',
|
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
|
40
|
-
const paddedCount = count + (options.
|
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
@@ -0,0 +1,42 @@
|
|
1
|
+
import chalk from 'chalk';
|
2
|
+
import { formatPercentage } from '../format.js';
|
3
|
+
import { fetchNpmLastWeekDownloads } from '../npm-api.js';
|
4
|
+
import { printChart } from '../output.js';
|
5
|
+
export async function comparePackages(packageNames, options) {
|
6
|
+
const rawPackages = await Promise.all(packageNames.map((packageName) => fetchPackageData(packageName)));
|
7
|
+
const packagesToDisplay = rawPackages
|
8
|
+
.filter((pkg) => pkg !== undefined)
|
9
|
+
.sort((a, b) => b.downloads - a.downloads);
|
10
|
+
if (packagesToDisplay.length === 0) {
|
11
|
+
console.error(chalk.red('\nNo packages found.\n'));
|
12
|
+
process.exit(1);
|
13
|
+
}
|
14
|
+
console.log(chalk.bold(`\nNPM weekly downloads\n`));
|
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,
|
24
|
+
});
|
25
|
+
}
|
26
|
+
async function fetchPackageData(packageName) {
|
27
|
+
try {
|
28
|
+
const data = await fetchNpmLastWeekDownloads(packageName);
|
29
|
+
if (!Object.keys(data.downloads).length) {
|
30
|
+
console.warn(chalk.yellow(`No data found for package "${packageName}".`));
|
31
|
+
return undefined;
|
32
|
+
}
|
33
|
+
return {
|
34
|
+
packageName,
|
35
|
+
downloads: Object.values(data.downloads).reduce((sum, downloads) => sum + downloads, 0),
|
36
|
+
};
|
37
|
+
}
|
38
|
+
catch (error) {
|
39
|
+
console.warn(chalk.yellow(`Failed to fetch data for package "${packageName}"`, error));
|
40
|
+
return undefined;
|
41
|
+
}
|
42
|
+
}
|
@@ -0,0 +1,57 @@
|
|
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 packageDetails(packageName, options) {
|
9
|
+
let data;
|
10
|
+
try {
|
11
|
+
data = await fetchNpmLastWeekDownloads(packageName);
|
12
|
+
}
|
13
|
+
catch (error) {
|
14
|
+
console.error(`Failed to fetch data for package "${packageName}"`, error);
|
15
|
+
return;
|
16
|
+
}
|
17
|
+
if (!Object.keys(data.downloads).length) {
|
18
|
+
console.error(`No data found for package "${packageName}".\n`);
|
19
|
+
process.exit(1);
|
20
|
+
}
|
21
|
+
const npmStats = Object.keys(data.downloads)
|
22
|
+
.map((versionString) => {
|
23
|
+
const version = parseVersion(versionString);
|
24
|
+
return {
|
25
|
+
...version,
|
26
|
+
downloads: data.downloads[versionString],
|
27
|
+
};
|
28
|
+
})
|
29
|
+
.sort(versionCompare);
|
30
|
+
const { type, stats } = groupStats(npmStats, options.group);
|
31
|
+
const totalDownloads = Object.values(stats).reduce((sum, version) => sum + version.downloads, 0);
|
32
|
+
const statsToDisplay = filterStats(stats, {
|
33
|
+
totalDownloads,
|
34
|
+
all: options.all,
|
35
|
+
top: options.top,
|
36
|
+
});
|
37
|
+
const colors = getColors(statsToDisplay.length, options.color);
|
38
|
+
const primaryColor = chalk.hex(colors[0]);
|
39
|
+
console.log(chalk.bold(`\nNPM weekly downloads for ${primaryColor(packageName)}\n`));
|
40
|
+
console.log(`Total: ${primaryColor(totalDownloads.toLocaleString())} last week\n`);
|
41
|
+
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))}`);
|
56
|
+
});
|
57
|
+
}
|
@@ -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
|
+
}
|
@@ -0,0 +1,57 @@
|
|
1
|
+
import chalk from 'chalk';
|
2
|
+
import { getPrimaryColor } from '../colors.js';
|
3
|
+
import { formatPercentage } from '../format.js';
|
4
|
+
import { fetchNpmLastWeekDownloads } from '../npm-api.js';
|
5
|
+
import { printChart } from '../output.js';
|
6
|
+
import { filterStats, groupStats } from '../stats.js';
|
7
|
+
import { parseVersion, versionCompare } from '../version.js';
|
8
|
+
export async function printPackageStats(packageName, options) {
|
9
|
+
let data;
|
10
|
+
try {
|
11
|
+
data = await fetchNpmLastWeekDownloads(packageName);
|
12
|
+
}
|
13
|
+
catch (error) {
|
14
|
+
console.error(`Failed to fetch data for package "${packageName}"`, error);
|
15
|
+
return;
|
16
|
+
}
|
17
|
+
if (!Object.keys(data.downloads).length) {
|
18
|
+
console.error(`No data found for package "${packageName}".\n`);
|
19
|
+
process.exit(1);
|
20
|
+
}
|
21
|
+
const npmStats = Object.keys(data.downloads)
|
22
|
+
.map((versionString) => {
|
23
|
+
const version = parseVersion(versionString);
|
24
|
+
return {
|
25
|
+
...version,
|
26
|
+
downloads: data.downloads[versionString],
|
27
|
+
};
|
28
|
+
})
|
29
|
+
.sort(versionCompare);
|
30
|
+
const { type, stats } = groupStats(npmStats, options.group);
|
31
|
+
const totalDownloads = Object.values(stats).reduce((sum, version) => sum + version.downloads, 0);
|
32
|
+
const statsToDisplay = filterStats(stats, {
|
33
|
+
totalDownloads,
|
34
|
+
all: options.all,
|
35
|
+
top: options.top,
|
36
|
+
});
|
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));
|
45
|
+
console.log(chalk.bold(`\nNPM weekly downloads for ${primaryColor(packageName)}\n`));
|
46
|
+
console.log(`Total: ${primaryColor(totalDownloads.toLocaleString())} last week\n`);
|
47
|
+
console.log(options.top ? `Top ${options.top} ${type} versions:\n` : `By ${type} version:\n`);
|
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
|
+
});
|
57
|
+
}
|
@@ -0,0 +1,58 @@
|
|
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 singlePackage(packageName, options) {
|
9
|
+
let data;
|
10
|
+
try {
|
11
|
+
data = await fetchNpmLastWeekDownloads(packageName);
|
12
|
+
}
|
13
|
+
catch (error) {
|
14
|
+
console.error(`Failed to fetch data for package "${packageName}"`, error);
|
15
|
+
return;
|
16
|
+
}
|
17
|
+
if (!Object.keys(data.downloads).length) {
|
18
|
+
console.error(`No data found for package "${packageName}".\n`);
|
19
|
+
process.exit(1);
|
20
|
+
}
|
21
|
+
const npmStats = Object.keys(data.downloads)
|
22
|
+
.map((versionString) => {
|
23
|
+
const version = parseVersion(versionString);
|
24
|
+
return {
|
25
|
+
...version,
|
26
|
+
downloads: data.downloads[versionString],
|
27
|
+
};
|
28
|
+
})
|
29
|
+
.sort(versionCompare);
|
30
|
+
const { type, stats } = groupStats(npmStats, options.group);
|
31
|
+
const totalDownloads = Object.values(stats).reduce((sum, version) => sum + version.downloads, 0);
|
32
|
+
const statsToDisplay = filterStats(stats, {
|
33
|
+
totalDownloads,
|
34
|
+
all: options.all,
|
35
|
+
top: options.top,
|
36
|
+
});
|
37
|
+
const colors = getColors(statsToDisplay.length, options.color);
|
38
|
+
const primaryColor = chalk.hex(colors[0]);
|
39
|
+
console.log(chalk.bold(`\nNPM weekly downloads for ${primaryColor(packageName)}\n`));
|
40
|
+
console.log(`Total: ${primaryColor(totalDownloads.toLocaleString())} last week\n`);
|
41
|
+
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))}`);
|
56
|
+
});
|
57
|
+
console.log('');
|
58
|
+
}
|
File without changes
|
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,
|
@@ -65,7 +65,22 @@ function groupByPatch(stats) {
|
|
65
65
|
}
|
66
66
|
return Object.values(result).sort((a, b) => versionCompare(a.version, b.version));
|
67
67
|
}
|
68
|
-
export function
|
68
|
+
export function filterStats(stats, options) {
|
69
|
+
if (options.all) {
|
70
|
+
return stats;
|
71
|
+
}
|
72
|
+
if (options.top) {
|
73
|
+
return pickTopStats(stats, options.top);
|
74
|
+
}
|
75
|
+
const downloadThreshold = 0.005 * options.totalDownloads; // 0.5%
|
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;
|
82
|
+
}
|
83
|
+
function pickTopStats(stats, top) {
|
69
84
|
const sortedStats = stats.sort((a, b) => b.downloads - a.downloads);
|
70
85
|
const topStats = sortedStats.slice(0, top);
|
71
86
|
return topStats.sort((a, b) => versionCompare(a.version, b.version));
|
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,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
|
-
});
|