Skip to content

Commit d1e45cb

Browse files
committed
writenpmstat.js: create WriteNpmStat
1 parent a322bb7 commit d1e45cb

File tree

6 files changed

+287
-0
lines changed

6 files changed

+287
-0
lines changed

.eslintrc.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module.exports = {
2+
env: {
3+
browser: true,
4+
commonjs: true,
5+
es2021: true
6+
},
7+
extends: [
8+
'standard',
9+
'prettier'
10+
],
11+
parserOptions: {
12+
ecmaVersion: 'latest'
13+
},
14+
rules: {
15+
'no-unused-vars': 1
16+
}
17+
}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
3+
package-lock.json

.prettierrc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"printWidth": 80,
3+
"tabWidth": 4
4+
}

package.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "write-npmstat",
3+
"version": "0.1.0",
4+
"description": "write-npmstat makes it easy to collect, filter and save npm statistics to csv files.",
5+
"main": "writenpmstat.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1",
8+
"check-prettier": "prettier -c ./*js",
9+
"prettier": "prettier -w ./*js",
10+
"check-eslint": "eslint ./*js",
11+
"eslint": "eslint --fix ./*js",
12+
"format": "npm-run-all prettier eslint",
13+
"check": "npm-run-all check-prettier check-eslint"
14+
},
15+
"repository": {
16+
"type": "git",
17+
"url": "https://github.com/veghdev/write-npmstat.git"
18+
},
19+
"keywords": [
20+
"js",
21+
"csv",
22+
"npmstats"
23+
],
24+
"author": "",
25+
"license": "Apache-2.0",
26+
"bugs": {
27+
"url": "https://github.com/veghdev/write-npmstat/issues"
28+
},
29+
"homepage": "https://github.com/veghdev/write-npmstat#readme",
30+
"dependencies": {
31+
"npm-stat-api": "^1.0.0",
32+
"enum": "^3.0.0"
33+
},
34+
"devDependencies": {
35+
"npm-run-all": "^4.1.0",
36+
"prettier": "^2.6.0",
37+
"eslint": "^8.14.0",
38+
"eslint-config-standard": "^17.0.0",
39+
"eslint-config-prettier": "^8.5.0",
40+
"eslint-plugin-import": "^2.26.0",
41+
"eslint-plugin-n": "^15.2.0",
42+
"eslint-plugin-promise": "^6.0.0"
43+
}
44+
}

statdate.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
class StatDate {
2+
#start;
3+
#end;
4+
5+
constructor(start, end) {
6+
this.#start = StatDate.formatStart(start);
7+
this.#end = StatDate.formatEnd(end);
8+
if (this.#start >= this.#end) {
9+
throw new Error("start must be before end");
10+
}
11+
}
12+
13+
get start() {
14+
return this.#start;
15+
}
16+
17+
set start(start) {
18+
start = StatDate.formatStart(start);
19+
if (this.end && start) {
20+
if (start <= this.end) {
21+
throw new Error("start must be before end");
22+
}
23+
}
24+
this.#start = start;
25+
}
26+
27+
get end() {
28+
return this.#end;
29+
}
30+
31+
set end(end) {
32+
end = StatDate.formatEnd(end);
33+
if (this.start && end) {
34+
if (this.start <= end) {
35+
throw new Error("start must be before end");
36+
}
37+
}
38+
this.#end = end;
39+
}
40+
41+
static formatDate(date) {
42+
const dateObj = new Date(date);
43+
const year = dateObj.getFullYear();
44+
const day = ("0" + dateObj.getDate()).slice(-2);
45+
const month = ("0" + (dateObj.getMonth() + 1)).slice(-2);
46+
return year + "-" + month + "-" + day;
47+
}
48+
49+
static formatStart(start) {
50+
const timeDeltaMax = 181;
51+
if (!start) {
52+
start = new Date();
53+
start.setDate(start.getDate() - timeDeltaMax);
54+
}
55+
if (start instanceof Date) {
56+
start = StatDate.formatDate(start);
57+
}
58+
const parsed = start.split("-");
59+
if (parsed.length === 1) {
60+
start += "-01-01";
61+
} else if (parsed.length === 2) {
62+
start += "-01";
63+
} else if (parsed.length !== 3) {
64+
throw new Error("start format is incorrect");
65+
}
66+
return start;
67+
}
68+
69+
static formatEnd(end) {
70+
if (!end) {
71+
end = new Date();
72+
}
73+
if (end instanceof Date) {
74+
end = StatDate.formatDate(end);
75+
}
76+
const parsed = end.split("-");
77+
if (parsed.length === 1) {
78+
end += "-12-31";
79+
} else if (parsed.length === 2) {
80+
const lastDay = new Date(
81+
parseInt(parsed[0]),
82+
parseInt(parsed[1]) + 1,
83+
0
84+
);
85+
end += "-" + ("0" + lastDay.getDate()).slice(-2);
86+
} else if (parsed.length !== 3) {
87+
throw new Error("end format is incorrect");
88+
}
89+
return end;
90+
}
91+
}
92+
93+
module.exports = StatDate;

