feat: 分类锚点&质量检查&依赖治理
- 分类生成唯一 slug,模板/子菜单/滚动/扩展定位统一使用 data-id - lint 覆盖 src/scripts/test,CI 增量格式检查 - 清理冗余依赖,升级 esbuild,overrides 修复审计项 - 补充单测并更新修复清单
This commit is contained in:
131
scripts/format-check-changed.js
Normal file
131
scripts/format-check-changed.js
Normal file
@@ -0,0 +1,131 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
|
||||
function runGit(args, cwd) {
|
||||
return execFileSync('git', args, { cwd, encoding: 'utf8' }).trim();
|
||||
}
|
||||
|
||||
function tryReadGithubEvent(eventPath) {
|
||||
if (!eventPath) return null;
|
||||
try {
|
||||
const raw = fs.readFileSync(eventPath, 'utf8');
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isAllZerosSha(value) {
|
||||
return typeof value === 'string' && /^0{40}$/.test(value);
|
||||
}
|
||||
|
||||
function getDiffRangeFromGithubEvent(event) {
|
||||
if (!event || typeof event !== 'object') return null;
|
||||
|
||||
if (event.pull_request && event.pull_request.base && event.pull_request.head) {
|
||||
const base = event.pull_request.base.sha;
|
||||
const head = event.pull_request.head.sha;
|
||||
if (base && head) return { base, head };
|
||||
}
|
||||
|
||||
if (event.before && (event.after || event.head_commit)) {
|
||||
const base = event.before;
|
||||
const head = event.after || (event.head_commit && event.head_commit.id);
|
||||
if (base && head && !isAllZerosSha(base)) return { base, head };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectChangedFiles(repoRoot, range) {
|
||||
if (!range) return [];
|
||||
const output = runGit(
|
||||
['diff', '--name-only', '--diff-filter=ACMR', `${range.base}..${range.head}`],
|
||||
repoRoot
|
||||
);
|
||||
return output
|
||||
? output
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
}
|
||||
|
||||
function collectWorkingTreeChangedFiles(repoRoot) {
|
||||
const files = new Set();
|
||||
const unstaged = runGit(['diff', '--name-only', '--diff-filter=ACMR', 'HEAD'], repoRoot);
|
||||
const staged = runGit(['diff', '--cached', '--name-only', '--diff-filter=ACMR'], repoRoot);
|
||||
|
||||
[unstaged, staged].forEach((block) => {
|
||||
if (!block) return;
|
||||
block
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.forEach((filePath) => files.add(filePath));
|
||||
});
|
||||
|
||||
return Array.from(files).sort();
|
||||
}
|
||||
|
||||
function shouldCheckFile(filePath) {
|
||||
const normalized = filePath.split(path.sep).join('/');
|
||||
|
||||
if (normalized === 'package-lock.json') return false;
|
||||
|
||||
// 这两个文件历史上未统一为 Prettier 风格;避免为了启用检查产生巨量格式化 diff
|
||||
if (normalized === 'src/generator.js' || normalized === 'src/script.js') return false;
|
||||
|
||||
// 与现有 npm scripts 的检查范围对齐:不检查 docs/ 与 templates/
|
||||
const allowedRoots = ['src/', 'scripts/', 'test/', '.github/', 'config/'];
|
||||
const isRootFile = !normalized.includes('/');
|
||||
const hasAllowedRoot = allowedRoots.some((prefix) => normalized.startsWith(prefix));
|
||||
|
||||
const isAllowedPath =
|
||||
hasAllowedRoot || (isRootFile && (normalized.endsWith('.md') || normalized.endsWith('.json')));
|
||||
|
||||
if (!isAllowedPath) return false;
|
||||
|
||||
const ext = path.extname(normalized).toLowerCase();
|
||||
return ['.js', '.json', '.md', '.yml', '.yaml'].includes(ext);
|
||||
}
|
||||
|
||||
function resolvePrettierBin(repoRoot) {
|
||||
const base = path.join(repoRoot, 'node_modules', '.bin', 'prettier');
|
||||
if (fs.existsSync(base)) return base;
|
||||
if (fs.existsSync(`${base}.cmd`)) return `${base}.cmd`;
|
||||
return null;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
|
||||
const event = tryReadGithubEvent(process.env.GITHUB_EVENT_PATH);
|
||||
const range = getDiffRangeFromGithubEvent(event);
|
||||
|
||||
const candidateFiles = range
|
||||
? collectChangedFiles(repoRoot, range)
|
||||
: collectWorkingTreeChangedFiles(repoRoot);
|
||||
|
||||
const filesToCheck = candidateFiles.filter(shouldCheckFile);
|
||||
|
||||
if (filesToCheck.length === 0) {
|
||||
console.log('格式检查:未发现需要检查的文件,跳过。');
|
||||
return;
|
||||
}
|
||||
|
||||
const prettierBin = resolvePrettierBin(repoRoot);
|
||||
if (!prettierBin) {
|
||||
console.error('格式检查失败:未找到 prettier,可先运行 npm ci / npm install。');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`格式检查:共 ${filesToCheck.length} 个文件`);
|
||||
filesToCheck.forEach((filePath) => console.log(`- ${filePath}`));
|
||||
|
||||
execFileSync(prettierBin, ['--check', ...filesToCheck], { cwd: repoRoot, stdio: 'inherit' });
|
||||
}
|
||||
|
||||
main();
|
||||
67
scripts/lint.js
Normal file
67
scripts/lint.js
Normal file
@@ -0,0 +1,67 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
|
||||
function collectJsFiles(rootDir) {
|
||||
const files = [];
|
||||
|
||||
const walk = (currentDir) => {
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
entries.forEach((entry) => {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === 'node_modules' || entry.name === 'dist') return;
|
||||
walk(fullPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.isFile() && entry.name.endsWith('.js')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
walk(rootDir);
|
||||
return files;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const projectRoot = path.resolve(__dirname, '..');
|
||||
const targetDirs = ['src', 'scripts', 'test'].map((dir) => path.join(projectRoot, dir));
|
||||
|
||||
const jsFiles = targetDirs.flatMap((dir) => collectJsFiles(dir)).sort();
|
||||
|
||||
if (jsFiles.length === 0) {
|
||||
console.log('未发现需要检查的 .js 文件,跳过。');
|
||||
return;
|
||||
}
|
||||
|
||||
let hasError = false;
|
||||
jsFiles.forEach((filePath) => {
|
||||
const relativePath = path.relative(projectRoot, filePath);
|
||||
try {
|
||||
execFileSync(process.execPath, ['--check', filePath], { stdio: 'inherit' });
|
||||
} catch (error) {
|
||||
hasError = true;
|
||||
console.error(`\n语法检查失败:${relativePath}`);
|
||||
if (error && error.status) {
|
||||
console.error(`退出码:${error.status}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (hasError) {
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(`语法检查通过:${jsFiles.length} 个文件`);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user