#!/usr/bin/env node
'use strict';
/**
* Vue SFC 结构与脚本语法校验。
*
* audit-project.js 只做交叉引用检查,不解析 `;
const goodBlocks = splitSFC(good);
check('正常 SFC 能拆出 template', goodBlocks.template.length, 1);
check('正常 SFC 能拆出 script', goodBlocks.script.length, 1);
check('正常脚本语法通过', checkScriptSyntax(goodBlocks.script[0].body, 'good'), null);
// 语法错误的脚本必须被抓到
const bad = `
`;
const badBlocks = splitSFC(bad);
check('错误脚本语法被识别', checkScriptSyntax(badBlocks.script[0].body, 'bad') !== null, true);
// 空 script 应被识别
const empty = ``;
check('空 script 被识别', splitSFC(empty).script[0].body.trim().length, 0);
// 自闭合 template 不应被当成块
const selfClosed = ``;
check('自闭合 template 不计入块', splitSFC(selfClosed).template.filter((b) => b.body.includes('')).length, 0);
// import/export 处理:多行、字符串内含关键字、注释内含关键字
const multiImport = `
import {
a,
b
} from '@/x'
import y from '@/y';
export default {
data() {
return { a, b, y }
}
}`;
check('多行 import 被正确剥离', checkScriptSyntax(multiImport, 'multi'), null);
const trickyStrings = `
const s = "import { a } from 'fake'"
const t = 'export default nope'
const u = \`import x from "y"\`
export default { data(){ return { s, t, u } } }`;
check('字符串中的 import/export 不误伤', checkScriptSyntax(trickyStrings, 'tricky'), null);
const trickyComments = `
// import fake from 'nope'
/* export default alsoFake */
export default { data(){ return { ok: 1 } } }`;
check('注释中的 import/export 不误伤', checkScriptSyntax(trickyComments, 'comment'), null);
const condCompile = `
export default {
methods: {
init() {
// #ifdef MP-TOUTIAO
let info = tt.getRect()
// #endif
// #ifndef MP-TOUTIAO
let info = uni.getRect()
// #endif
}
}
}`;
check('条件编译分支不误报重复声明', checkScriptSyntax(condCompile, 'cond'), null);
// 行号必须保持:剥离 import 后不应改变后续代码的行位置
const lineKeep = `import a from 'b'\nimport c from 'd'\nconst boom = )`;
const errMsg = checkScriptSyntax(lineKeep, 'linekeep');
check('语法错误仍然能被发现', errMsg !== null, true);
// 生命周期钩子缩进判定:页面级(与 methods 同级)不应报错
const pageLevelHook = `export default {
methods: {
loadMore() {}
},
onReachBottom() {
this.loadMore()
}
}`;
check('页面级 onReachBottom 不误报', indentCheck(pageLevelHook, 'onReachBottom'), false);
// 嵌在 methods 里应当报错
const nestedHook = `export default {
methods: {
onReachBottom() {
this.loadMore()
}
}
}`;
check('methods 内的 onReachBottom 被识别', indentCheck(nestedHook, 'onReachBottom'), true);
return cases;
}
/** 复用主流程的缩进判定逻辑,供自测使用 */
function indentCheck(body, hook) {
const methodsMatch = /^[ \t]*methods\s*:/m.exec(body);
if (!methodsMatch) return false;
const methodsIndent = /^([ \t]*)/.exec(methodsMatch[0])[1].length;
const m = new RegExp(`^([ \\t]*)${hook}\\s*\\(`, 'm').exec(body);
if (!m) return false;
return m[1].length > methodsIndent;
}
function main() {
const files = walk(ROOT);
if (SELFTEST) {
const cases = selfTest();
console.log('');
console.log('Vue 校验器自测:');
for (const c of cases) {
console.log(` ${c.ok ? '✓' : '✗'} ${c.label}${c.ok ? '' : `(期望 ${c.expected},实得 ${c.actual})`}`);
}
const failed = cases.filter((c) => !c.ok).length;
console.log('');
console.log(failed ? `✗ ${failed} 项失败` : '✓ 校验器工作正常');
return failed ? 1 : 0;
}
for (const f of files) checkFile(f);
if (JSON_OUT) {
console.log(JSON.stringify({ ok: errors.length === 0, errors, warnings, scanned: files.length }, null, 2));
return errors.length ? 1 : 0;
}
console.log('');
console.log('═══════════════════════════════════════════════');
console.log(' Vue SFC 校验报告');
console.log('═══════════════════════════════════════════════');
console.log(` 扫描 ${files.length} 个 .vue 文件\n`);
const fmt = (list) =>
list.map((x) => ` ${x.file}:${x.line}\n ▸ ${x.msg}`).join('\n');
if (errors.length) {
console.log(`✗ 错误 ${errors.length} 项`);
console.log(fmt(errors));
console.log('');
}
if (warnings.length) {
console.log(`⚠ 警告 ${warnings.length} 项`);
console.log(fmt(warnings));
console.log('');
}
if (!errors.length && !warnings.length) console.log('✓ 全部通过\n');
console.log(`结论: ${errors.length} 错误 / ${warnings.length} 警告`);
console.log('═══════════════════════════════════════════════');
return errors.length ? 1 : 0;
}
process.exit(main());