#!/usr/bin/env node /** * 全站 .vue 三段编译校验(模板 / 样式 / 脚本) * * 覆盖三类真实会打断 HBuilderX 打包的错误: * 1) 模板:表达式里未加引号的 `#hex`、可选链 `?.`、`:class` 带参方法调用等 → * vue-template-compiler 编译即失败(小程序侧表现为白板) * 2) 样式:引用了 uni.scss 里不存在的 `$var` → dart-sass 报 Undefined variable * 3) 脚本:语法错误 / 截断 → node --check 失败 * * 用法: * node scripts/verify-vue-compile.js # 全量 * node scripts/verify-vue-compile.js a.vue b.vue # 指定文件 * 退出码: 0 全通过 / 1 有失败项 */ const fs = require('fs') const path = require('path') const cp = require('child_process') const os = require('os') const ROOT = path.resolve(__dirname, '..') const SKIP_DIRS = new Set(['node_modules', 'dist', 'unpackage', '.git', 'dist_bak_1792213156', '.tmp_equiv']) const SKIP_FILES = new Set(['components/mp-html/node/node.vue']) // 第三方渲染器:自定义标签 + 双 script 结构 let sass = null try { sass = require(path.join(ROOT, 'node_modules/sass')) } catch (e) { sass = null } let compiler = null try { compiler = require(path.join(ROOT, 'node_modules/vue-template-compiler')) } catch (e) { compiler = null } const uniScss = fs.readFileSync(path.join(ROOT, 'uni.scss'), 'utf8') function collect(dir, out = []) { for (const name of fs.readdirSync(dir)) { if (SKIP_DIRS.has(name)) continue const p = path.join(dir, name) const st = fs.statSync(p) if (st.isDirectory()) collect(p, out) else if (name.endsWith('.vue')) out.push(p) } return out } function blocks(src) { // SFC 顶层块:标签必须位于行首(避免误命中脚本里的 // 正则字面量) const out = { template: null, styles: [], scripts: [] } const tpl = src.match(/^]*>([\s\S]*?)^<\/template>/m) if (tpl) out.template = tpl[1] for (const m of src.matchAll(/^]*)>([\s\S]*?)^<\/style>/gm)) out.styles.push({ attrs: m[1] || '', code: m[2] || '' }) for (const m of src.matchAll(/^]*)>([\s\S]*?)^<\/script>/gm)) out.scripts.push({ attrs: m[1] || '', code: m[2] || '' }) return out } const args = process.argv.slice(2) const files = args.length ? args.map(a => path.resolve(ROOT, a)) : collect(ROOT) const failures = [] function fail(file, kind, msg) { failures.push({ file, kind, msg: String(msg).replace(/\s+/g, ' ').slice(0, 220) }) } let tplOk = 0, scssOk = 0, jsOk = 0 for (const abs of files) { const rel = path.relative(ROOT, abs).split(path.sep).join('/') if (SKIP_FILES.has(rel)) continue const src = fs.readFileSync(abs, 'utf8') const sfc = blocks(src) // 1) 模板 if (compiler) { const tpl = sfc.template || '' if (tpl) { const res = compiler.compile(tpl) if (res.errors && res.errors.length) fail(rel, 'template', res.errors.join(' | ')) else tplOk++ } else tplOk++ } // 2) 样式(SCSS 优先把 uni.scss 变量注入) if (sass) { const styleBlocks = sfc.styles let styleFailed = false for (const b of styleBlocks) { const attrs = b.attrs || '' const code = b.code || '' if (!/lang\s*=\s*["']scss["']/.test(attrs) && !/\$[a-zA-Z_-]/.test(code)) continue if (!/lang\s*=\s*["']scss["']/.test(attrs) && !/\$[a-zA-Z_-]/.test(code)) continue const attempts = [uniScss + '\n' + code, code] let ok = false let lastErr = null for (const candidate of attempts) { try { sass.compileString(candidate, { loadPaths: [ROOT], quietDeps: true, logger: { warn() {} } }) ok = true break } catch (e) { lastErr = e } } if (!ok) { styleFailed = true fail(rel, 'style', lastErr && lastErr.message ? lastErr.message.split('\n')[0] : 'compile failed') } } if (!styleFailed) scssOk++ } // 3) 脚本 const scriptBlocks = sfc.scripts let jsFailed = false for (const b of scriptBlocks) { const tmp = path.join(os.tmpdir(), 'verify_' + Math.random().toString(36).slice(2) + '.js') fs.writeFileSync(tmp, b.code, 'utf8') try { cp.execFileSync(process.execPath, ['--check', tmp], { stdio: 'pipe' }) } catch (e) { jsFailed = true fail(rel, 'script', String(e.stderr || '').split('\n').slice(0, 3).join(' ')) } finally { try { fs.unlinkSync(tmp) } catch (e) {} } } if (!jsFailed) jsOk++ } console.log('校验文件数: ' + files.length) console.log(' 模板通过: ' + tplOk + ' 样式通过: ' + scssOk + ' 脚本通过: ' + jsOk) if (compiler === null) console.log(' !! 未找到 vue-template-compiler,跳过模板校验') if (sass === null) console.log(' !! 未找到 sass,跳过样式校验') if (failures.length) { console.log('\n失败 ' + failures.length + ' 项:') for (const f of failures) console.log(' [' + f.kind + '] ' + f.file + '\n ' + f.msg) process.exit(1) } console.log('PASS')