trochilus 0.1.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/LICENSE +21 -0
- package/README.md +24 -0
- package/dist/index.js +354 -0
- package/package.json +46 -0
package/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 Victor Sidorov
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# Trochilus
|
2
|
+
|
3
|
+
CLI tool to check various issues in JavaScript project dependencies.
|
4
|
+
Specifically made for end projects, to ensure all dependencies are up-to-date, correctly installed and actively
|
5
|
+
maintained.
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
```
|
10
|
+
npx trochilus <path-to-project-root-folder>
|
11
|
+
```
|
12
|
+
|
13
|
+
### Options
|
14
|
+
|
15
|
+
| Option | Description |
|
16
|
+
|--------|-----------------------|
|
17
|
+
| -v | Enable verbose output |
|
18
|
+
| -h | Print help |
|
19
|
+
|
20
|
+
## Current checks
|
21
|
+
|
22
|
+
- [X] Correct type of dependencies (e.g. `dependencies` and `devDependencies`)
|
23
|
+
- [X] Stale dependencies with archived source code repository
|
24
|
+
- [X] New minor and patch versions of dependencies
|
package/dist/index.js
ADDED
@@ -0,0 +1,354 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
import process$1 from 'node:process';
|
3
|
+
import fs from 'node:fs/promises';
|
4
|
+
import path from 'node:path';
|
5
|
+
import { createCommand } from 'commander';
|
6
|
+
import pc from 'picocolors';
|
7
|
+
import fetch from 'node-fetch';
|
8
|
+
import semver from 'semver';
|
9
|
+
|
10
|
+
function parsePackageJson(content) {
|
11
|
+
try {
|
12
|
+
return JSON.parse(content);
|
13
|
+
} catch {
|
14
|
+
return void 0;
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
18
|
+
var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
|
19
|
+
LogLevel2[LogLevel2["FATAL"] = 0] = "FATAL";
|
20
|
+
LogLevel2[LogLevel2["ERROR"] = 1] = "ERROR";
|
21
|
+
LogLevel2[LogLevel2["WARN"] = 2] = "WARN";
|
22
|
+
LogLevel2[LogLevel2["INFO"] = 3] = "INFO";
|
23
|
+
LogLevel2[LogLevel2["DEBUG"] = 4] = "DEBUG";
|
24
|
+
LogLevel2[LogLevel2["TRACE"] = 5] = "TRACE";
|
25
|
+
return LogLevel2;
|
26
|
+
})(LogLevel || {});
|
27
|
+
let logLevel = 3 /* INFO */;
|
28
|
+
function setLogLevel(level) {
|
29
|
+
logLevel = level;
|
30
|
+
}
|
31
|
+
const logger = {
|
32
|
+
fatal(...args) {
|
33
|
+
if (0 /* FATAL */ <= logLevel) {
|
34
|
+
console.error(pc.red("FATAL "), ...args);
|
35
|
+
}
|
36
|
+
},
|
37
|
+
error(...args) {
|
38
|
+
if (1 /* ERROR */ <= logLevel) {
|
39
|
+
console.error(pc.red("ERROR "), ...args);
|
40
|
+
}
|
41
|
+
},
|
42
|
+
warn(...args) {
|
43
|
+
if (2 /* WARN */ <= logLevel) {
|
44
|
+
console.warn(pc.yellow("WARN "), ...args);
|
45
|
+
}
|
46
|
+
},
|
47
|
+
info(...args) {
|
48
|
+
if (3 /* INFO */ <= logLevel) {
|
49
|
+
console.info(...args);
|
50
|
+
}
|
51
|
+
},
|
52
|
+
debug(...args) {
|
53
|
+
if (4 /* DEBUG */ <= logLevel) {
|
54
|
+
console.debug(pc.blue("DEBUG "), ...args);
|
55
|
+
}
|
56
|
+
},
|
57
|
+
trace(...args) {
|
58
|
+
if (5 /* TRACE */ <= logLevel) {
|
59
|
+
console.trace(pc.gray("TRACE "), ...args);
|
60
|
+
}
|
61
|
+
}
|
62
|
+
};
|
63
|
+
|
64
|
+
function isError(arg) {
|
65
|
+
return typeof arg === "object" && arg != null && "error" in arg && typeof arg.error === "string";
|
66
|
+
}
|
67
|
+
const cache = /* @__PURE__ */ new Map();
|
68
|
+
function resolvePackageMetadata(name) {
|
69
|
+
if (cache.has(name)) {
|
70
|
+
logger.debug(`Package metadata for "${name}" resolved from cache`);
|
71
|
+
return cache.get(name);
|
72
|
+
}
|
73
|
+
const promise = fetch(`https://registry.npmjs.org/${name}`).then((r) => r.json()).then((data) => isError(data) ? void 0 : data);
|
74
|
+
cache.set(name, promise);
|
75
|
+
logger.debug(`Fetching package metadata for "${name}"`);
|
76
|
+
return promise;
|
77
|
+
}
|
78
|
+
|
79
|
+
var IssueCode = /* @__PURE__ */ ((IssueCode2) => {
|
80
|
+
IssueCode2[IssueCode2["WRONG_DEPENDENCY_TYPE"] = 0] = "WRONG_DEPENDENCY_TYPE";
|
81
|
+
IssueCode2[IssueCode2["ABANDONED"] = 1] = "ABANDONED";
|
82
|
+
IssueCode2[IssueCode2["OUTDATED"] = 2] = "OUTDATED";
|
83
|
+
return IssueCode2;
|
84
|
+
})(IssueCode || {});
|
85
|
+
function stringifyIssue(issue) {
|
86
|
+
switch (issue.code) {
|
87
|
+
case 0 /* WRONG_DEPENDENCY_TYPE */: {
|
88
|
+
return `Marked as a wrong dependency type, expected ${issue.expected}, got ${issue.got}`;
|
89
|
+
}
|
90
|
+
case 1 /* ABANDONED */: {
|
91
|
+
return `Project is no longer supported: ${issue.reason}`;
|
92
|
+
}
|
93
|
+
case 2 /* OUTDATED */: {
|
94
|
+
return `New version available: ${issue.max}, current version ${issue.current}`;
|
95
|
+
}
|
96
|
+
}
|
97
|
+
}
|
98
|
+
class Reporter {
|
99
|
+
issues = [];
|
100
|
+
addIssue(issue) {
|
101
|
+
this.issues.push(issue);
|
102
|
+
}
|
103
|
+
}
|
104
|
+
function groupIssues(issues) {
|
105
|
+
return issues.reduce((acc, cur) => {
|
106
|
+
let path;
|
107
|
+
if (acc.has(cur.path)) {
|
108
|
+
path = acc.get(cur.path);
|
109
|
+
} else {
|
110
|
+
path = /* @__PURE__ */ new Map();
|
111
|
+
acc.set(cur.path, path);
|
112
|
+
}
|
113
|
+
let pkg;
|
114
|
+
if (path.has(cur.packageName)) {
|
115
|
+
pkg = path.get(cur.packageName);
|
116
|
+
} else {
|
117
|
+
pkg = [];
|
118
|
+
path.set(cur.packageName, pkg);
|
119
|
+
}
|
120
|
+
pkg.push(cur);
|
121
|
+
return acc;
|
122
|
+
}, /* @__PURE__ */ new Map());
|
123
|
+
}
|
124
|
+
|
125
|
+
function isGitHub(url) {
|
126
|
+
return url.includes("github.com");
|
127
|
+
}
|
128
|
+
function getOwnerAndRepo(url) {
|
129
|
+
if (!isGitHub(url)) {
|
130
|
+
return;
|
131
|
+
}
|
132
|
+
const matches = url.match(/github.com\/([a-zA-Z0-9-]+)\/([a-zA-Z0-9-]+)/);
|
133
|
+
if (matches && matches.length === 3) {
|
134
|
+
return {
|
135
|
+
owner: matches[1],
|
136
|
+
repo: matches[2]
|
137
|
+
};
|
138
|
+
}
|
139
|
+
}
|
140
|
+
async function isArchived(repo) {
|
141
|
+
if (!process.env.GITHUB_TOKEN) {
|
142
|
+
logger.warn("GITHUB_TOKEN env variable is missing, without it you will be rate-limited too fast");
|
143
|
+
logger.warn("Create your access token here https://github.com/settings/tokens and retry");
|
144
|
+
return false;
|
145
|
+
}
|
146
|
+
const response = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}`, {
|
147
|
+
headers: {
|
148
|
+
authorization: `Bearer ${process.env.GITHUB_TOKEN}`
|
149
|
+
}
|
150
|
+
});
|
151
|
+
if (!response.ok) {
|
152
|
+
logger.error((await response.json()).message);
|
153
|
+
return false;
|
154
|
+
}
|
155
|
+
const repositoryInfo = await response.json();
|
156
|
+
if (typeof repositoryInfo === "object" && repositoryInfo != null && "archived" in repositoryInfo) {
|
157
|
+
return repositoryInfo.archived === true;
|
158
|
+
}
|
159
|
+
return false;
|
160
|
+
}
|
161
|
+
|
162
|
+
const archived = {
|
163
|
+
name: "archived",
|
164
|
+
async run(path, pkg, reporter) {
|
165
|
+
const dependencies = [
|
166
|
+
...Object.keys(pkg.dependencies ?? {}),
|
167
|
+
...Object.keys(pkg.devDependencies ?? {})
|
168
|
+
];
|
169
|
+
for (const dependency of dependencies) {
|
170
|
+
logger.debug(`Getting package metadata from npm for ${dependency}`);
|
171
|
+
const data = await resolvePackageMetadata(dependency);
|
172
|
+
if (!data) {
|
173
|
+
logger.warn(`Failed to resolve ${dependency} from npm registry`);
|
174
|
+
continue;
|
175
|
+
}
|
176
|
+
const repo = getOwnerAndRepo(data.repository?.url ?? "");
|
177
|
+
if (repo) {
|
178
|
+
logger.debug(`Detected GitHub repo for ${dependency}`, repo);
|
179
|
+
if (await isArchived(repo)) {
|
180
|
+
reporter.addIssue({
|
181
|
+
path,
|
182
|
+
packageName: dependency,
|
183
|
+
code: IssueCode.ABANDONED,
|
184
|
+
reason: `GitHub repository is archived at https://github.com/${repo.owner}/${repo.repo}`
|
185
|
+
});
|
186
|
+
}
|
187
|
+
}
|
188
|
+
}
|
189
|
+
}
|
190
|
+
};
|
191
|
+
|
192
|
+
class PackageGroup {
|
193
|
+
rules = [];
|
194
|
+
add(rule, description) {
|
195
|
+
this.rules.push({
|
196
|
+
value: rule,
|
197
|
+
description: description ?? ""
|
198
|
+
});
|
199
|
+
}
|
200
|
+
test(name) {
|
201
|
+
return this.rules.some((rule) => {
|
202
|
+
if (typeof rule.value === "string") {
|
203
|
+
return rule.value === name;
|
204
|
+
}
|
205
|
+
if (Array.isArray(rule.value)) {
|
206
|
+
return rule.value.includes(name);
|
207
|
+
}
|
208
|
+
return rule.value.test(name);
|
209
|
+
});
|
210
|
+
}
|
211
|
+
}
|
212
|
+
|
213
|
+
const ts = new PackageGroup();
|
214
|
+
ts.add("typescript", "TypeScript compiler");
|
215
|
+
ts.add(/^@types\//, "TypeScript definitions");
|
216
|
+
ts.add([
|
217
|
+
"ts-node",
|
218
|
+
"tsx",
|
219
|
+
"@swc/register",
|
220
|
+
"@swc-node/register",
|
221
|
+
"esbuild-runner",
|
222
|
+
"jiti",
|
223
|
+
"sucrase",
|
224
|
+
"tsm"
|
225
|
+
], "TypeScript runtimes for Node.js");
|
226
|
+
const typescript = {
|
227
|
+
name: "typescript",
|
228
|
+
async run(path, pkg, reporter) {
|
229
|
+
for (const dependency of Object.keys(pkg.dependencies ?? {})) {
|
230
|
+
if (ts.test(dependency)) {
|
231
|
+
reporter.addIssue({
|
232
|
+
path,
|
233
|
+
packageName: dependency,
|
234
|
+
code: IssueCode.WRONG_DEPENDENCY_TYPE,
|
235
|
+
expected: "development",
|
236
|
+
got: "production"
|
237
|
+
});
|
238
|
+
}
|
239
|
+
}
|
240
|
+
return Promise.resolve();
|
241
|
+
}
|
242
|
+
};
|
243
|
+
|
244
|
+
const webpack = new PackageGroup();
|
245
|
+
webpack.add("webpack", "Webpack bundler");
|
246
|
+
webpack.add(/^webpack-/, "Webpack loaders and plugins");
|
247
|
+
const vite = new PackageGroup();
|
248
|
+
vite.add("vite", "Vite bundler");
|
249
|
+
vite.add(/^@vitejs\//, "Vite plugins");
|
250
|
+
const bundler = {
|
251
|
+
name: "bundler",
|
252
|
+
run(path, pkg, reporter) {
|
253
|
+
for (const dependency of Object.keys(pkg.dependencies ?? {})) {
|
254
|
+
if (webpack.test(dependency) || vite.test(dependency)) {
|
255
|
+
reporter.addIssue({
|
256
|
+
path,
|
257
|
+
packageName: dependency,
|
258
|
+
code: IssueCode.WRONG_DEPENDENCY_TYPE,
|
259
|
+
expected: "development",
|
260
|
+
got: "production"
|
261
|
+
});
|
262
|
+
}
|
263
|
+
}
|
264
|
+
return Promise.resolve();
|
265
|
+
}
|
266
|
+
};
|
267
|
+
|
268
|
+
const updates = {
|
269
|
+
name: "updates",
|
270
|
+
async run(projectPath, pkg, reporter) {
|
271
|
+
const dependencies = [
|
272
|
+
...Object.keys(pkg.dependencies ?? {}),
|
273
|
+
...Object.keys(pkg.devDependencies ?? {})
|
274
|
+
];
|
275
|
+
for (const dep of dependencies) {
|
276
|
+
const dependency = await resolvePackageMetadata(dep);
|
277
|
+
if (!dependency) {
|
278
|
+
logger.warn(`Failed to resolve ${dep} from npm registry`);
|
279
|
+
continue;
|
280
|
+
}
|
281
|
+
const versions = Object.keys(dependency.versions);
|
282
|
+
const currentVersion = pkg.dependencies?.[dep] ?? pkg.devDependencies?.[dep];
|
283
|
+
if (!currentVersion) {
|
284
|
+
continue;
|
285
|
+
}
|
286
|
+
const maxSemver = semver.maxSatisfying(versions, currentVersion);
|
287
|
+
const curSemver = semver.minVersion(currentVersion);
|
288
|
+
if (!semver.eq(maxSemver, curSemver)) {
|
289
|
+
reporter.addIssue({
|
290
|
+
code: IssueCode.OUTDATED,
|
291
|
+
path: projectPath,
|
292
|
+
packageName: dep,
|
293
|
+
max: maxSemver,
|
294
|
+
current: curSemver.toString()
|
295
|
+
});
|
296
|
+
}
|
297
|
+
}
|
298
|
+
}
|
299
|
+
};
|
300
|
+
|
301
|
+
const checks = [
|
302
|
+
archived,
|
303
|
+
typescript,
|
304
|
+
bundler,
|
305
|
+
updates
|
306
|
+
];
|
307
|
+
const program = createCommand("trochilus");
|
308
|
+
program.option("-v, --verbose", "Verbose messages", false).argument("[path]", "Path to project directory", "").action(async (projectPath, opts) => {
|
309
|
+
setLogLevel(opts?.verbose ? LogLevel.DEBUG : LogLevel.INFO);
|
310
|
+
const root = path.resolve(projectPath ? path.resolve(process$1.cwd(), projectPath) : process$1.cwd());
|
311
|
+
logger.debug(`Root folder set to ${root}`);
|
312
|
+
const packageFile = path.join(root, "package.json");
|
313
|
+
try {
|
314
|
+
const stat = await fs.stat(packageFile);
|
315
|
+
if (!stat.isFile()) {
|
316
|
+
logger.error(`${packageFile} is not a file`);
|
317
|
+
process$1.exit(1);
|
318
|
+
}
|
319
|
+
} catch (e) {
|
320
|
+
if (e.code === "ENOENT") {
|
321
|
+
logger.error(`File ${packageFile} not found`);
|
322
|
+
process$1.exit(1);
|
323
|
+
}
|
324
|
+
throw e;
|
325
|
+
}
|
326
|
+
const content = await fs.readFile(packageFile, "utf-8");
|
327
|
+
const pkg = parsePackageJson(content);
|
328
|
+
if (!pkg) {
|
329
|
+
logger.error(`Failed to parse ${packageFile}, file is not a valid JSON file`);
|
330
|
+
return;
|
331
|
+
}
|
332
|
+
const reporter = new Reporter();
|
333
|
+
for (const check of checks) {
|
334
|
+
logger.debug(`Starting ${check.name} check`);
|
335
|
+
await check.run(root, pkg, reporter);
|
336
|
+
logger.debug(`Finished ${check.name} check`);
|
337
|
+
}
|
338
|
+
if (reporter.issues.length === 0) {
|
339
|
+
process$1.exit(0);
|
340
|
+
}
|
341
|
+
const result = groupIssues(reporter.issues);
|
342
|
+
for (const group of result) {
|
343
|
+
logger.info(pc.underline(group[0]));
|
344
|
+
for (const group1 of group[1]) {
|
345
|
+
logger.info(` ${group1[0]}:`);
|
346
|
+
for (const issue of group1[1]) {
|
347
|
+
logger.info(` ${stringifyIssue(issue)}`);
|
348
|
+
}
|
349
|
+
logger.info("");
|
350
|
+
}
|
351
|
+
}
|
352
|
+
process$1.exit(1);
|
353
|
+
});
|
354
|
+
program.parse();
|
package/package.json
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
{
|
2
|
+
"name": "trochilus",
|
3
|
+
"type": "module",
|
4
|
+
"version": "0.1.0",
|
5
|
+
"description": "CLI tool to check issues in JavaScript projects",
|
6
|
+
"files": [
|
7
|
+
"dist"
|
8
|
+
],
|
9
|
+
"repository": {
|
10
|
+
"type": "git",
|
11
|
+
"url": "git+https://github.com/Solant/trochilus.git"
|
12
|
+
},
|
13
|
+
"bugs": {
|
14
|
+
"email": "https://github.com/Solant/trochilus/issues"
|
15
|
+
},
|
16
|
+
"bin": "./dist/index.js",
|
17
|
+
"keywords": [
|
18
|
+
"dependencies",
|
19
|
+
"update",
|
20
|
+
"package.json"
|
21
|
+
],
|
22
|
+
"license": "MIT",
|
23
|
+
"devDependencies": {
|
24
|
+
"@eslint/js": "^9.8.0",
|
25
|
+
"@stylistic/eslint-plugin": "^2.6.1",
|
26
|
+
"@types/node": "^22.0.2",
|
27
|
+
"@types/semver": "^7.5.8",
|
28
|
+
"eslint": "9.x",
|
29
|
+
"globals": "^15.9.0",
|
30
|
+
"pkgroll": "^2.4.2",
|
31
|
+
"tsx": "^4.16.5",
|
32
|
+
"typescript": "^5.5.4",
|
33
|
+
"typescript-eslint": "^8.0.0",
|
34
|
+
"vitest": "^2.0.5"
|
35
|
+
},
|
36
|
+
"dependencies": {
|
37
|
+
"commander": "^12.1.0",
|
38
|
+
"node-fetch": "^3.3.2",
|
39
|
+
"picocolors": "^1.0.1",
|
40
|
+
"semver": "^7.6.3"
|
41
|
+
},
|
42
|
+
"scripts": {
|
43
|
+
"build": "pkgroll",
|
44
|
+
"test": "vitest"
|
45
|
+
}
|
46
|
+
}
|