Skip to content

Commit b795c9f

Browse files
committed
Switch to TypeScript
1 parent c1e8f9f commit b795c9f

File tree

6 files changed

+2052
-48
lines changed

6 files changed

+2052
-48
lines changed

.github/workflows/code-quality.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: 'Code quality'
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
7+
jobs:
8+
build:
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- uses: actions/checkout@v5
13+
- uses: actions/setup-node@v6
14+
with:
15+
node-version: lts/*
16+
- name: 'Code Quality'
17+
run: |
18+
node -v
19+
yarn
20+
yarn prettier
21+
yarn lint
22+
yarn tsc
23+
env:
24+
CURRENCY_API_ENPOINT_TOKEN: ${{ secrets.CURRENCY_API_ENPOINT_TOKEN }}
25+
MONGODB_URI: ${{ secrets.MONGODB_URI }}

eslint.config.mjs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import path from 'node:path';
2+
import { fileURLToPath } from 'node:url';
3+
4+
import js from '@eslint/js';
5+
import globals from 'globals';
6+
import tsParser from '@typescript-eslint/parser';
7+
import tsESLint from '@typescript-eslint/eslint-plugin';
8+
import importPlugin from 'eslint-plugin-import';
9+
import promisePlugin from 'eslint-plugin-promise';
10+
import perfectionist from 'eslint-plugin-perfectionist';
11+
import prettierExtends from 'eslint-config-prettier';
12+
13+
const __filename = fileURLToPath(import.meta.url);
14+
const __dirname = path.dirname(__filename);
15+
16+
export default [
17+
{
18+
files: ['./index.ts'],
19+
plugins: {
20+
'@typescript-eslint': tsESLint,
21+
perfectionist,
22+
import: importPlugin,
23+
promise: promisePlugin
24+
},
25+
languageOptions: {
26+
globals: {
27+
...globals.node
28+
},
29+
parser: tsParser,
30+
ecmaVersion: 'latest',
31+
sourceType: 'module',
32+
parserOptions: {
33+
project: 'tsconfig.json',
34+
tsconfigRootDir: __dirname
35+
}
36+
},
37+
rules: {
38+
...js.configs.recommended.rules,
39+
...prettierExtends.rules,
40+
...tsESLint.configs.recommended.rules,
41+
...importPlugin.configs.recommended.rules,
42+
...promisePlugin.configs.recommended.rules,
43+
'perfectionist/sort-objects': [
44+
'error',
45+
{
46+
type: 'natural',
47+
order: 'asc'
48+
}
49+
],
50+
'sort-vars': 'error',
51+
'perfectionist/sort-jsx-props': 'error',
52+
'perfectionist/sort-interfaces': 'error',
53+
'perfectionist/sort-object-types': 'error',
54+
'perfectionist/sort-imports': [
55+
'error',
56+
{
57+
type: 'natural',
58+
order: 'asc',
59+
newlinesBetween: 'always',
60+
groups: ['builtin', 'external', 'internal']
61+
}
62+
],
63+
'perfectionist/sort-named-imports': [
64+
'error',
65+
{
66+
type: 'natural',
67+
order: 'asc'
68+
}
69+
],
70+
'perfectionist/sort-union-types': [
71+
'error',
72+
{
73+
type: 'natural',
74+
order: 'asc'
75+
}
76+
],
77+
'@typescript-eslint/consistent-type-imports': 'error'
78+
}
79+
}
80+
];

index.mjs renamed to index.ts

Lines changed: 46 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { writeFileSync } from 'fs';
2-
import { join, resolve } from 'path';
3-
import { randomBytes } from 'crypto';
1+
import { randomBytes } from 'node:crypto';
2+
import { writeFileSync } from 'node:fs';
3+
import { join, resolve } from 'node:path';
44

5+
// @ts-expect-error Seems the type definitions are wrong here
56
import { markdownMagic } from 'markdown-magic';
67
import { markdownTable } from 'markdown-table';
78

8-
import pkg from './package.json' with { type: 'json' };
99
import badgeConfig from './badge.json' with { type: 'json' };
10+
import pkg from './package.json' with { type: 'json' };
1011

1112
const key = pkg.stats?.user;
1213
const __dirname = resolve();
@@ -15,6 +16,16 @@ if (!key) {
1516
throw new Error('Please add `user` in the `stats` field of your package.json');
1617
}
1718

19+
type NPMResponse = {
20+
downloads: Array<{
21+
day: string;
22+
downloads: number;
23+
}>;
24+
end: string;
25+
package: string;
26+
start: string;
27+
};
28+
1829
const names = [
1930
'@three11/accordion',
2031
'@three11/animate-top-offset',
@@ -71,7 +82,7 @@ function randomFloat() {
7182
return num / 0xffffffff;
7283
}
7384

74-
async function fetchPackageDownloads(name, maxRetries = 3) {
85+
async function fetchPackageDownloads(name: string, maxRetries = 3) {
7586
const today = new Date();
7687
const endDate = `${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate()}`;
7788
const startDate = pkg.stats.startDate;
@@ -95,16 +106,20 @@ async function fetchPackageDownloads(name, maxRetries = 3) {
95106
throw new Error('Non-JSON response');
96107
}
97108

98-
const json = JSON.parse(text);
109+
const json = JSON.parse(text) as NPMResponse;
99110
const count = json.downloads ? json.downloads.reduce((sum, d) => sum + d.downloads, 0) : 0;
100111

101112
console.log(`✅ [${name}] ${count.toLocaleString()} downloads`);
102113

103114
return count;
104-
} catch (err) {
115+
} catch (err: unknown) {
105116
const delay = 1000 * Math.pow(2, attempt - 1) + randomFloat() * 500;
106117

107-
console.warn(`⏳ [${name}] Retry ${attempt}/${maxRetries} in ${Math.round(delay)}ms (${err.message})`);
118+
if (err instanceof Error) {
119+
console.warn(`⏳ [${name}] Retry ${attempt}/${maxRetries} in ${Math.round(delay)}ms (${err.message})`);
120+
} else {
121+
console.warn(`⏳ [${name}] Retry ${attempt}/${maxRetries} in ${Math.round(delay)}ms (Unknown error)`);
122+
}
108123

109124
await new Promise(resolve => setTimeout(resolve, delay));
110125
}
@@ -115,42 +130,38 @@ async function fetchPackageDownloads(name, maxRetries = 3) {
115130
return 0;
116131
}
117132

118-
(async () => {
119-
console.log(`⏳ Fetching packages downloads data for user ${key} from NPM. Please wait...`);
133+
console.log(`⏳ Fetching packages downloads data for user ${key} from NPM. Please wait...`);
120134

121-
const data = [];
135+
const data = [];
122136

123-
for (const name of names) {
124-
const count = await fetchPackageDownloads(name);
125-
const delay = 1500 + randomFloat() * 500;
137+
for (const name of names) {
138+
const count = await fetchPackageDownloads(name);
139+
const delay = 1500 + randomFloat() * 500;
126140

127-
data.push({ name, count });
141+
data.push({ count, name });
128142

129-
console.log(`⏸ Waiting ${Math.round(delay)}ms before next package...`);
143+
console.log(`⏸ Waiting ${Math.round(delay)}ms before next package...`);
130144

131-
await new Promise(resolve => setTimeout(resolve, delay));
132-
}
145+
await new Promise(resolve => setTimeout(resolve, delay));
146+
}
133147

134-
const sum = data.reduce((sum, curr) => sum + curr.count, 0);
148+
const sum = data.reduce((sum, curr) => sum + curr.count, 0);
135149

136-
const sortedStats = data.toSorted(({ name: aName }, { name: bName }) =>
137-
aName > bName ? 1 : aName < bName ? -1 : 0
138-
);
150+
const sortedStats = data.toSorted(({ name: aName }, { name: bName }) => (aName > bName ? 1 : aName < bName ? -1 : 0));
139151

140-
const mappedStats = sortedStats.map(item => {
141-
const { name, count } = item;
152+
const mappedStats = sortedStats.map(item => {
153+
const { count, name } = item;
142154

143-
return [`[${name}](https://www.npmjs.com/package/${name})`, count];
144-
});
155+
return [`[${name}](https://www.npmjs.com/package/${name})`, count.toString()];
156+
});
145157

146-
badgeConfig.message = `${sum} Downloads`;
158+
badgeConfig.message = `${sum} Downloads`;
147159

148-
await writeFileSync('./badge.json', JSON.stringify(badgeConfig, null, 2));
160+
writeFileSync('./badge.json', JSON.stringify(badgeConfig, null, 2));
149161

150-
await markdownMagic(join(__dirname, 'README.md'), {
151-
matchWord: 'AUTO-GENERATED-CONTENT',
152-
transforms: {
153-
customTransform: () => markdownTable([['Name', 'Downloads'], ...mappedStats, ['**Sum**', `**${sum}**`]])
154-
}
155-
});
156-
})();
162+
markdownMagic(join(__dirname, 'README.md'), {
163+
matchWord: 'AUTO-GENERATED-CONTENT',
164+
transforms: {
165+
customTransform: () => markdownTable([['Name', 'Downloads'], ...mappedStats, ['**Sum**', `**${sum}**`]])
166+
}
167+
});

package.json

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"name": "npm-stats",
3+
"type": "module",
34
"version": "1.0.0",
45
"description": "Statistics for NPM packages downloads",
56
"keywords": [
@@ -21,14 +22,28 @@
2122
"url": "github:scriptex/npm-stats"
2223
},
2324
"scripts": {
24-
"start": "node --experimental-json-modules index.mjs"
25+
"start": "tsx index.ts",
26+
"prettier": "prettier",
27+
"lint": "eslint",
28+
"tsc": "tsc --noEmit --skipLibCheck"
2529
},
2630
"dependencies": {
2731
"markdown-magic": "4.0.4",
2832
"markdown-table": "3.0.4"
2933
},
3034
"devDependencies": {
31-
"prettier": "3.6.2"
35+
"@types/markdown-magic": "1.0.4",
36+
"@types/node": "24.10.1",
37+
"@typescript-eslint/eslint-plugin": "8.46.4",
38+
"@typescript-eslint/parser": "8.46.4",
39+
"eslint": "9.39.1",
40+
"eslint-config-prettier": "10.1.8",
41+
"eslint-plugin-import": "2.32.0",
42+
"eslint-plugin-perfectionist": "4.15.1",
43+
"eslint-plugin-promise": "7.2.1",
44+
"prettier": "3.6.2",
45+
"tsx": "4.20.6",
46+
"typescript": "5.9.3"
3247
},
3348
"private": true,
3449
"stats": {

tsconfig.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ESNext",
4+
"module": "NodeNext",
5+
"moduleResolution": "NodeNext",
6+
"esModuleInterop": true,
7+
"forceConsistentCasingInFileNames": true,
8+
"strict": true,
9+
"skipLibCheck": true,
10+
"outDir": "dist"
11+
},
12+
"include": ["./index.ts"]
13+
}

0 commit comments

Comments
 (0)