pkg-stats 0.2.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
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,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
- const options = parseCliOptions(argv);
9
- if (options.help) {
10
- printHelp();
11
- return;
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
- const response = await fetch(`https://api.npmjs.org/versions/${encodeURIComponent(options.name)}/last-week`);
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.name}"`);
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.name}".\n`);
28
+ console.error(`No data found for package "${options.packageName}".\n`);
24
29
  process.exit(1);
25
30
  }
26
- const rawStats = Object.keys(data.downloads)
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
- let groupedStats = groupByType(options.group, rawStats);
36
- const totalDownloads = Object.values(groupedStats).reduce((sum, version) => sum + version.downloads, 0);
37
- const groupedStatsToDisplay = options.top
38
- ? pickTopStats(groupedStats, options.top)
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.name)}\n`));
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` : 'By version:\n');
45
- const maxDownloads = Math.max(...groupedStats.map((v) => v.downloads));
46
- groupedStatsToDisplay.forEach((item, i) => {
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 version = versionParts.length < 3 ? `${item.versionString}.x` : item.versionString;
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(`${version.padStart(8)} ${color(chart)} ${color(downloads.padStart(6))}`);
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
- instagram: { colors: ['#833ab4', '#fd1d1d', '#fcb045'], options: {} },
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: { colors: ['#ff0000', '#ff0100'], options: { interpolation: 'hsv', hsvSpin: 'long' } },
29
- pastel: { colors: ['#74ebd5', '#74ecd5'], options: { interpolation: 'hsv', hsvSpin: 'long' } },
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 ?? getRandomScheme()];
33
- if (count < colors.length) {
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(count, options.hsvSpin ?? false)
39
- : gradient.rgb(count);
46
+ ? gradient.hsv(paddedCount, options.hsvSpin ?? false)
47
+ : gradient.rgb(paddedCount);
40
48
  return tinyColors.map((c) => c.toHexString());
41
49
  }
42
- function getRandomScheme() {
43
- return Object.keys(gradients)[Math.floor(Math.random() * Object.keys(gradients).length)];
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
@@ -0,0 +1,9 @@
1
+ export function formatDownloads(downloads, maxDownloads) {
2
+ if (maxDownloads > 1000000) {
3
+ return `${(downloads / 1000000).toFixed(1)}M`;
4
+ }
5
+ if (maxDownloads > 1000) {
6
+ return `${(downloads / 1000).toFixed(1)}K`;
7
+ }
8
+ return downloads.toString();
9
+ }
@@ -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 groupByType(type, stats) {
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 > 1) {
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 > 1) {
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.2.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
- "minimist": "^1.2.8",
37
+ "meow": "^13.2.0",
38
+ "redent": "^4.0.0",
25
39
  "tinygradient": "^1.1.5"
26
40
  },
27
41
  "devDependencies": {
28
- "@types/minimist": "^1.2.5",
42
+ "@eslint/js": "^9.18.0",
43
+ "@release-it/conventional-changelog": "^10.0.0",
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
+ "release-it": "^18.1.1",
49
+ "typescript": "^5.7.3",
50
+ "typescript-eslint": "^8.19.1",
51
+ "vitest": "^2.1.8"
31
52
  },
32
- "scripts": {
33
- "build": "tsc"
34
- }
35
- }
53
+ "packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a"
54
+ }