| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- #!/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 顶层块:标签必须位于行首(避免误命中脚本里的 /<style[\s\S]*?<\/style>/ 正则字面量)
- const out = { template: null, styles: [], scripts: [] }
- const tpl = src.match(/^<template[^>]*>([\s\S]*?)^<\/template>/m)
- if (tpl) out.template = tpl[1]
- for (const m of src.matchAll(/^<style([^>]*)>([\s\S]*?)^<\/style>/gm)) out.styles.push({ attrs: m[1] || '', code: m[2] || '' })
- for (const m of src.matchAll(/^<script([^>]*)>([\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')
|