Files
danyang-match/scripts/miniapp-compile-check.mjs
T
WorkBuddy 2c325136ab fix(miniapp): 修复 WXSS 通配符导致编译失败,并把 WXSS 校验升级为真编译
问题
- v2 设计收敛在 app.wxss 新增 `.tap-gap > * + *`。WXSS 不支持通配符 `*`,
  wcc/wcsc 编译期直接报 `error at token '*'`,整个小程序编译不过。
- 更值得警惕的是当时 19 个 UI 契约守卫全绿:静态守卫刻意只扫
  `miniapp/components/`(page 的 wxss 允许标签选择器,套用会误报),
  app.wxss 落在扫描范围外,且判据里根本没有通配符这一项。

修复
- 移除 `.tap-gap > * + *`。复检 v2 新增的 .tap-min / .tap-gap / .focusable / .pressable
  四个工具类全仓零引用(等价能力已各自写在组件样式内,如 button/index.wxss 直接用
  `min-height: var(--tap-min)`),且 `.focusable:focus` / `.pressable:active` 在 view 上
  不生效(小程序须用 hover-class),属"写了等于没写",故整块移除,只保留 token。
- 修掉同块注释里的嵌套 `/* */`:内层结束标记会提前终止外层注释,其后的文字会被
  当成真实样式解析。该约束已写进注释,防止后人再踩。

守卫加固
- scripts/lib/wxss-selectors.mjs
  * 新增 findUniversalSelectors(通配符判据,排除 `*=` 属性匹配符与注释内星号)
  * 新增 findNestedComments(注释嵌套判据)
  * 修 stripComments:原实现用等长空格替换,把注释内的换行也吃掉,导致报错行号
    整体偏移(实测偏差 68 行);现保留换行,并在定位时跳过选择器前的注释占位空白
- ui-tests/wxss-component-selector-contracts.mjs
  * 第 1 级:miniapp/components/(原判据,保留)
  * 第 2 级(新增):全量 miniapp/**/*.wxss,覆盖 app.wxss 与 pages
  * 第 3 级(新增):注释嵌套
  * 三级各自带正/反自检样例

新增
- scripts/miniapp-compile-check.mjs:直接调用微信开发者工具内置的 wcc/wcsc 做真编译预检
  (wcsc 单独调用不解析 @import 相对路径,脚本会先内联再编译)。
  CI runner 无该工具时输出 SKIPPED 并退出 0;刻意不挂进 run-ui-contracts —— 一个在 CI 里
  什么都不做的守卫被计入"全绿",正是本项目反复强调的假绿。CI 拦截由上面的静态守卫承担。

验证(均实际执行)
- node scripts/miniapp-compile-check.mjs → WXSS 88 个 + WXML 86 个,0 失败
- 负向注入:把 `.tap-gap > * + *` 改回去 → 预检 FAIL(exit 1),报错与用户原始报错逐字一致
  `app.wxss(364:12): error at token '*'`;已还原并复测通过
- node scripts/run-ui-contracts.mjs → 20 / 20 PASS
2026-09-23 08:25:11 +08:00

