verify-vue-compile.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. #!/usr/bin/env node
  2. /**
  3. * 全站 .vue 三段编译校验(模板 / 样式 / 脚本)
  4. *
  5. * 覆盖三类真实会打断 HBuilderX 打包的错误:
  6. * 1) 模板:表达式里未加引号的 `#hex`、可选链 `?.`、`:class` 带参方法调用等 →
  7. * vue-template-compiler 编译即失败(小程序侧表现为白板)
  8. * 2) 样式:引用了 uni.scss 里不存在的 `$var` → dart-sass 报 Undefined variable
  9. * 3) 脚本:语法错误 / 截断 → node --check 失败
  10. *
  11. * 用法:
  12. * node scripts/verify-vue-compile.js # 全量
  13. * node scripts/verify-vue-compile.js a.vue b.vue # 指定文件
  14. * 退出码: 0 全通过 / 1 有失败项
  15. */
  16. const fs = require('fs')
  17. const path = require('path')
  18. const cp = require('child_process')
  19. const os = require('os')
  20. const ROOT = path.resolve(__dirname, '..')
  21. const SKIP_DIRS = new Set(['node_modules', 'dist', 'unpackage', '.git', 'dist_bak_1792213156', '.tmp_equiv'])
  22. const SKIP_FILES = new Set(['components/mp-html/node/node.vue']) // 第三方渲染器:自定义标签 + 双 script 结构
  23. let sass = null
  24. try { sass = require(path.join(ROOT, 'node_modules/sass')) } catch (e) { sass = null }
  25. let compiler = null
  26. try { compiler = require(path.join(ROOT, 'node_modules/vue-template-compiler')) } catch (e) { compiler = null }
  27. const uniScss = fs.readFileSync(path.join(ROOT, 'uni.scss'), 'utf8')
  28. function collect(dir, out = []) {
  29. for (const name of fs.readdirSync(dir)) {
  30. if (SKIP_DIRS.has(name)) continue
  31. const p = path.join(dir, name)
  32. const st = fs.statSync(p)
  33. if (st.isDirectory()) collect(p, out)
  34. else if (name.endsWith('.vue')) out.push(p)
  35. }
  36. return out
  37. }
  38. function blocks(src) {
  39. // SFC 顶层块:标签必须位于行首(避免误命中脚本里的 /<style[\s\S]*?<\/style>/ 正则字面量)
  40. const out = { template: null, styles: [], scripts: [] }
  41. const tpl = src.match(/^<template[^>]*>([\s\S]*?)^<\/template>/m)
  42. if (tpl) out.template = tpl[1]
  43. for (const m of src.matchAll(/^<style([^>]*)>([\s\S]*?)^<\/style>/gm)) out.styles.push({ attrs: m[1] || '', code: m[2] || '' })
  44. for (const m of src.matchAll(/^<script([^>]*)>([\s\S]*?)^<\/script>/gm)) out.scripts.push({ attrs: m[1] || '', code: m[2] || '' })
  45. return out
  46. }
  47. const args = process.argv.slice(2)
  48. const files = args.length ? args.map(a => path.resolve(ROOT, a)) : collect(ROOT)
  49. const failures = []
  50. function fail(file, kind, msg) {
  51. failures.push({ file, kind, msg: String(msg).replace(/\s+/g, ' ').slice(0, 220) })
  52. }
  53. let tplOk = 0, scssOk = 0, jsOk = 0
  54. for (const abs of files) {
  55. const rel = path.relative(ROOT, abs).split(path.sep).join('/')
  56. if (SKIP_FILES.has(rel)) continue
  57. const src = fs.readFileSync(abs, 'utf8')
  58. const sfc = blocks(src)
  59. // 1) 模板
  60. if (compiler) {
  61. const tpl = sfc.template || ''
  62. if (tpl) {
  63. const res = compiler.compile(tpl)
  64. if (res.errors && res.errors.length) fail(rel, 'template', res.errors.join(' | '))
  65. else tplOk++
  66. } else tplOk++
  67. }
  68. // 2) 样式(SCSS 优先把 uni.scss 变量注入)
  69. if (sass) {
  70. const styleBlocks = sfc.styles
  71. let styleFailed = false
  72. for (const b of styleBlocks) {
  73. const attrs = b.attrs || ''
  74. const code = b.code || ''
  75. if (!/lang\s*=\s*["']scss["']/.test(attrs) && !/\$[a-zA-Z_-]/.test(code)) continue
  76. if (!/lang\s*=\s*["']scss["']/.test(attrs) && !/\$[a-zA-Z_-]/.test(code)) continue
  77. const attempts = [uniScss + '\n' + code, code]
  78. let ok = false
  79. let lastErr = null
  80. for (const candidate of attempts) {
  81. try {
  82. sass.compileString(candidate, { loadPaths: [ROOT], quietDeps: true, logger: { warn() {} } })
  83. ok = true
  84. break
  85. } catch (e) { lastErr = e }
  86. }
  87. if (!ok) {
  88. styleFailed = true
  89. fail(rel, 'style', lastErr && lastErr.message ? lastErr.message.split('\n')[0] : 'compile failed')
  90. }
  91. }
  92. if (!styleFailed) scssOk++
  93. }
  94. // 3) 脚本
  95. const scriptBlocks = sfc.scripts
  96. let jsFailed = false
  97. for (const b of scriptBlocks) {
  98. const tmp = path.join(os.tmpdir(), 'verify_' + Math.random().toString(36).slice(2) + '.js')
  99. fs.writeFileSync(tmp, b.code, 'utf8')
  100. try {
  101. cp.execFileSync(process.execPath, ['--check', tmp], { stdio: 'pipe' })
  102. } catch (e) {
  103. jsFailed = true
  104. fail(rel, 'script', String(e.stderr || '').split('\n').slice(0, 3).join(' '))
  105. } finally {
  106. try { fs.unlinkSync(tmp) } catch (e) {}
  107. }
  108. }
  109. if (!jsFailed) jsOk++
  110. }
  111. console.log('校验文件数: ' + files.length)
  112. console.log(' 模板通过: ' + tplOk + ' 样式通过: ' + scssOk + ' 脚本通过: ' + jsOk)
  113. if (compiler === null) console.log(' !! 未找到 vue-template-compiler,跳过模板校验')
  114. if (sass === null) console.log(' !! 未找到 sass,跳过样式校验')
  115. if (failures.length) {
  116. console.log('\n失败 ' + failures.length + ' 项:')
  117. for (const f of failures) console.log(' [' + f.kind + '] ' + f.file + '\n ' + f.msg)
  118. process.exit(1)
  119. }
  120. console.log('PASS')