meodp 0.0.1
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 +41 -0
- package/bin/index.ts +6 -0
- package/dist/cli/index.cjs +76 -0
- package/dist/cli/index.d.cts +11 -0
- package/dist/cli/index.d.mts +11 -0
- package/dist/cli/index.d.ts +11 -0
- package/dist/cli/index.mjs +65 -0
- package/dist/index.cjs +72 -0
- package/dist/index.d.cts +360 -0
- package/dist/index.d.mts +360 -0
- package/dist/index.d.ts +360 -0
- package/dist/index.mjs +45 -0
- package/dist/shared/meodp.6ae77875.cjs +862 -0
- package/dist/shared/meodp.faa3c78c.mjs +819 -0
- package/package.json +82 -0
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
import consola from 'consola';
|
|
2
|
+
import PQueue from 'p-queue';
|
|
3
|
+
import { colors } from 'consola/utils';
|
|
4
|
+
import { chromium } from 'playwright';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
import { formatDate } from 'date-fns';
|
|
8
|
+
import fs from 'fs-extra';
|
|
9
|
+
import stripAnsi from 'strip-ansi';
|
|
10
|
+
import winston from 'winston';
|
|
11
|
+
import transports from 'winston/lib/winston/transports';
|
|
12
|
+
import cliProgress from 'cli-progress';
|
|
13
|
+
import Options from 'cli-progress/lib/options';
|
|
14
|
+
import _defaultFormatBar from 'cli-progress/lib/format-bar';
|
|
15
|
+
import _defaultFormatTime from 'cli-progress/lib/format-time';
|
|
16
|
+
|
|
17
|
+
const siteUrlMap = /* @__PURE__ */ new Map();
|
|
18
|
+
const PQueueMap = /* @__PURE__ */ new Map();
|
|
19
|
+
|
|
20
|
+
var __defProp$1 = Object.defineProperty;
|
|
21
|
+
var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
22
|
+
var __publicField$1 = (obj, key, value) => {
|
|
23
|
+
__defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
24
|
+
return value;
|
|
25
|
+
};
|
|
26
|
+
function logUrlItemInfo(urlItem) {
|
|
27
|
+
consola.info("urlItem", urlItem);
|
|
28
|
+
}
|
|
29
|
+
const _LocalLog = class _LocalLog {
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
_LocalLog.options = options;
|
|
32
|
+
_LocalLog.nameRule = options.nameRule || "url";
|
|
33
|
+
_LocalLog.init(options);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* @desc init ensure dir
|
|
37
|
+
*/
|
|
38
|
+
static async init(options) {
|
|
39
|
+
const cwd = options?.cwd || process.cwd();
|
|
40
|
+
_LocalLog.rootLogFolder = path.resolve(cwd, "logs/meodp");
|
|
41
|
+
fs.ensureDirSync(this.rootLogFolder);
|
|
42
|
+
if (_LocalLog.nameRule === "timestamp") {
|
|
43
|
+
const now = formatDate(/* @__PURE__ */ new Date(), "yyyy-MM-dd-HH-mm-ss");
|
|
44
|
+
this.logFolder = path.resolve(this.rootLogFolder, now);
|
|
45
|
+
} else {
|
|
46
|
+
this.logFolder = path.resolve(this.rootLogFolder);
|
|
47
|
+
}
|
|
48
|
+
fs.ensureDirSync(this.logFolder);
|
|
49
|
+
this.siteLogFolder = path.resolve(this.logFolder, "site");
|
|
50
|
+
fs.ensureDirSync(this.siteLogFolder);
|
|
51
|
+
this.linkLogFile = path.resolve(this.logFolder, "link.log");
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* @desc clean log
|
|
55
|
+
*/
|
|
56
|
+
static async clean() {
|
|
57
|
+
await fs.remove(this.rootLogFolder);
|
|
58
|
+
}
|
|
59
|
+
static async log(filePath, ...args) {
|
|
60
|
+
const now = formatDate(/* @__PURE__ */ new Date(), "yyyy-MM-dd HH:mm:ss");
|
|
61
|
+
const content = stripAnsi(`${[now, ...args].join(" ")}
|
|
62
|
+
`);
|
|
63
|
+
await fs.appendFile(filePath, content);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* @desc append log
|
|
67
|
+
*/
|
|
68
|
+
static async SiteLog(urlItem, ...args) {
|
|
69
|
+
const logName = (urlItem.name || urlItem.url).replace("https://", "").replace("http://", "").replace(/\//g, "-");
|
|
70
|
+
const logPath = path.resolve(this.logFolder, urlItem.type, `${logName}.log`);
|
|
71
|
+
await _LocalLog.log(logPath, ...args);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* console link log
|
|
75
|
+
*/
|
|
76
|
+
static async linkLog(...args) {
|
|
77
|
+
await _LocalLog.log(this.linkLogFile, ...args);
|
|
78
|
+
}
|
|
79
|
+
static async logByType(urlItem, ...args) {
|
|
80
|
+
switch (urlItem.type) {
|
|
81
|
+
case "site":
|
|
82
|
+
this.SiteLog(urlItem, ...args);
|
|
83
|
+
break;
|
|
84
|
+
case "link":
|
|
85
|
+
this.linkLog(...args);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* @desc create log
|
|
91
|
+
*/
|
|
92
|
+
static createLog(urlItem) {
|
|
93
|
+
return (...args) => {
|
|
94
|
+
_LocalLog.logByType(urlItem, ...args);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* 日志根目录
|
|
100
|
+
*/
|
|
101
|
+
__publicField$1(_LocalLog, "rootLogFolder");
|
|
102
|
+
/**
|
|
103
|
+
* rootLogFolder + yyyy-MM-dd-HH-mm-ss
|
|
104
|
+
*/
|
|
105
|
+
__publicField$1(_LocalLog, "logFolder");
|
|
106
|
+
/**
|
|
107
|
+
* rootLogFolder + site
|
|
108
|
+
*/
|
|
109
|
+
__publicField$1(_LocalLog, "siteLogFolder");
|
|
110
|
+
/**
|
|
111
|
+
* link.log
|
|
112
|
+
*/
|
|
113
|
+
__publicField$1(_LocalLog, "linkLogFile");
|
|
114
|
+
/**
|
|
115
|
+
* 日志命名规则
|
|
116
|
+
* - timestamp: yyyy-MM-dd-HH-mm-ss 每次都会建立一个以时间戳命名的文件夹
|
|
117
|
+
* - url: url 在同一个文件夹下
|
|
118
|
+
*/
|
|
119
|
+
__publicField$1(_LocalLog, "nameRule");
|
|
120
|
+
__publicField$1(_LocalLog, "options");
|
|
121
|
+
let LocalLog = _LocalLog;
|
|
122
|
+
const localLog = new LocalLog();
|
|
123
|
+
|
|
124
|
+
const RAW_SYMBOLS = {
|
|
125
|
+
block: "\u2503",
|
|
126
|
+
start: "\u25D0",
|
|
127
|
+
pointer: "\u276F",
|
|
128
|
+
uncheck: "\u25CC",
|
|
129
|
+
success: "\u2714",
|
|
130
|
+
info: "\u2139",
|
|
131
|
+
warn: "\u26A0",
|
|
132
|
+
error: "\u2716",
|
|
133
|
+
line: "\u2502",
|
|
134
|
+
end: "\u2514"
|
|
135
|
+
};
|
|
136
|
+
const COLORFUL_SYMBOLS = {
|
|
137
|
+
block: colors.bold(colors.dim(RAW_SYMBOLS.block)),
|
|
138
|
+
start: colors.magenta(RAW_SYMBOLS.start),
|
|
139
|
+
uncheck: colors.gray(RAW_SYMBOLS.uncheck),
|
|
140
|
+
pointer: colors.cyan(RAW_SYMBOLS.pointer),
|
|
141
|
+
success: colors.green(RAW_SYMBOLS.success),
|
|
142
|
+
info: colors.blue(RAW_SYMBOLS.info),
|
|
143
|
+
warn: colors.yellow(RAW_SYMBOLS.warn),
|
|
144
|
+
error: colors.red(RAW_SYMBOLS.error),
|
|
145
|
+
line: colors.dim(RAW_SYMBOLS.line)
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const formatter = (options, params, payload) => {
|
|
149
|
+
const { value, total } = params;
|
|
150
|
+
const formatTime = options.formatTime || _defaultFormatTime;
|
|
151
|
+
const formatBar = options.formatBar || _defaultFormatBar;
|
|
152
|
+
const bar = formatBar(params.progress, options);
|
|
153
|
+
const stopTime = params.stopTime || Date.now();
|
|
154
|
+
const elapsedTime = Math.round((stopTime - params.startTime) / 1e3);
|
|
155
|
+
const duration_formatted = formatTime(elapsedTime, options, 1);
|
|
156
|
+
consola.debug(duration_formatted);
|
|
157
|
+
if (!payload.type) {
|
|
158
|
+
options = Object.assign({}, options, cliProgress.Presets.rect);
|
|
159
|
+
Options.assignDerivedOptions(options);
|
|
160
|
+
const bar2 = formatBar(params.progress, options);
|
|
161
|
+
const { site } = payload;
|
|
162
|
+
const url = payload.url || "";
|
|
163
|
+
return `${bar2} ${value}/${total} | ${colors.yellow(site || "MEODP Item")} | ${colors.cyan(url || "URL")}`;
|
|
164
|
+
} else {
|
|
165
|
+
const { error_count, emoji, url, type } = payload;
|
|
166
|
+
const errorInfo = error_count ? `\u274C${error_count} ` : "";
|
|
167
|
+
return `${bar} \u2705${colors.green(value)}/${total} ${errorInfo}| ${duration_formatted} | ${emoji} ${colors.yellow(type)} | ${colors.cyan(url)}`;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
const multiBar = new cliProgress.MultiBar({
|
|
171
|
+
hideCursor: true,
|
|
172
|
+
// format: '{bar} {percentage}% | ✅{value}/{total} ❌{error_count} | {duration_formatted} | {name}',
|
|
173
|
+
format: formatter,
|
|
174
|
+
barsize: 20,
|
|
175
|
+
autopadding: true,
|
|
176
|
+
barCompleteChar: "\u2588",
|
|
177
|
+
barIncompleteChar: "\u2591",
|
|
178
|
+
// important! redraw everything to avoid "empty" completed bars
|
|
179
|
+
forceRedraw: true
|
|
180
|
+
}, cliProgress.Presets.rect);
|
|
181
|
+
const progressBarMap = /* @__PURE__ */ new Map();
|
|
182
|
+
|
|
183
|
+
function formatArgs(args) {
|
|
184
|
+
return args.join(" ");
|
|
185
|
+
}
|
|
186
|
+
function getFormattedDataFromResponse(res) {
|
|
187
|
+
const statusCode = res?.status();
|
|
188
|
+
const statusText = res?.statusText();
|
|
189
|
+
const statusInfo = statusText ? `${statusCode?.toString()} ${statusText}` : statusCode?.toString();
|
|
190
|
+
const req = res.request();
|
|
191
|
+
const duration = req.timing().responseEnd - req.timing().requestStart;
|
|
192
|
+
const durationText = colors.dim(`\u2502${colors.blue(`${duration.toString().padStart(4, " ")}${colors.white("ms")}`)} \u2502`);
|
|
193
|
+
return {
|
|
194
|
+
duration,
|
|
195
|
+
durationText,
|
|
196
|
+
statusCode,
|
|
197
|
+
statusText,
|
|
198
|
+
statusInfo,
|
|
199
|
+
linkText: colors.dim(res.url())
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function registerPageEvents(page, urlItem) {
|
|
204
|
+
const urlMap = /* @__PURE__ */ new Map();
|
|
205
|
+
const log = LocalLog.createLog(urlItem);
|
|
206
|
+
let respondNum = 0;
|
|
207
|
+
const curBar = progressBarMap.get("CURRENT");
|
|
208
|
+
page.on("request", (request) => {
|
|
209
|
+
if (urlMap.size === 0) {
|
|
210
|
+
curBar?.start(1, 0);
|
|
211
|
+
}
|
|
212
|
+
const requestUrl = request.url();
|
|
213
|
+
if (MEODP.isIgnoredLink(requestUrl)) {
|
|
214
|
+
urlMap.set(requestUrl, {
|
|
215
|
+
ignored: true,
|
|
216
|
+
request
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (requestUrl && !urlMap.has(requestUrl)) {
|
|
221
|
+
urlMap.set(requestUrl, {
|
|
222
|
+
request
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
curBar?.setTotal(urlMap.size);
|
|
226
|
+
});
|
|
227
|
+
page.on("response", (response) => {
|
|
228
|
+
const request = response.request();
|
|
229
|
+
const item = urlMap.get(response.url());
|
|
230
|
+
if (!item || item.ignored) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
urlMap.set(response.url(), {
|
|
234
|
+
request,
|
|
235
|
+
response
|
|
236
|
+
});
|
|
237
|
+
const urlInfo = urlMap.get(response.url());
|
|
238
|
+
if (!urlInfo)
|
|
239
|
+
return;
|
|
240
|
+
if (urlInfo.response)
|
|
241
|
+
return;
|
|
242
|
+
respondNum += 1;
|
|
243
|
+
const {
|
|
244
|
+
durationText,
|
|
245
|
+
statusInfo,
|
|
246
|
+
linkText
|
|
247
|
+
} = getFormattedDataFromResponse(response);
|
|
248
|
+
const info = ` [${statusInfo}] ${durationText} ${linkText}`;
|
|
249
|
+
if (response.status() >= 400) {
|
|
250
|
+
MEODP.logger.error(info);
|
|
251
|
+
} else {
|
|
252
|
+
MEODP.logger.info(info);
|
|
253
|
+
}
|
|
254
|
+
log(urlItem.type === "link" ? info.trim() : info);
|
|
255
|
+
curBar?.update(respondNum, {
|
|
256
|
+
site: urlItem.url,
|
|
257
|
+
url: response.url()
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
page.on("requestfailed", (request) => {
|
|
261
|
+
const url = request.url();
|
|
262
|
+
const urlInfo = urlMap.get(url);
|
|
263
|
+
if (!urlInfo?.ignored) {
|
|
264
|
+
urlMap.set(url, {
|
|
265
|
+
request,
|
|
266
|
+
failed: true
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
return urlMap;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function isLink(url) {
|
|
274
|
+
if (url) {
|
|
275
|
+
return url.startsWith("http") || url.startsWith("/");
|
|
276
|
+
} else {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function parseUrlMap(urlMap) {
|
|
282
|
+
const successCount = Array.from(urlMap.values()).filter(
|
|
283
|
+
(value) => value.response && value.response.status() && value.response.status() < 400
|
|
284
|
+
).length;
|
|
285
|
+
const timeoutCount = Array.from(urlMap.values()).filter((value) => !value.response && !value.ignored).length;
|
|
286
|
+
const timeoutTxt = timeoutCount ? colors.redBright(colors.redBright(`(\u{1F6AB} ${timeoutCount} Timeout)`)) : "";
|
|
287
|
+
const failedCount = Array.from(urlMap.values()).filter((value) => {
|
|
288
|
+
return value.failed || value.response && value.response?.status() > 400;
|
|
289
|
+
}).length;
|
|
290
|
+
const ignoredCount = Array.from(urlMap.values()).filter((value) => value.ignored).length;
|
|
291
|
+
const success = {
|
|
292
|
+
count: successCount,
|
|
293
|
+
text: `${COLORFUL_SYMBOLS.success} ${colors.green(`${successCount} OK`)}`
|
|
294
|
+
};
|
|
295
|
+
const timeout = {
|
|
296
|
+
count: timeoutCount,
|
|
297
|
+
text: timeoutTxt
|
|
298
|
+
};
|
|
299
|
+
const failedTxt = `${COLORFUL_SYMBOLS.error} ${failedCount} Failed`;
|
|
300
|
+
const failed = {
|
|
301
|
+
count: failedCount,
|
|
302
|
+
text: `${failedCount ? colors.red(failedTxt) : colors.dim(failedTxt)}`
|
|
303
|
+
};
|
|
304
|
+
const ignored = {
|
|
305
|
+
count: ignoredCount,
|
|
306
|
+
text: colors.dim(`\u{1F47B} ${ignoredCount} Ignored`)
|
|
307
|
+
};
|
|
308
|
+
const total = {
|
|
309
|
+
count: urlMap.size,
|
|
310
|
+
text: `${urlMap.size} Total Requests`
|
|
311
|
+
};
|
|
312
|
+
return {
|
|
313
|
+
success,
|
|
314
|
+
failed,
|
|
315
|
+
/**
|
|
316
|
+
* Ignored links, controlled by `ignoreLinks` and `ignoreExtensions`
|
|
317
|
+
*/
|
|
318
|
+
ignored,
|
|
319
|
+
timeout,
|
|
320
|
+
total
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const emojiMap = {
|
|
325
|
+
site: "\u{1F3E0}",
|
|
326
|
+
link: "\u{1F517}",
|
|
327
|
+
sitemap: "\u{1F4C4}"
|
|
328
|
+
};
|
|
329
|
+
function getMEODPUrlItemInfo(urlItem) {
|
|
330
|
+
const type = typeof urlItem === "string" ? "link" : urlItem.type;
|
|
331
|
+
const url = typeof urlItem === "string" ? urlItem : urlItem.url;
|
|
332
|
+
const emoji = emojiMap[type] || "\u{1F517}";
|
|
333
|
+
return {
|
|
334
|
+
type,
|
|
335
|
+
url,
|
|
336
|
+
emoji
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function createMEODPLogger() {
|
|
341
|
+
return {
|
|
342
|
+
_log: (...args) => {
|
|
343
|
+
const allLogPath = path.resolve(MEODP.logFolder, "all.log");
|
|
344
|
+
LocalLog.log(allLogPath, stripAnsi(args.join(" ")));
|
|
345
|
+
},
|
|
346
|
+
log: (...args) => {
|
|
347
|
+
const content = formatArgs(args);
|
|
348
|
+
consola.log(content);
|
|
349
|
+
MEODP.logger._log(content);
|
|
350
|
+
},
|
|
351
|
+
inner: (...args) => {
|
|
352
|
+
const content = formatArgs(args);
|
|
353
|
+
consola.log(COLORFUL_SYMBOLS.line, " ", content);
|
|
354
|
+
MEODP.logger._log(COLORFUL_SYMBOLS.line, " ", content);
|
|
355
|
+
},
|
|
356
|
+
start: (...args) => {
|
|
357
|
+
const content = formatArgs(args);
|
|
358
|
+
consola.start(content);
|
|
359
|
+
MEODP.logger._log(COLORFUL_SYMBOLS.start, content);
|
|
360
|
+
},
|
|
361
|
+
success: (...args) => {
|
|
362
|
+
const content = formatArgs(args);
|
|
363
|
+
consola.success(content);
|
|
364
|
+
MEODP.logger._log(COLORFUL_SYMBOLS.success, content);
|
|
365
|
+
},
|
|
366
|
+
info: (...args) => {
|
|
367
|
+
const content = formatArgs(args);
|
|
368
|
+
consola.info(content);
|
|
369
|
+
MEODP.logger._log(COLORFUL_SYMBOLS.info, content);
|
|
370
|
+
},
|
|
371
|
+
warn: (...args) => {
|
|
372
|
+
const content = formatArgs(args);
|
|
373
|
+
consola.warn(content);
|
|
374
|
+
MEODP.logger._log(COLORFUL_SYMBOLS.warn, content);
|
|
375
|
+
},
|
|
376
|
+
error: (...args) => {
|
|
377
|
+
const content = formatArgs(args);
|
|
378
|
+
consola.log(COLORFUL_SYMBOLS.error, content);
|
|
379
|
+
MEODP.logger._log(COLORFUL_SYMBOLS.error, content);
|
|
380
|
+
MEODP.wLogger.error(content);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function createWinstonLogger() {
|
|
386
|
+
const allLogPath = path.resolve(MEODP.logFolder, "all.log");
|
|
387
|
+
const errorLogPath = path.resolve(MEODP.logFolder, "error.log");
|
|
388
|
+
const customFormats = [
|
|
389
|
+
winston.format.timestamp({
|
|
390
|
+
format: "YYYY-MM-DD HH:mm:ss"
|
|
391
|
+
})
|
|
392
|
+
// winston.format.colorize(),
|
|
393
|
+
// winston.format.errors({ stack: true }),
|
|
394
|
+
// winston.format.json(),
|
|
395
|
+
// winston.format.simple(),
|
|
396
|
+
// winston.format.uncolorize(),
|
|
397
|
+
];
|
|
398
|
+
customFormats.push(
|
|
399
|
+
winston.format.printf(({ level, message, timestamp, service }) => {
|
|
400
|
+
const content = [
|
|
401
|
+
timestamp,
|
|
402
|
+
`[${service}]`,
|
|
403
|
+
level.toUpperCase(),
|
|
404
|
+
message
|
|
405
|
+
];
|
|
406
|
+
return content.join(" ");
|
|
407
|
+
})
|
|
408
|
+
);
|
|
409
|
+
const customTransports = [
|
|
410
|
+
new transports.File({
|
|
411
|
+
filename: errorLogPath,
|
|
412
|
+
level: "error"
|
|
413
|
+
}),
|
|
414
|
+
new transports.File({
|
|
415
|
+
filename: allLogPath
|
|
416
|
+
// level: 'info',
|
|
417
|
+
})
|
|
418
|
+
];
|
|
419
|
+
return winston.createLogger({
|
|
420
|
+
level: "info",
|
|
421
|
+
format: winston.format.combine(...customFormats),
|
|
422
|
+
defaultMeta: { service: "MEODP" },
|
|
423
|
+
transports: customTransports,
|
|
424
|
+
exitOnError: false
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
var __defProp = Object.defineProperty;
|
|
429
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
430
|
+
var __publicField = (obj, key, value) => {
|
|
431
|
+
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
432
|
+
return value;
|
|
433
|
+
};
|
|
434
|
+
const _MEODP = class _MEODP {
|
|
435
|
+
constructor(config) {
|
|
436
|
+
_MEODP.config = config;
|
|
437
|
+
}
|
|
438
|
+
static async init(config) {
|
|
439
|
+
_MEODP.config = config;
|
|
440
|
+
if (config.clean) {
|
|
441
|
+
console.log();
|
|
442
|
+
consola.start("Cleaning history log...");
|
|
443
|
+
await LocalLog.clean();
|
|
444
|
+
consola.success("Clean history log.");
|
|
445
|
+
console.log();
|
|
446
|
+
}
|
|
447
|
+
await LocalLog.init();
|
|
448
|
+
_MEODP.logFolder = LocalLog.logFolder || path.resolve(process.cwd(), "logs/meodp");
|
|
449
|
+
_MEODP.wLogger = createWinstonLogger();
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* filter link by ignoreLinks
|
|
453
|
+
*/
|
|
454
|
+
static isIgnoredLink(link) {
|
|
455
|
+
const isIgnoredLinks = _MEODP.config.ignoreLinks?.some((ignoreLink) => {
|
|
456
|
+
if (ignoreLink instanceof RegExp) {
|
|
457
|
+
return ignoreLink.test(link);
|
|
458
|
+
} else if (link.startsWith(ignoreLink)) {
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
return false;
|
|
462
|
+
});
|
|
463
|
+
const isIgnoredExtensions = _MEODP.config.ignoreExtensions?.some((ext) => link.endsWith(ext));
|
|
464
|
+
return isIgnoredLinks || isIgnoredExtensions;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* @desc 是否为外链
|
|
468
|
+
*/
|
|
469
|
+
static isExternalLink(url, urlItem) {
|
|
470
|
+
return url.startsWith("http") && !url.startsWith(urlItem.url);
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* @desc 通用日志
|
|
474
|
+
* - 本地日志
|
|
475
|
+
* - winston 日志
|
|
476
|
+
*/
|
|
477
|
+
static log(urlItem, ...args) {
|
|
478
|
+
_MEODP.logger.info(args);
|
|
479
|
+
LocalLog.logByType(urlItem, args.join(" "));
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
/**
|
|
483
|
+
* global config
|
|
484
|
+
*/
|
|
485
|
+
__publicField(_MEODP, "config");
|
|
486
|
+
__publicField(_MEODP, "configFile");
|
|
487
|
+
/**
|
|
488
|
+
* browser instance by playwright
|
|
489
|
+
*/
|
|
490
|
+
__publicField(_MEODP, "browser");
|
|
491
|
+
/**
|
|
492
|
+
* log folder
|
|
493
|
+
* logs/meodp/
|
|
494
|
+
*/
|
|
495
|
+
__publicField(_MEODP, "logFolder");
|
|
496
|
+
__publicField(_MEODP, "logger", createMEODPLogger());
|
|
497
|
+
__publicField(_MEODP, "wLogger");
|
|
498
|
+
let MEODP = _MEODP;
|
|
499
|
+
|
|
500
|
+
async function getBrowser() {
|
|
501
|
+
if (!MEODP.browser) {
|
|
502
|
+
MEODP.browser = await chromium.launch({
|
|
503
|
+
// 15s
|
|
504
|
+
timeout: 15e3
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
return MEODP.browser;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function getSiteLinks(page, options) {
|
|
511
|
+
const links = await page.$$eval("a", (elements) => {
|
|
512
|
+
return elements.map((element) => element.getAttribute("href")).filter(Boolean);
|
|
513
|
+
});
|
|
514
|
+
const filterLinks = links.map((link) => {
|
|
515
|
+
if (!isLink(link)) {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
const isExternalLink = MEODP.isExternalLink(link, options.urlItem);
|
|
519
|
+
if (!isExternalLink) {
|
|
520
|
+
{
|
|
521
|
+
const url = new URL(link, page.url());
|
|
522
|
+
url.search = "";
|
|
523
|
+
return url.toString();
|
|
524
|
+
}
|
|
525
|
+
} else {
|
|
526
|
+
return link;
|
|
527
|
+
}
|
|
528
|
+
}).filter(Boolean);
|
|
529
|
+
return filterLinks;
|
|
530
|
+
}
|
|
531
|
+
async function checkUrlNomoduleAssets(page) {
|
|
532
|
+
const legacyScripts = await page.$$eval("script", (elements) => {
|
|
533
|
+
return elements.filter((element) => element.getAttribute("nomodule") && element.getAttribute("src")).map((el) => el.getAttribute("src"));
|
|
534
|
+
}).catch((e) => {
|
|
535
|
+
consola.error(e);
|
|
536
|
+
}) || [];
|
|
537
|
+
if (!legacyScripts.length)
|
|
538
|
+
return true;
|
|
539
|
+
const browser = await getBrowser();
|
|
540
|
+
const context = await browser.newContext();
|
|
541
|
+
const loadPromises = legacyScripts.map((nomoduleUrl) => {
|
|
542
|
+
return (async () => {
|
|
543
|
+
if (!nomoduleUrl) {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const startTime = Date.now();
|
|
547
|
+
const newPage = await context.newPage();
|
|
548
|
+
const response = await newPage.goto(nomoduleUrl);
|
|
549
|
+
if (!response) {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const {
|
|
553
|
+
statusCode,
|
|
554
|
+
statusInfo,
|
|
555
|
+
linkText
|
|
556
|
+
} = getFormattedDataFromResponse(response);
|
|
557
|
+
const duration = Date.now() - startTime;
|
|
558
|
+
const durationTxt = colors.dim(`\u2502${colors.blue(`${duration.toString().padStart(4, " ")}${colors.white("ms")}`)} \u2502`);
|
|
559
|
+
if (statusCode >= 400) {
|
|
560
|
+
MEODP.logger.inner(" ", COLORFUL_SYMBOLS.error, colors.red(statusInfo), durationTxt, linkText);
|
|
561
|
+
} else {
|
|
562
|
+
MEODP.logger.inner(" ", COLORFUL_SYMBOLS.success, colors.green(statusInfo), durationTxt, linkText);
|
|
563
|
+
}
|
|
564
|
+
await newPage.close();
|
|
565
|
+
return statusCode ? statusCode < 400 : false;
|
|
566
|
+
})();
|
|
567
|
+
});
|
|
568
|
+
const results = await Promise.all(loadPromises);
|
|
569
|
+
return results.every((result) => result);
|
|
570
|
+
}
|
|
571
|
+
async function checkSiteUrl(url, options) {
|
|
572
|
+
const browser = await getBrowser();
|
|
573
|
+
const page = await browser.newPage();
|
|
574
|
+
const bar = progressBarMap.get(options.urlItem.url);
|
|
575
|
+
const startTime = Date.now();
|
|
576
|
+
const isExternalLink = MEODP.isExternalLink(url, options.urlItem);
|
|
577
|
+
const log = LocalLog.createLog(options.urlItem);
|
|
578
|
+
if (!MEODP.config.checkExternalLinks && isExternalLink) {
|
|
579
|
+
let res = null;
|
|
580
|
+
try {
|
|
581
|
+
res = await page.goto(url, {
|
|
582
|
+
waitUntil: "load"
|
|
583
|
+
});
|
|
584
|
+
} catch (e) {
|
|
585
|
+
MEODP.logger.error(e);
|
|
586
|
+
}
|
|
587
|
+
if (!res)
|
|
588
|
+
return;
|
|
589
|
+
const statusCode = res?.status() || 0;
|
|
590
|
+
const checkStatus = statusCode < 400 ? "passed" : "failed";
|
|
591
|
+
siteUrlMap.set(url, {
|
|
592
|
+
response: res,
|
|
593
|
+
statusCode,
|
|
594
|
+
checkStatus
|
|
595
|
+
});
|
|
596
|
+
MEODP.logger.log(COLORFUL_SYMBOLS.line, " ", colors.green(`[${statusCode}]`), colors.cyan(url), colors.dim(`(in ${((Date.now() - startTime) / 1e3).toFixed(2)}s)`));
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const urlMap = registerPageEvents(page, options.urlItem);
|
|
600
|
+
try {
|
|
601
|
+
const res = await page.goto(url, {
|
|
602
|
+
waitUntil: "networkidle"
|
|
603
|
+
});
|
|
604
|
+
const statusCode = res?.status() || 0;
|
|
605
|
+
siteUrlMap.set(url, {
|
|
606
|
+
response: res,
|
|
607
|
+
statusCode,
|
|
608
|
+
checkStatus: "goto"
|
|
609
|
+
});
|
|
610
|
+
const { success, failed, total, ignored, timeout } = parseUrlMap(urlMap);
|
|
611
|
+
const duration = (Date.now() - startTime) / 1e3;
|
|
612
|
+
const statusText = res?.statusText();
|
|
613
|
+
const logInfo = [
|
|
614
|
+
COLORFUL_SYMBOLS.line,
|
|
615
|
+
" ",
|
|
616
|
+
colors.green(`[${statusCode}${statusText ? ` ${statusText}` : ""}]`),
|
|
617
|
+
colors.cyan(url),
|
|
618
|
+
colors.dim(`(in ${duration}s)`),
|
|
619
|
+
total.text,
|
|
620
|
+
success.text,
|
|
621
|
+
failed.text,
|
|
622
|
+
timeout.text,
|
|
623
|
+
ignored.text
|
|
624
|
+
];
|
|
625
|
+
MEODP.logger.log(...logInfo);
|
|
626
|
+
log(...logInfo);
|
|
627
|
+
log();
|
|
628
|
+
if (MEODP.config.log?.type === "progress") {
|
|
629
|
+
bar?.update(success.count, {
|
|
630
|
+
value: colors.green(success.count),
|
|
631
|
+
error_count: failed.count ? colors.red(failed.count) : 0
|
|
632
|
+
});
|
|
633
|
+
} else {
|
|
634
|
+
}
|
|
635
|
+
} catch (e) {
|
|
636
|
+
MEODP.logger.error(e);
|
|
637
|
+
}
|
|
638
|
+
for (const [url2, info] of urlMap) {
|
|
639
|
+
if (!info.response && !info.ignored) {
|
|
640
|
+
MEODP.logger.log(COLORFUL_SYMBOLS.line, " ", COLORFUL_SYMBOLS.error, `Timeout: ${colors.underline(url2)}`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
let noModuleAssetsPassed = true;
|
|
644
|
+
if (options.checkNoModule) {
|
|
645
|
+
noModuleAssetsPassed = await checkUrlNomoduleAssets(page);
|
|
646
|
+
}
|
|
647
|
+
const urlMapValues = Array.from(urlMap.values());
|
|
648
|
+
const siteUrlItem = siteUrlMap.get(url);
|
|
649
|
+
if (siteUrlItem) {
|
|
650
|
+
if (urlMapValues.every((value) => value.response?.status() && value.response?.status() < 400) && noModuleAssetsPassed) {
|
|
651
|
+
siteUrlItem.checkStatus = "passed";
|
|
652
|
+
const successCount = Array.from(siteUrlMap.values()).filter((value) => value.checkStatus === "passed").length;
|
|
653
|
+
bar?.update(successCount);
|
|
654
|
+
} else {
|
|
655
|
+
siteUrlItem.checkStatus = "failed";
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (!isExternalLink) {
|
|
659
|
+
const queue = PQueueMap.get(url);
|
|
660
|
+
if (queue) {
|
|
661
|
+
const links = await getSiteLinks(page, options);
|
|
662
|
+
for (const link of links) {
|
|
663
|
+
if (!siteUrlMap.has(link)) {
|
|
664
|
+
siteUrlMap.set(link, {
|
|
665
|
+
statusCode: 0,
|
|
666
|
+
checkStatus: "pending"
|
|
667
|
+
});
|
|
668
|
+
bar?.setTotal(siteUrlMap.size);
|
|
669
|
+
const url2 = new URL(link, page.url()).href;
|
|
670
|
+
await queue.add(async () => {
|
|
671
|
+
await checkSiteUrl(url2, options);
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
await page.close();
|
|
678
|
+
}
|
|
679
|
+
async function checkSite(props) {
|
|
680
|
+
const { url } = props;
|
|
681
|
+
const queue = new PQueue({
|
|
682
|
+
concurrency: MEODP.config.concurrency
|
|
683
|
+
});
|
|
684
|
+
PQueueMap.set(url, queue);
|
|
685
|
+
const bar = progressBarMap.get(url);
|
|
686
|
+
const browser = await getBrowser();
|
|
687
|
+
const context = await browser.newContext();
|
|
688
|
+
const page = await context.newPage();
|
|
689
|
+
const homeUrl = url;
|
|
690
|
+
siteUrlMap.set(homeUrl, {
|
|
691
|
+
statusCode: 0,
|
|
692
|
+
checkStatus: "pending"
|
|
693
|
+
});
|
|
694
|
+
bar?.setTotal(siteUrlMap.size);
|
|
695
|
+
await checkSiteUrl(homeUrl, {
|
|
696
|
+
urlItem: props,
|
|
697
|
+
checkNoModule: true
|
|
698
|
+
});
|
|
699
|
+
await queue.onIdle();
|
|
700
|
+
await page.close();
|
|
701
|
+
await context.close();
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
async function checkLink(urlItem) {
|
|
705
|
+
const browser = await getBrowser();
|
|
706
|
+
const context = await browser.newContext();
|
|
707
|
+
const page = await context.newPage();
|
|
708
|
+
const startTime = Date.now();
|
|
709
|
+
const { url } = urlItem;
|
|
710
|
+
const bar = progressBarMap.get(url);
|
|
711
|
+
const urlMap = registerPageEvents(page, urlItem);
|
|
712
|
+
const res = await page.goto(url, {
|
|
713
|
+
waitUntil: "networkidle"
|
|
714
|
+
});
|
|
715
|
+
const isHtml = res?.headers()["content-type"].includes("text/html");
|
|
716
|
+
if (isHtml) {
|
|
717
|
+
const { success, failed, total, ignored, timeout } = parseUrlMap(urlMap);
|
|
718
|
+
const duration = (Date.now() - startTime) / 1e3;
|
|
719
|
+
const statusCode = res?.status() || 0;
|
|
720
|
+
const statusText = res?.statusText();
|
|
721
|
+
const logInfo = [
|
|
722
|
+
COLORFUL_SYMBOLS.line,
|
|
723
|
+
" ",
|
|
724
|
+
colors.green(`[${statusCode}${statusText ? ` ${statusText}` : ""}]`),
|
|
725
|
+
colors.cyan(url),
|
|
726
|
+
total.text,
|
|
727
|
+
colors.dim(`(in ${duration}s)`),
|
|
728
|
+
success.text,
|
|
729
|
+
failed.text,
|
|
730
|
+
timeout.text,
|
|
731
|
+
ignored.text
|
|
732
|
+
];
|
|
733
|
+
MEODP.logger.log(...logInfo);
|
|
734
|
+
bar?.setTotal(urlMap.size);
|
|
735
|
+
bar?.update(success.count);
|
|
736
|
+
} else {
|
|
737
|
+
bar?.update(1);
|
|
738
|
+
}
|
|
739
|
+
await checkUrlNomoduleAssets(page);
|
|
740
|
+
await page.close();
|
|
741
|
+
await context.close();
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async function checkMEODPUrl(meodpUrl) {
|
|
745
|
+
if (typeof meodpUrl === "string") {
|
|
746
|
+
meodpUrl = {
|
|
747
|
+
type: "link",
|
|
748
|
+
url: meodpUrl
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
const { type, url, emoji } = getMEODPUrlItemInfo(meodpUrl);
|
|
752
|
+
MEODP.logger.log();
|
|
753
|
+
MEODP.logger.start(`${emoji} ${colors.yellow(`[${type}]`)} ${colors.cyan(url)}`);
|
|
754
|
+
const startTime = Date.now();
|
|
755
|
+
if (typeof meodpUrl === "object") {
|
|
756
|
+
switch (meodpUrl.type) {
|
|
757
|
+
case "site":
|
|
758
|
+
await checkSite(meodpUrl);
|
|
759
|
+
break;
|
|
760
|
+
case "link":
|
|
761
|
+
await checkLink(meodpUrl);
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
const duration = Date.now() - startTime;
|
|
766
|
+
const bar = progressBarMap.get(meodpUrl.url);
|
|
767
|
+
bar?.stop();
|
|
768
|
+
MEODP.logger.success(`Done in ${duration / 1e3}s.`);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const defaultMEODPConfig = {
|
|
772
|
+
urls: [],
|
|
773
|
+
concurrency: 10,
|
|
774
|
+
log: {
|
|
775
|
+
type: "raw",
|
|
776
|
+
file: true
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
function defineConfig(config) {
|
|
780
|
+
return config;
|
|
781
|
+
}
|
|
782
|
+
async function runByConfig(config) {
|
|
783
|
+
const { urls, debug } = config;
|
|
784
|
+
consola.level = debug ? 5 : 3;
|
|
785
|
+
const queue = new PQueue({
|
|
786
|
+
concurrency: config.concurrency
|
|
787
|
+
});
|
|
788
|
+
if (config.log?.type === "progress") {
|
|
789
|
+
for (const urlItem of urls) {
|
|
790
|
+
const { type, url, emoji } = getMEODPUrlItemInfo(urlItem);
|
|
791
|
+
const bar = multiBar.create(1, 0, {
|
|
792
|
+
emoji,
|
|
793
|
+
url,
|
|
794
|
+
type,
|
|
795
|
+
error_count: 0
|
|
796
|
+
});
|
|
797
|
+
progressBarMap.set(url, bar);
|
|
798
|
+
}
|
|
799
|
+
const curBar = multiBar.create(1, 0, {
|
|
800
|
+
name: "CURRENT"
|
|
801
|
+
});
|
|
802
|
+
progressBarMap.set("CURRENT", curBar);
|
|
803
|
+
}
|
|
804
|
+
const browser = await getBrowser();
|
|
805
|
+
for (const urlItem of urls) {
|
|
806
|
+
await queue.add(async () => {
|
|
807
|
+
await checkMEODPUrl(urlItem);
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
await queue.onIdle();
|
|
811
|
+
if (config.log?.type === "progress") {
|
|
812
|
+
multiBar.stop();
|
|
813
|
+
}
|
|
814
|
+
await browser.close();
|
|
815
|
+
consola.debug("browser closed");
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
export { LocalLog as L, MEODP as M, PQueueMap as P, checkLink as a, getSiteLinks as b, checkMEODPUrl as c, checkUrlNomoduleAssets as d, checkSiteUrl as e, checkSite as f, getBrowser as g, defaultMEODPConfig as h, defineConfig as i, localLog as j, createMEODPLogger as k, logUrlItemInfo as l, createWinstonLogger as m, multiBar as n, registerPageEvents as o, progressBarMap as p, formatArgs as q, runByConfig as r, siteUrlMap as s, getFormattedDataFromResponse as t, isLink as u, parseUrlMap as v, emojiMap as w, getMEODPUrlItemInfo as x };
|