130 lines
5.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 小程序「真编译」预检 —— 直接调用微信开发者工具内置的 wcc / wcsc 编译器。
*
* 为什么需要它(2026-09-23 教训):
* v2 设计收敛在 app.wxss 写了 `.tap-gap > * + *`(通配符 + 兄弟选择器),
* WXSS 不支持通配符 → wcc 编译期直接 `error at token '*'`,整个小程序编译不过。
* 而当时 19 个 UI 契约守卫全绿 —— 因为静态守卫只扫 `miniapp/components/`,
* 且判据里没有通配符这一项。**静态分析全绿 ≠ 能编译**。
* 本脚本用真编译器兜住这一类,属静态守卫的补充而非替代。
*
* 用法:
* node scripts/miniapp-compile-check.mjs
* 环境变量 WX_DEVTOOLS 可指定微信开发者工具安装目录。
*
* CI 说明:GitHub Actions 的 runner 上没有微信开发者工具,本脚本会输出 SKIPPED
* 并以 0 退出。**它刻意不挂进 scripts/run-ui-contracts.mjs** —— 一个在 CI 里
* 什么都不做的守卫被计入"全绿",正是本项目反复强调的"假绿"。
* CI 的拦截依赖静态守卫 ui-tests/wxss-component-selector-contracts.mjs。
*/
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { execFileSync } from 'node:child_process'
const ROOT = path.resolve(import.meta.dirname, '..', 'miniapp')
/** wcc/wcsc 相对开发者工具安装目录的位置 */
const REL = 'resources/app.asar.unpacked/node_modules/wcc-exec'
const CANDIDATES = [
process.env.WX_DEVTOOLS,
'E:/Program Files/微信web开发者工具',
'C:/Program Files (x86)/Tencent/微信web开发者工具',
'C:/Program Files/Tencent/微信web开发者工具',
'D:/Program Files (x86)/Tencent/微信web开发者工具',
].filter(Boolean)
function locateCompiler() {
for (const base of CANDIDATES) {
const dir = path.join(base, REL)
const wcc = path.join(dir, 'wcc.exe')
const wcsc = path.join(dir, 'wcsc.exe')
if (fs.existsSync(wcc) && fs.existsSync(wcsc)) return { wcc, wcsc }
}
return null
}
function walk(dir, ext, out = []) {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, e.name)
if (e.isDirectory()) {
if (!['node_modules', 'miniprogram_npm'].includes(e.name)) walk(p, ext, out)
} else if (e.name.endsWith(ext)) out.push(p)
}
return out
}
/** 把 @import "x"; 内联为被导入文件的原文(wcsc 单独调用时不做相对路径解析) */
function inlineImports(file, depth = 0) {
let src = fs.readFileSync(file, 'utf8')
if (depth > 3) return src
return src.replace(/@import\s+["']([^"']+)["']\s*;/g, (m, rel) => {
const target = path.resolve(path.dirname(file), rel)
if (!fs.existsSync(target)) return `/* MISSING IMPORT: ${rel} */`
return `\n/* ==== inlined: ${rel} ==== */\n${inlineImports(target, depth + 1)}\n`
})
}
const compileOne = (exe, file, cwd) => {
try {
execFileSync(exe, [file], { stdio: ['ignore', 'ignore', 'pipe'], cwd })
return null
} catch (e) {
const msg = (e.stderr || '').toString().trim() || `exit ${e.status}`
return msg.replace(/\r?\n/g, ' | ').slice(0, 300)
}
}
const compiler = locateCompiler()
if (!compiler) {
console.log('SKIPPED: 未找到微信开发者工具内置编译器(wcc/wcsc)')
console.log(' 可通过 WX_DEVTOOLS 指定安装目录;CI 环境无此工具属预期。')
console.log(' CI 侧的同类拦截由静态守卫 ui-tests/wxss-component-selector-contracts.mjs 承担。')
process.exit(0)
}
if (!fs.existsSync(ROOT)) {
console.error(`FAIL: 找不到小程序目录 ${ROOT} —— 拒绝报绿`)
process.exit(1)
}
const tmp = path.join(os.tmpdir(), 'miniapp-compile-check')
fs.mkdirSync(tmp, { recursive: true })
const failures = []
// ── WXSS ─────────────────────────────────────────────────────────────
const wxssFiles = walk(ROOT, '.wxss')
for (const f of wxssFiles) {
const rel = path.relative(ROOT, f).replace(/\\/g, '/')
const hasImport = /@import/.test(fs.readFileSync(f, 'utf8'))
const target = path.join(tmp, `${rel.replace(/\//g, '__')}`)
fs.writeFileSync(target, hasImport ? inlineImports(f) : fs.readFileSync(f, 'utf8'), 'utf8')
const err = compileOne(compiler.wcsc, target, tmp)
if (err) failures.push({ kind: 'WXSS', rel, err })
}
// ── WXML ─────────────────────────────────────────────────────────────
const wxmlFiles = walk(ROOT, '.wxml')
for (const f of wxmlFiles) {
const rel = path.relative(ROOT, f).replace(/\\/g, '/')
const err = compileOne(compiler.wcc, f, path.dirname(f))
if (err) failures.push({ kind: 'WXML', rel, err })
}
console.log(`小程序真编译预检(wcc / wcsc)`)
console.log(` WXSS : ${wxssFiles.length} 个`)
console.log(` WXML : ${wxmlFiles.length} 个`)
console.log(` 失败 : ${failures.length}`)
if (failures.length) {
console.log('--- 失败明细 ---')
for (const f of failures) console.log(`[${f.kind}] ${f.rel}\n ${f.err}`)
console.error(`\n❌ FAIL:${failures.length} 个文件编译不过`)
process.exit(1)
}
console.log('\n✅ 全部编译通过')