|
|
@@ -0,0 +1,134 @@
|
|
|
+#!/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')
|