6.7.0 - Alpha6 - 重构 scrapers 工具并增加中英双语注释内容

This commit is contained in:
SuperMonster003
2025-09-24 16:01:46 +08:00
parent ef076d1cf8
commit c1ee107bc3
25 changed files with 1512 additions and 1072 deletions

View File

@@ -1,55 +1,67 @@
// run-scrapers.mjs
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
/**
* @typedef {Object} ScriptItem
* @property {string} name
* @property {string} abs
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as fs from 'node:fs/promises';
import * as readline from 'node:readline';
import { fileURLToPath } from 'node:url';
import { spawn } from 'node:child_process';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 待执行脚本 (顺序可按需调整)
const SCRIPT_LIST = [
'scrape-and-update-readme-template-contributors-table.mjs',
'scrape-and-update-foojay-resolver-version.mjs',
'scrape-and-inject-latest-gradle-wrapper.mjs',
'scrape-and-inject-agp-releases.mjs',
'scrape-and-inject-android-studio-agp-version-map.mjs',
'scrape-and-inject-android-studio-codename_maps.mjs',
'scrape-and-inject-embedded-kotlin-list.mjs',
'scrape-and-inject-gradle-kotlin-compatibility-list.mjs',
'scrape-and-inject-ksp-releases.mjs',
'scrape-and-inject-agp-gradle-compatibility-list.mjs',
'scrape-and-inject-java-gradle-compatibility-list.mjs',
'scrape-and-inject-rhino-engine-data.mjs',
'scrape-and-update-readme-template-contributors-table.mjs',
];
const childProcessOutput = [];
/**
* 解析 CLI 参数.
*
* @param {string[]} [argv=process.argv.slice(2)]
* @return {{ continueOnError: boolean, dryRun: boolean, nodePath: string, filters: string[] }}
* @param {ScriptItem[]} scripts
* @param {number} idx
* @returns {string}
*/
function parseArgs(argv = process.argv.slice(2)) {
function getScriptProgressMessage(scripts, idx) {
return `[${idx + 1}/${scripts.length}] ${scripts[idx].name}`;
}
/**
* Parse CLI arguments.<br>
* zh-CN: 解析 CLI 参数.
*
* @returns {{ nodePath: string }}
*/
function parseArgs() {
const argv = process.argv.slice(2);
const opts = {
continueOnError: false,
dryRun: false,
nodePath: process.execPath, // 使用当前 Node 可执行文件, 避免 PATH 问题
filters: [],
nodePath: process.execPath,
};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--continue-on-error') opts.continueOnError = true;
else if (a === '--dry-run') opts.dryRun = true;
else if (a === '--node') opts.nodePath = argv[++i];
else if (a === '--filter') opts.filters.push(argv[++i]);
else if (a.startsWith('--filter=')) opts.filters.push(a.split('=').slice(1).join('='));
else if (a.startsWith('--node=')) opts.nodePath = a.split('=').slice(1).join('=');
if (a === '--node') {
opts.nodePath = argv[++i];
} else if (a.startsWith('--node=')) {
opts.nodePath = a.split('=').slice(1).join('=');
}
}
return opts;
}
/**
* @param {number} ms
* @return {string}
* @returns {string}
*/
function formatDuration(ms) {
const sec = Math.floor(ms / 1000);
@@ -59,11 +71,11 @@ function formatDuration(ms) {
/**
* @param {import('fs').PathLike} filePath
* @return {Promise<boolean>}
* @returns {boolean}
*/
async function ensureExists(filePath) {
function ensureExists(filePath) {
try {
await fs.access(filePath);
fs.accessSync(filePath);
return true;
} catch {
return false;
@@ -75,23 +87,29 @@ async function ensureExists(filePath) {
* @param {string} options.nodePath
* @param {string} options.scriptPath
* @param {string | URL | undefined} options.cwd
* @return {Promise<{ code: number, signal: NodeJS.Signals | null, ms: number, error?: Error }>}
* @returns {Promise<{ code: number, signal: NodeJS.Signals | null, ms: number, error?: Error }>}
*/
async function runOne({ nodePath, scriptPath, cwd }) {
return new Promise((resolve) => {
const start = Date.now();
const child = spawn(nodePath, [ scriptPath ], {
cwd,
stdio: 'inherit', // 直接把子进程的输出打到当前控制台
// Capture child process output.
// zh-CN: 捕获子进程输出.
stdio: [ 'inherit', 'pipe', 'pipe' ],
env: process.env,
});
child.stdout.on('data', (chunk) => {
childProcessOutput.push(chunk);
});
child.stderr.on('data', (chunk) => {
process.stderr.write(chunk);
});
child.on('close', (code, signal) => {
const end = Date.now();
resolve({
code: code ?? 0,
signal: signal ?? null,
ms: end - start,
});
resolve({ code: code ?? 0, signal: signal ?? null, ms: end - start });
});
child.on('error', (err) => {
const end = Date.now();
@@ -103,69 +121,106 @@ async function runOne({ nodePath, scriptPath, cwd }) {
async function main() {
const opts = parseArgs();
const utilsDir = __dirname; // 运行器位于 .utils
const scripts = SCRIPT_LIST
.map(name => ({ name, abs: path.resolve(utilsDir, name) }))
.filter(s => opts.filters.length === 0 || opts.filters.some(f => s.name.includes(f)));
const utilsDir = path.dirname(fileURLToPath(import.meta.url));
/** @type {ScriptItem[]} */
const scripts = SCRIPT_LIST.map((name) => {
const abs = path.resolve(utilsDir, name);
if (!ensureExists(abs)) {
throw new Error(`File not found: ${abs}`);
}
return { name, abs };
});
console.log('\n============================================================');
console.log(' Running scrapers in sequence (Node ESM)');
console.log(` UTILS_DIR = ${utilsDir}`);
console.log(` NODE_EXE = ${opts.nodePath}`);
if (opts.filters.length) console.log(` FILTERS = ${opts.filters.join(', ')}`);
console.log('============================================================\n');
const title = 'Running scrapers in sequence (Node ESM)';
const exhibition = {
UTILS_DIR: utilsDir,
NODE_EXE: opts.nodePath,
};
const exhibitionItems = (/* @IIFE */ () => {
const maxKeyLength = Math.max(...Object.keys(exhibition).map(k => k.length));
return Object.entries(exhibition).map(([ key, value ]) => {
return ` ${key.padEnd(maxKeyLength)} : ${value}`;
});
})();
const lineLength = Math.min(Math.max(
title.length,
...exhibitionItems.map(s => s.length - 1),
) + 2, process.stdout.columns || 80);
const lineDouble = '='.repeat(lineLength);
const lineSingle = '-'.repeat(lineLength);
console.log('\n');
console.log(lineDouble);
console.log(` ${title}`);
console.log(lineSingle);
console.log(exhibitionItems.join('\n'));
console.log(lineDouble);
console.log('\n');
if (scripts.length === 0) {
console.log('No scripts to run after filtering.');
return process.exit(0);
}
// 检查存在性
const finalScripts = [];
for (const s of scripts) {
if (await ensureExists(s.abs)) {
finalScripts.push(s);
} else {
console.log(`File not found: ${s.name}`);
}
}
if (finalScripts.length === 0) {
console.log('No existing scripts to run.');
return process.exit(0);
}
if (opts.dryRun) {
console.log('The following scripts would run in order:');
finalScripts.forEach((s, i) => console.log(` (${i + 1}/${finalScripts.length}) ${s.name}`));
return process.exit(0);
const results = [];
for (let i = 0; i < scripts.length; i++) {
const script = scripts[i];
const startLine = getScriptProgressMessage(scripts, i);
process.stdout.write(`\r${startLine}\n`);
const res = await runOne({ nodePath: opts.nodePath, scriptPath: script.abs, cwd: utilsDir });
const endLine = `${startLine} (${formatDuration(res.ms)})`;
if (res.code === 0) {
childProcessOutput.forEach((chunk) => {
process.stdout.write(chunk);
});
readline.moveCursor(process.stdout, 0, -childProcessOutput.length - 1);
process.stdout.write(`\r${endLine}`);
readline.moveCursor(process.stdout, 0, childProcessOutput.length + 2);
process.stdout.write('\r');
results.push({ ...res, name: script.name });
childProcessOutput.splice(0, childProcessOutput.length);
} else {
readline.moveCursor(process.stdout, 0, -childProcessOutput.length - 1);
process.stdout.write(`\r${endLine} [code: ${res.code}]`);
process.stdout.write('\n');
results.push({ ...res, name: script.name });
break;
}
}
const results = [];
for (let i = 0; i < finalScripts.length; i++) {
const s = finalScripts[i];
console.log(`[${i + 1}/${finalScripts.length}] ${s.name}`);
const res = await runOne({ nodePath: opts.nodePath, scriptPath: s.abs, cwd: utilsDir });
if (res.code === 0) {
console.log(`[Duration] ${formatDuration(res.ms)}\n`);
} else {
console.log(`[Duration] ${formatDuration(res.ms)} | [Exit Code] ${res.code}\n`);
results.push({ ...res, name: s.name });
if (!opts.continueOnError) break;
continue;
}
results.push({ ...res, name: s.name });
}
process.stdout.write('\n');
const failed = results.filter(r => r.code !== 0);
console.log('============================================================');
if (failed.length === 0) {
console.log(' All tasks completed successfully.');
console.log('============================================================\n');
const title = 'All tasks completed successfully';
const lineLength = Math.min(Math.max(title.length) + 1, process.stdout.columns || 80);
const line = '='.repeat(lineLength);
console.log(line);
console.log(` ${title}`);
console.log(line);
process.stdout.write('\n');
process.exit(0);
} else {
const title = `${failed.length} task(s) failed`;
const messages = failed.map(r => {
return ` - ${r.name} (${formatDuration(r.ms)}) [code: ${r.code}]`;
});
const lineLength = Math.min(Math.max(
title.length,
...messages.map(s => s.length - 1),
) + 2, process.stdout.columns || 80);
const lineDouble = '='.repeat(lineLength);
const lineSingle = '-'.repeat(lineLength);
console.log(lineDouble);
console.log(` ${failed.length} task(s) failed:`);
failed.forEach(r => console.log(` - ${r.name} (code ${r.code}, ${formatDuration(r.ms)})`));
console.log('============================================================\n');
console.log(lineSingle);
console.log(messages.join('\n'));
console.log(lineDouble);
process.stdout.write('\n');
process.exit(1);
}
}
@@ -173,4 +228,4 @@ async function main() {
main().catch(err => {
console.error(err);
process.exit(1);
});
});