pkg-stats 0.2.0 → 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/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,26 +1,22 @@
1
1
  import chalk from 'chalk';
2
- import minimist from 'minimist';
3
2
  import { renderChart } from './chart.js';
3
+ import { parseCliOptions } from './cli-options.js';
4
4
  import { getColors } from './colors.js';
5
- import { parseVersion, versionCompare } from './version.js';
5
+ import { fetchNpmLastWeekDownloads } from './npm-api.js';
6
6
  import { groupByType, pickTopStats } from './stats.js';
7
+ import { parseVersion, versionCompare } from './version.js';
7
8
  export async function pkgStats(argv) {
8
9
  const options = parseCliOptions(argv);
9
- if (options.help) {
10
- printHelp();
11
- return;
12
- }
13
10
  let data;
14
11
  try {
15
- const response = await fetch(`https://api.npmjs.org/versions/${encodeURIComponent(options.name)}/last-week`);
16
- data = await response.json();
12
+ data = await fetchNpmLastWeekDownloads(options.packageName);
17
13
  }
18
14
  catch (error) {
19
- console.error(`Failed to fetch data for package "${options.name}"`);
15
+ console.error(`Failed to fetch data for package "${options.packageName}"`, error);
20
16
  return;
21
17
  }
22
18
  if (!Object.keys(data.downloads).length) {
23
- console.error(`No data found for package "${options.name}".\n`);
19
+ console.error(`No data found for package "${options.packageName}".\n`);
24
20
  process.exit(1);
25
21
  }
26
22
  const rawStats = Object.keys(data.downloads)
@@ -32,14 +28,14 @@ export async function pkgStats(argv) {
32
28
  };
33
29
  })
34
30
  .sort(versionCompare);
35
- let groupedStats = groupByType(options.group, rawStats);
31
+ const groupedStats = groupByType(options.group, rawStats);
36
32
  const totalDownloads = Object.values(groupedStats).reduce((sum, version) => sum + version.downloads, 0);
37
33
  const groupedStatsToDisplay = options.top
38
34
  ? pickTopStats(groupedStats, options.top)
39
35
  : groupedStats;
40
- const colors = getColors(groupedStatsToDisplay.length);
36
+ const colors = getColors(groupedStatsToDisplay.length, options.color);
41
37
  const primaryColor = chalk.hex(colors[0]);
42
- console.log(chalk.bold(`\nNPM weekly downloads for ${primaryColor(options.name)}\n`));
38
+ console.log(chalk.bold(`\nNPM weekly downloads for ${primaryColor(options.packageName)}\n`));
43
39
  console.log(`Total: ${primaryColor(totalDownloads.toLocaleString())} last week\n`);
44
40
  console.log(options.top ? `Top ${options.top} versions:\n` : 'By version:\n');
45
41
  const maxDownloads = Math.max(...groupedStats.map((v) => v.downloads));
@@ -53,43 +49,6 @@ export async function pkgStats(argv) {
53
49
  });
54
50
  console.log('');
55
51
  }
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
52
  function formatDownloads(downloads, maxDownloads) {
94
53
  if (maxDownloads > 1000000) {
95
54
  return `${(downloads / 1000000).toFixed(1)}M`;
@@ -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
@@ -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
  }
@@ -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/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "pkg-stats",
3
- "version": "0.2.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
- "minimist": "^1.2.8",
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
- "typescript": "^5.7.3"
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
- "scripts": {
33
- "build": "tsc"
34
- }
35
- }
54
+ "packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a"
55
+ }