prscan 1.0.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/.vscode/launch.json +14 -0
- package/README.MD +32 -0
- package/dist/bot/lark.d.ts +2 -0
- package/dist/bot/lark.d.ts.map +1 -0
- package/dist/bot/lark.js +156 -0
- package/dist/bot/lark.js.map +1 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/cli/cli.js +77 -0
- package/dist/cli/cli.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/report/index.d.ts +7 -0
- package/dist/report/index.d.ts.map +1 -0
- package/dist/report/index.js +45 -0
- package/dist/report/index.js.map +1 -0
- package/dist/tool/prscan.d.ts +72 -0
- package/dist/tool/prscan.d.ts.map +1 -0
- package/dist/tool/prscan.js +477 -0
- package/dist/tool/prscan.js.map +1 -0
- package/dist/util/analyze.d.ts +4 -0
- package/dist/util/analyze.d.ts.map +1 -0
- package/dist/util/analyze.js +213 -0
- package/dist/util/analyze.js.map +1 -0
- package/dist/util/archive.d.ts +34 -0
- package/dist/util/archive.d.ts.map +1 -0
- package/dist/util/archive.js +110 -0
- package/dist/util/archive.js.map +1 -0
- package/dist/util/memory-archive.d.ts +37 -0
- package/dist/util/memory-archive.d.ts.map +1 -0
- package/dist/util/memory-archive.js +128 -0
- package/dist/util/memory-archive.js.map +1 -0
- package/dist/util/npm.d.ts +46 -0
- package/dist/util/npm.d.ts.map +1 -0
- package/dist/util/npm.js +35 -0
- package/dist/util/npm.js.map +1 -0
- package/dist/util/parse.d.ts +18 -0
- package/dist/util/parse.d.ts.map +1 -0
- package/dist/util/parse.js +92 -0
- package/dist/util/parse.js.map +1 -0
- package/dist/util/proxy.d.ts +45 -0
- package/dist/util/proxy.d.ts.map +1 -0
- package/dist/util/proxy.js +143 -0
- package/dist/util/proxy.js.map +1 -0
- package/dist/util/repo.d.ts +103 -0
- package/dist/util/repo.d.ts.map +1 -0
- package/dist/util/repo.js +170 -0
- package/dist/util/repo.js.map +1 -0
- package/package.json +35 -0
- package/report.png +0 -0
- package/src/bot/lark.ts +184 -0
- package/src/cli/cli.ts +80 -0
- package/src/index.ts +67 -0
- package/src/report/index.ts +50 -0
- package/src/tool/prscan.ts +634 -0
- package/src/util/analyze.ts +248 -0
- package/src/util/memory-archive.ts +184 -0
- package/src/util/npm.ts +100 -0
- package/src/util/parse.ts +103 -0
- package/src/util/repo.ts +224 -0
- package/tsconfig.json +43 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import * as t from "@babel/types";
|
|
2
|
+
import { parse } from "@babel/parser";
|
|
3
|
+
|
|
4
|
+
import _traverse, { type Visitor, type NodePath } from "@babel/traverse";
|
|
5
|
+
|
|
6
|
+
// 使用 require 方式导入以获得正确的类型
|
|
7
|
+
const traverse: (node: t.Node, visitor: Visitor) => void = _traverse.default as any;
|
|
8
|
+
|
|
9
|
+
export type GlobalUsageMap = Record<string, "r" | "rw">;
|
|
10
|
+
|
|
11
|
+
export const ECMAGlobals = [
|
|
12
|
+
"AggregateError",
|
|
13
|
+
"Array",
|
|
14
|
+
"ArrayBuffer",
|
|
15
|
+
"Atomics",
|
|
16
|
+
"BigInt",
|
|
17
|
+
"BigInt64Array",
|
|
18
|
+
"BigUint64Array",
|
|
19
|
+
"Boolean",
|
|
20
|
+
"DataView",
|
|
21
|
+
"Date",
|
|
22
|
+
"Error",
|
|
23
|
+
"EvalError",
|
|
24
|
+
"FinalizationRegistry",
|
|
25
|
+
"Float16Array",
|
|
26
|
+
"Float32Array",
|
|
27
|
+
"Float64Array",
|
|
28
|
+
"Function",
|
|
29
|
+
"Infinity",
|
|
30
|
+
"Int16Array",
|
|
31
|
+
"Int32Array",
|
|
32
|
+
"Int8Array",
|
|
33
|
+
"Intl",
|
|
34
|
+
"Iterator",
|
|
35
|
+
"JSON",
|
|
36
|
+
"Map",
|
|
37
|
+
"Math",
|
|
38
|
+
"NaN",
|
|
39
|
+
"Number",
|
|
40
|
+
"Object",
|
|
41
|
+
"Promise",
|
|
42
|
+
"Proxy",
|
|
43
|
+
"RangeError",
|
|
44
|
+
"ReferenceError",
|
|
45
|
+
"Reflect",
|
|
46
|
+
"RegExp",
|
|
47
|
+
"Set",
|
|
48
|
+
"SharedArrayBuffer",
|
|
49
|
+
"String",
|
|
50
|
+
"Symbol",
|
|
51
|
+
"SyntaxError",
|
|
52
|
+
"TypeError",
|
|
53
|
+
"Uint16Array",
|
|
54
|
+
"Uint32Array",
|
|
55
|
+
"Uint8Array",
|
|
56
|
+
"Uint8ClampedArray",
|
|
57
|
+
"URIError",
|
|
58
|
+
"WeakMap",
|
|
59
|
+
"WeakRef",
|
|
60
|
+
"WeakSet",
|
|
61
|
+
"decodeURI",
|
|
62
|
+
"decodeURIComponent",
|
|
63
|
+
"encodeURI",
|
|
64
|
+
"encodeURIComponent",
|
|
65
|
+
"escape",
|
|
66
|
+
// "eval",
|
|
67
|
+
"globalThis",
|
|
68
|
+
"isFinite",
|
|
69
|
+
"isNaN",
|
|
70
|
+
"parseFloat",
|
|
71
|
+
"parseInt",
|
|
72
|
+
"undefined",
|
|
73
|
+
"unescape",
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
class GVPool {
|
|
77
|
+
public globals: Map<string, "r" | "rw"> = new Map();
|
|
78
|
+
|
|
79
|
+
add(name: string, perm: "r" | "rw") {
|
|
80
|
+
// 不管控除eval以外的ECMA全局变量的读权限
|
|
81
|
+
if (ECMAGlobals.includes(name) && perm === "r") {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (this.globals.has(name)) {
|
|
86
|
+
if (perm === "rw") {
|
|
87
|
+
this.globals.set(name, "rw");
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
this.globals.set(name, perm);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function analyzeGlobals(code: string): GlobalUsageMap {
|
|
96
|
+
const pool = new GVPool();
|
|
97
|
+
|
|
98
|
+
const ast = parse(code, {
|
|
99
|
+
sourceType: "unambiguous",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
traverse(ast, {
|
|
103
|
+
MemberExpression(path: NodePath<t.MemberExpression>) {
|
|
104
|
+
let expr: string[] = [];
|
|
105
|
+
let node: t.MemberExpression = path.node;
|
|
106
|
+
let flag = true;
|
|
107
|
+
while (t.isMemberExpression(node)) {
|
|
108
|
+
if (t.isIdentifier(node.property) && !node.computed) {
|
|
109
|
+
expr.unshift(node.property.name);
|
|
110
|
+
} else if (t.isStringLiteral(node.property)) {
|
|
111
|
+
expr.unshift(node.property.value);
|
|
112
|
+
} else {
|
|
113
|
+
flag = false;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (t.isIdentifier(node.object)) {
|
|
118
|
+
expr.unshift(node.object.name);
|
|
119
|
+
break;
|
|
120
|
+
} else if (t.isMemberExpression(node.object)) {
|
|
121
|
+
node = node.object;
|
|
122
|
+
} else {
|
|
123
|
+
flag = false;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!flag) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 处理 __webpack_require__.g.xxx 的情况, __webpack_require__.g 就是 globalThis
|
|
133
|
+
if (
|
|
134
|
+
expr.length >= 2 &&
|
|
135
|
+
expr[0] === "__webpack_require__" &&
|
|
136
|
+
expr[1] === "g"
|
|
137
|
+
) {
|
|
138
|
+
expr = expr.slice(2);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const globalAlias = ["globalThis", "self", "window"];
|
|
142
|
+
let mustBeGlobal = false;
|
|
143
|
+
while (
|
|
144
|
+
expr.length > 0 &&
|
|
145
|
+
globalAlias.includes(expr[0]!) &&
|
|
146
|
+
!path.scope.hasBinding(expr[0]!, { noGlobals: true })
|
|
147
|
+
) {
|
|
148
|
+
expr = expr.slice(1);
|
|
149
|
+
// 如果是globalThis/self/window开头的, 则一定是全局变量
|
|
150
|
+
mustBeGlobal = true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (
|
|
154
|
+
expr.length > 0 &&
|
|
155
|
+
(mustBeGlobal ||
|
|
156
|
+
!path.scope.hasBinding(expr[0]!, { noGlobals: true })) &&
|
|
157
|
+
expr[0] !== "arguments"
|
|
158
|
+
) {
|
|
159
|
+
pool.add(
|
|
160
|
+
expr[0]!,
|
|
161
|
+
|
|
162
|
+
t.isAssignmentExpression(path.parent) &&
|
|
163
|
+
path.parent.left === path.node &&
|
|
164
|
+
expr.length === 1
|
|
165
|
+
? "rw"
|
|
166
|
+
: "r"
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
path.skip();
|
|
171
|
+
},
|
|
172
|
+
Identifier(path: NodePath<t.Identifier>) {
|
|
173
|
+
if (!path.scope.hasBinding(path.node.name, { noGlobals: true })) {
|
|
174
|
+
// 排除一些不算真正变量读取的场景
|
|
175
|
+
if (
|
|
176
|
+
(t.isMemberExpression(path.parent) ||
|
|
177
|
+
t.isOptionalMemberExpression(path.parent)) &&
|
|
178
|
+
path.parent.property === path.node
|
|
179
|
+
) {
|
|
180
|
+
return;
|
|
181
|
+
} else if (
|
|
182
|
+
t.isObjectProperty(path.parent) &&
|
|
183
|
+
path.parent.key === path.node
|
|
184
|
+
) {
|
|
185
|
+
return;
|
|
186
|
+
} else if (
|
|
187
|
+
(t.isLabeledStatement(path.parent) ||
|
|
188
|
+
t.isBreakStatement(path.parent) ||
|
|
189
|
+
t.isContinueStatement(path.parent)) &&
|
|
190
|
+
path.parent.label === path.node
|
|
191
|
+
) {
|
|
192
|
+
return;
|
|
193
|
+
} else if (
|
|
194
|
+
(t.isFunctionDeclaration(path.parent) ||
|
|
195
|
+
t.isFunctionExpression(path.parent)) &&
|
|
196
|
+
path.parent.id === path.node
|
|
197
|
+
) {
|
|
198
|
+
return;
|
|
199
|
+
} else if (
|
|
200
|
+
t.isCatchClause(path.parent) &&
|
|
201
|
+
path.parent.param === path.node
|
|
202
|
+
) {
|
|
203
|
+
return;
|
|
204
|
+
} else if (
|
|
205
|
+
t.isFunction(path.parent) &&
|
|
206
|
+
(path.parent.params.includes(path.node) ||
|
|
207
|
+
(path.parent as any).id === path.node ||
|
|
208
|
+
(path.parent as any).kind === path.node.name ||
|
|
209
|
+
(path.parent as any).key === path.node)
|
|
210
|
+
) {
|
|
211
|
+
return;
|
|
212
|
+
} else if (t.isMetaProperty(path.parent)) {
|
|
213
|
+
return;
|
|
214
|
+
} else if (
|
|
215
|
+
t.isClassProperty(path.parent) &&
|
|
216
|
+
path.parent.key === path.node
|
|
217
|
+
) {
|
|
218
|
+
return;
|
|
219
|
+
// globalAlias默认可访问
|
|
220
|
+
} else if (
|
|
221
|
+
path.node.name === "globalThis" ||
|
|
222
|
+
path.node.name === "window" ||
|
|
223
|
+
path.node.name === "self"
|
|
224
|
+
) {
|
|
225
|
+
return;
|
|
226
|
+
} else if (path.node.name === "arguments") {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
pool.add(
|
|
231
|
+
path.node.name,
|
|
232
|
+
|
|
233
|
+
t.isAssignmentExpression(path.parent) &&
|
|
234
|
+
path.parent.left === path.node
|
|
235
|
+
? "rw"
|
|
236
|
+
: "r"
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const obj: GlobalUsageMap = Object.create(null);
|
|
243
|
+
for (const [name, perm] of pool.globals.entries()) {
|
|
244
|
+
obj[name] = perm;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return obj;
|
|
248
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import tarStream from 'tar-stream';
|
|
2
|
+
import gunzip from 'gunzip-maybe';
|
|
3
|
+
import { Readable } from 'stream';
|
|
4
|
+
import got from 'got';
|
|
5
|
+
|
|
6
|
+
export interface ExtractedFile {
|
|
7
|
+
path: string;
|
|
8
|
+
content: Buffer;
|
|
9
|
+
size: number;
|
|
10
|
+
type: 'file' | 'directory' | 'symlink';
|
|
11
|
+
mode: number;
|
|
12
|
+
mtime: Date;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ExtractionOptions {
|
|
16
|
+
/** 只解压匹配的文件路径 */
|
|
17
|
+
filter?: (path: string) => boolean;
|
|
18
|
+
/** 最大文件大小限制 (bytes) */
|
|
19
|
+
maxFileSize?: number;
|
|
20
|
+
/** 最大总解压大小限制 (bytes) */
|
|
21
|
+
maxTotalSize?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 在内存中解压 .tar.gz Buffer
|
|
26
|
+
*/
|
|
27
|
+
export async function extractTarGzFromBuffer(
|
|
28
|
+
tarGzBuffer: Buffer,
|
|
29
|
+
options: ExtractionOptions = {}
|
|
30
|
+
): Promise<ExtractedFile[]> {
|
|
31
|
+
const { filter, maxFileSize = 50 * 1024 * 1024, maxTotalSize = 500 * 1024 * 1024 } = options;
|
|
32
|
+
const files: ExtractedFile[] = [];
|
|
33
|
+
let totalSize = 0;
|
|
34
|
+
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const extract = tarStream.extract();
|
|
37
|
+
|
|
38
|
+
extract.on('entry', (header, stream, next) => {
|
|
39
|
+
// 应用过滤器
|
|
40
|
+
if (filter && !filter(header.name)) {
|
|
41
|
+
stream.on('end', next);
|
|
42
|
+
stream.resume();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 检查文件大小限制
|
|
47
|
+
if (header.size && header.size > maxFileSize) {
|
|
48
|
+
stream.on('end', next);
|
|
49
|
+
stream.resume();
|
|
50
|
+
console.warn(`File ${header.name} exceeds size limit, skipping`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const chunks: Buffer[] = [];
|
|
55
|
+
let fileSize = 0;
|
|
56
|
+
|
|
57
|
+
stream.on('data', (chunk: Buffer) => {
|
|
58
|
+
fileSize += chunk.length;
|
|
59
|
+
totalSize += chunk.length;
|
|
60
|
+
|
|
61
|
+
// 检查总大小限制
|
|
62
|
+
if (totalSize > maxTotalSize) {
|
|
63
|
+
reject(new Error('Total extraction size limit exceeded'));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
chunks.push(chunk);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
stream.on('end', () => {
|
|
71
|
+
if (header.type === 'file') {
|
|
72
|
+
files.push({
|
|
73
|
+
path: header.name,
|
|
74
|
+
content: Buffer.concat(chunks),
|
|
75
|
+
size: fileSize,
|
|
76
|
+
type: 'file',
|
|
77
|
+
mode: header.mode || 0,
|
|
78
|
+
mtime: header.mtime || new Date()
|
|
79
|
+
});
|
|
80
|
+
} else if (header.type === 'directory') {
|
|
81
|
+
files.push({
|
|
82
|
+
path: header.name,
|
|
83
|
+
content: Buffer.alloc(0),
|
|
84
|
+
size: 0,
|
|
85
|
+
type: 'directory',
|
|
86
|
+
mode: header.mode || 0,
|
|
87
|
+
mtime: header.mtime || new Date()
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
next();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
stream.on('error', reject);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
extract.on('finish', () => {
|
|
97
|
+
resolve(files);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
extract.on('error', reject);
|
|
101
|
+
|
|
102
|
+
// 处理压缩流
|
|
103
|
+
Readable.from(tarGzBuffer)
|
|
104
|
+
.pipe(gunzip())
|
|
105
|
+
.pipe(extract);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 从URL下载并在内存中解压
|
|
112
|
+
*/
|
|
113
|
+
export async function extractTarGzFromUrl(
|
|
114
|
+
url: string,
|
|
115
|
+
options: ExtractionOptions = {}
|
|
116
|
+
): Promise<ExtractedFile[]> {
|
|
117
|
+
const response = await got(url);
|
|
118
|
+
if (response.statusCode !== 200) {
|
|
119
|
+
throw new Error(`Failed to download: ${response.statusMessage}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const buffer = response.rawBody;
|
|
123
|
+
return extractTarGzFromBuffer(buffer, options);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 获取特定文件内容
|
|
128
|
+
*/
|
|
129
|
+
export async function getFileFromTarGz(
|
|
130
|
+
tarGzBuffer: Buffer,
|
|
131
|
+
filePath: string
|
|
132
|
+
): Promise<Buffer | null> {
|
|
133
|
+
const files = await extractTarGzFromBuffer(tarGzBuffer, {
|
|
134
|
+
filter: (path) => path === filePath
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return files.length > 0 ? files[0]!.content : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 只获取文件列表,不提取内容
|
|
142
|
+
*/
|
|
143
|
+
export async function listTarGzContents(tarGzBuffer: Buffer): Promise<string[]> {
|
|
144
|
+
const fileList: string[] = [];
|
|
145
|
+
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
const extract = tarStream.extract();
|
|
148
|
+
|
|
149
|
+
extract.on('entry', (header, stream, next) => {
|
|
150
|
+
fileList.push(header.name);
|
|
151
|
+
stream.on('end', next);
|
|
152
|
+
stream.resume(); // 跳过内容读取
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
extract.on('finish', () => {
|
|
156
|
+
resolve(fileList);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
extract.on('error', reject);
|
|
160
|
+
|
|
161
|
+
Readable.from(tarGzBuffer)
|
|
162
|
+
.pipe(gunzip())
|
|
163
|
+
.pipe(extract);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 按文件扩展名过滤提取
|
|
169
|
+
*/
|
|
170
|
+
export async function extractByExtensions(
|
|
171
|
+
tarGzBuffer: Buffer,
|
|
172
|
+
extensions: string[]
|
|
173
|
+
): Promise<ExtractedFile[]> {
|
|
174
|
+
const normalizedExts = extensions.map(ext =>
|
|
175
|
+
ext.startsWith('.') ? ext.toLowerCase() : `.${ext.toLowerCase()}`
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return extractTarGzFromBuffer(tarGzBuffer, {
|
|
179
|
+
filter: (path) => {
|
|
180
|
+
const ext = path.toLowerCase().substring(path.lastIndexOf('.'));
|
|
181
|
+
return normalizedExts.includes(ext);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
package/src/util/npm.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import got from "got";
|
|
2
|
+
|
|
3
|
+
export interface NpmPackageInfo {
|
|
4
|
+
_id: string;
|
|
5
|
+
_rev: string;
|
|
6
|
+
bugs: {
|
|
7
|
+
url: string;
|
|
8
|
+
};
|
|
9
|
+
description: string;
|
|
10
|
+
"dist-tags": {
|
|
11
|
+
latest: string;
|
|
12
|
+
beta: string;
|
|
13
|
+
};
|
|
14
|
+
homepage: string;
|
|
15
|
+
keywords: string[];
|
|
16
|
+
license: string;
|
|
17
|
+
name: string;
|
|
18
|
+
repository: {
|
|
19
|
+
type: string;
|
|
20
|
+
url: string;
|
|
21
|
+
};
|
|
22
|
+
time: Record<string, string>;
|
|
23
|
+
versions: Record<
|
|
24
|
+
string,
|
|
25
|
+
{
|
|
26
|
+
_id: string;
|
|
27
|
+
version: string;
|
|
28
|
+
name: string;
|
|
29
|
+
license: string;
|
|
30
|
+
keywords: string[];
|
|
31
|
+
homepage: string;
|
|
32
|
+
description: string;
|
|
33
|
+
dist: {
|
|
34
|
+
fileCount: number;
|
|
35
|
+
unpackedSize: number;
|
|
36
|
+
tarball: string;
|
|
37
|
+
shasum: string;
|
|
38
|
+
integrity: string;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface NpmDownloadStats {
|
|
45
|
+
downloads: number;
|
|
46
|
+
start: string;
|
|
47
|
+
end: string;
|
|
48
|
+
package: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const maxRetries = 3;
|
|
52
|
+
|
|
53
|
+
export async function getNpmPackageInfo(
|
|
54
|
+
packageName: string
|
|
55
|
+
): Promise<NpmPackageInfo | null> {
|
|
56
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
57
|
+
try {
|
|
58
|
+
const response = await got(
|
|
59
|
+
`https://registry.npmjs.org/${packageName}`
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (response.statusCode !== 200) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`HTTP ${response.statusCode}: ${response.statusMessage}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const packageData = JSON.parse(response.body) as NpmPackageInfo;
|
|
69
|
+
return packageData;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function getNpmPackageDownloadStats(
|
|
78
|
+
packageName: string,
|
|
79
|
+
period: "last-day" | "last-week" | "last-month" | "last-year" = "last-month"
|
|
80
|
+
): Promise<NpmDownloadStats | null> {
|
|
81
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
82
|
+
try {
|
|
83
|
+
const response = await got(
|
|
84
|
+
`https://api.npmjs.org/downloads/point/${period}/${packageName}`
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (response.statusCode !== 200) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`HTTP ${response.statusCode}: ${response.statusMessage}`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const data = JSON.parse(response.body) as NpmDownloadStats;
|
|
94
|
+
return data || null;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { parseSyml } from "@yarnpkg/parsers";
|
|
2
|
+
import * as yaml from "js-yaml";
|
|
3
|
+
|
|
4
|
+
abstract class LockParser {
|
|
5
|
+
abstract getDependencies(): Record<string, string[]>;
|
|
6
|
+
constructor(content: string) {}
|
|
7
|
+
|
|
8
|
+
public static deps2Set(deps: Record<string, string[]>): Set<string> {
|
|
9
|
+
const r = new Set<string>();
|
|
10
|
+
for (const key in deps) {
|
|
11
|
+
const versions = deps[key]!;
|
|
12
|
+
for (let i = 0; i < versions.length; i++) {
|
|
13
|
+
r.add(`${key}@${versions[i]}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return r;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class PnpmLockParser extends LockParser {
|
|
21
|
+
private yaml: any;
|
|
22
|
+
constructor(content: string) {
|
|
23
|
+
super(content);
|
|
24
|
+
this.yaml = yaml.load(content) as any;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public getDependencies(): Record<string, string[]> {
|
|
28
|
+
const r: Record<string, string[]> = Object.create(null);
|
|
29
|
+
if (!this.yaml.packages) {
|
|
30
|
+
return r;
|
|
31
|
+
}
|
|
32
|
+
Object.entries(this.yaml.packages).forEach(([pkgKey, pkgInfo]) => {
|
|
33
|
+
// 解析包键,格式如: "/lodash/4.17.21" 或 "lodash@4.17.21"
|
|
34
|
+
let name, version;
|
|
35
|
+
|
|
36
|
+
if (pkgKey.startsWith("/")) {
|
|
37
|
+
// 旧格式: /lodash/4.17.21
|
|
38
|
+
const parts = pkgKey.split("/");
|
|
39
|
+
name = parts[1];
|
|
40
|
+
version = parts[2];
|
|
41
|
+
} else {
|
|
42
|
+
// 新格式: lodash@4.17.21
|
|
43
|
+
const match = pkgKey.match(/^(.+?)@(.+)$/);
|
|
44
|
+
if (match) {
|
|
45
|
+
name = match[1];
|
|
46
|
+
version = match[2];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (name && version) {
|
|
51
|
+
// 处理 peer dependency 后缀,如 "babel-jest@27.5.1(@babel/core@7.23.6)"
|
|
52
|
+
let cleanVersion = version.split("(")[0];
|
|
53
|
+
if (cleanVersion == null) {
|
|
54
|
+
cleanVersion = version;
|
|
55
|
+
}
|
|
56
|
+
if (!r[name]) {
|
|
57
|
+
r[name] = [];
|
|
58
|
+
}
|
|
59
|
+
if (r[name]!.indexOf(cleanVersion) === -1) {
|
|
60
|
+
r[name]!.push(cleanVersion);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
return r;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class YarnLockParser extends LockParser {
|
|
69
|
+
private syml: any;
|
|
70
|
+
|
|
71
|
+
constructor(content: string) {
|
|
72
|
+
super(content);
|
|
73
|
+
this.syml = parseSyml(content);
|
|
74
|
+
}
|
|
75
|
+
private static getPackageName(key: string): string {
|
|
76
|
+
key = key.split(",")[0]!.trim();
|
|
77
|
+
key = key.split(":")[0]!.trim();
|
|
78
|
+
return key.lastIndexOf("@") > 0
|
|
79
|
+
? key.slice(0, key.lastIndexOf("@"))
|
|
80
|
+
: key;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public getDependencies(): Record<string, string[]> {
|
|
84
|
+
const r: Record<string, string[]> = Object.create(null);
|
|
85
|
+
for (const key in this.syml) {
|
|
86
|
+
const entry = this.syml[key];
|
|
87
|
+
if (entry === "__metadata") {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const pkgName = YarnLockParser.getPackageName(key);
|
|
92
|
+
if (!r[pkgName]) {
|
|
93
|
+
r[pkgName] = [];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (r[pkgName].indexOf(entry.version) === -1) {
|
|
97
|
+
r[pkgName].push(entry.version);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return r;
|
|
102
|
+
}
|
|
103
|
+
}
|