writenpmstat.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
const npm = require("npm-stat-api");
2+
const Enum = require("enum");
3+
4+
const StatDate = require("./statdate.js");
5+
6+
const StatPeriod = new Enum(
7+
["year", "month", "day", null],
8+
{ ignoreCase: true },
9+
{ freeze: true }
10+
);
11+
12+
class WriteNpmStat {
13+
#packageName;
14+
15+
#datePeriod;
16+
17+
constructor(packageName) {
18+
if (!packageName) {
19+
throw new Error("packageName is a required argument");
20+
}
21+
22+
this.#packageName = packageName;
23+
24+
this.#datePeriod = StatPeriod.year;
25+
}
26+
27+
get packageName() {
28+
return this.#packageName;
29+
}
30+
31+
get datePeriod() {
32+
return this.#datePeriod;
33+
}
34+
35+
set datePeriod(datePeriod) {
36+
this.#datePeriod = StatPeriod.get(datePeriod);
37+
}
38+
39+
getNpmStat(startDay, endDay) {
40+
const statDate = new StatDate(startDay, endDay);
41+
const days = WriteNpmStat.getDays(statDate.start, statDate.end);
42+
return new Promise((resolve) => {
43+
const stats = [];
44+
days.forEach((day) => {
45+
stats.push(this.#getStat(day, 100));
46+
});
47+
Promise.all(stats).then((stats) => {
48+
resolve(Object.fromEntries(stats));
49+
});
50+
});
51+
}
52+
53+
static getDays(startDay, endDay) {
54+
const arr = [];
55+
const dt = new Date(startDay);
56+
for (dt; dt <= new Date(endDay); dt.setDate(dt.getDate() + 1)) {
57+
arr.push(StatDate.formatDate(new Date(dt)));
58+
}
59+
return arr;
60+
}
61+
62+
#getStat(day, retryLimit, retryCount) {
63+
retryLimit = retryLimit || Number.MAX_VALUE;
64+
retryCount = Math.max(retryCount || 0, 0);
65+
return new Promise((resolve) => {
66+
npm.stat(this.packageName, day, day, (err, res) => {
67+
if (err) {
68+
if (retryCount < retryLimit) {
69+
return this.#getStat(day, retryLimit, retryCount + 1);
70+
}
71+
throw new Error("retryLimit reached");
72+
}
73+
return resolve([res.start, res.downloads]);
74+
});
75+
});
76+
}
77+
78+
writeNpmStat(startDay, endDay, postfix = "npmstat") {
79+
return new Promise((resolve) => {
80+
const stats = this.getNpmStat(startDay, endDay);
81+
stats.then((stats) => {
82+
this.#groupStats(stats, startDay, endDay, postfix);
83+
return resolve();
84+
});
85+
});
86+
}
87+
88+
#groupStats(stats, startDay, endDay, postfix) {
89+
const statDate = new StatDate(startDay, endDay);
90+
const days = WriteNpmStat.getDays(statDate.start, statDate.end);
91+
const processedStats = {};
92+
if (this.datePeriod) {
93+
let substring;
94+
if (this.datePeriod === StatPeriod.year) {
95+
substring = 4;
96+
} else if (this.datePeriod === StatPeriod.month) {
97+
substring = 7;
98+
} else if (this.datePeriod === StatPeriod.month) {
99+
substring = 10;
100+
}
101+
const initialized = {};
102+
days.forEach((day) => {
103+
const prefix = day.substring(0, substring);
104+
if (!initialized[prefix]) {
105+
initialized[prefix] = true;
106+
processedStats[prefix + "_" + postfix + ".csv"] = [
107+
[day, stats[day]],
108+
];
109+
} else {
110+
processedStats[prefix + "_" + postfix + ".csv"].push([
111+
day,
112+
stats[day],
113+
]);
114+
}
115+
});
116+
} else {
117+
processedStats[postfix + ".csv"] = [];
118+
days.forEach((day) => {
119+
processedStats[postfix + ".csv"].push([day, stats[day]]);
120+
});
121+
}
122+
console.log(processedStats);
123+
}
124+
}
125+
126+
module.exports = WriteNpmStat;

0 commit comments

Comments
 (0)