Files
t/tools/build.js
T
root 3117146281 feat: 打通 HBuilderX CLI 构建 + 修复文章闭环静态盲区
构建:
- 新增 tools/build.js:junction HBuilderX 工具链,CLI 构建 h5/mp-weixin
- vue 指向补丁版 @dcloudio/uni-h5-vue(官方 npm vue 不导出 isInSSRComponentSetup)
- 设 HX_APP_ROOT 避免退化成 H5 空壳产物;产物完整性校验

校验工具:
- 新增 check-cloud-methods.js:acorn 解析云对象方法,比对 94 处调用点
- 新增 check-android-contract.js:Kotlin 侧云对象契约校验
- audit-project.js 修 downloadFile 误报(注释未剥离);tools/ 排除出扫描
- package.json 声明此前隐式依赖的 acorn

功能:
- 补 uni-cms-articles.getPublishedArticles(安卓端依赖但此前不存在)
- 修 u-parse <audio> 引用已移除组件导致 H5 构建失败
2026-09-12 01:02:45 +08:00

305 lines
11 KiB
JavaScript
Raw 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
'use strict'
/**
* HBuilderX CLI 构建包装器。
*
* 背景:本项目是 HBuilderX 工程,Vue3 编译所需的 @dcloudio/* 工具链只随
* HBuilderX 分发,不在项目 package.json 里。直接在项目目录跑 `npm install`
* 会装入 Vue2 时代的旧包,且每次 install 都会清掉手工建的联接。
*
* 因此本脚本在构建前自动完成三件事(幂等,可反复跑):
* 1. 把 HBuilderX 自带的工具链目录联接到项目 node_modules/
* 2. 设置 HX_APP_ROOT / UNI_INPUT_DIR / UNI_OUTPUT_DIR,让 uni CLI
* 走 HBuilderX 内置模块解析路径(缺 HX_APP_ROOT 时会退化成 H5 空壳产物)
* 3. 用 HBuilderX 自带的 node 执行 `uni build -p <platform>`
*
* 不用打开 HBuilderX GUI,也不需要联网。
*
* 用法:
* node tools/build.js -p h5
* node tools/build.js -p mp-weixin
* node tools/build.js -p h5 --report # 额外打印产物体积明细
* node tools/build.js --link-only # 只建联接,不构建
* node tools/build.js --doctor # 只做环境体检
*
* 环境变量:
* HBUILDERX_HOME HBuilderX 安装目录,默认 C:/Program Files/HBuilderX/HBuilderX
*/
const fs = require('fs')
const path = require('path')
const { spawnSync } = require('child_process')
const ROOT = path.resolve(__dirname, '..')
const HBX = process.env.HBUILDERX_HOME || 'C:/Program Files/HBuilderX/HBuilderX'
const HBX_TC = path.join(HBX, 'plugins', 'uniapp-cli-vite', 'node_modules')
const HBX_SASS = path.join(HBX, 'plugins', 'compile-dart-sass', 'node_modules')
const UNI_JS = path.join(HBX_TC, '@dcloudio', 'vite-plugin-uni', 'bin', 'uni.js')
const NM = path.join(ROOT, 'node_modules')
// HBuilderX 自带的 node。系统 node 太新时 vite 的 config 缓存格式不兼容
// (failed to load config / Invalid or incompatible cached data),
// 固定用 HBuilderX 的 node 最稳。
const HBX_NODE_CANDIDATES = [
path.join(HBX, 'plugins', 'node', 'node.exe'),
path.join(HBX, 'plugins', 'node18', 'node.exe')
]
const PLATFORM_OUTPUT = {
h5: 'unpackage/dist/build/web',
'mp-weixin': 'unpackage/dist/build/mp-weixin',
'mp-alipay': 'unpackage/dist/build/mp-alipay',
app: 'unpackage/dist/build/app-plus'
}
// ── 联接管理 ────────────────────────────────────────────────
function isLink(target) {
try {
return fs.lstatSync(target).isSymbolicLink() || fs.lstatSync(target).isDirectory()
} catch {
return false
}
}
/**
* 建立 junction。Node 没有跨平台的 mklink API,Windows 上退到 cmd。
* 目录联接不需要管理员权限(符号链接才需要)。
*/
function junction(linkPath, targetPath) {
const r = spawnSync('cmd', ['/c', 'mklink', '/J', linkPath, targetPath], {
encoding: 'utf8',
windowsHide: true
})
return r.status === 0
}
function linkAll() {
if (!fs.existsSync(HBX_TC)) {
console.error(`✗ 找不到 HBuilderX 工具链:${HBX_TC}`)
console.error(' 请设置 HBUILDERX_HOME 指向 HBuilderX 安装目录。')
return { ok: false, created: 0 }
}
if (!fs.existsSync(UNI_JS)) {
console.error(`✗ 找不到 uni CLI:${UNI_JS}`)
return { ok: false, created: 0 }
}
fs.mkdirSync(NM, { recursive: true })
let created = 0
// 1) scoped 包:@dcloudio/*、@vue/* 等
const scopes = []
for (const entry of fs.readdirSync(HBX_TC, { withFileTypes: true })) {
if (entry.isDirectory() && entry.name.startsWith('@')) scopes.push(entry.name)
}
for (const scope of scopes) {
const scopeDir = path.join(HBX_TC, scope)
fs.mkdirSync(path.join(NM, scope), { recursive: true })
for (const pkg of fs.readdirSync(scopeDir)) {
const src = path.join(scopeDir, pkg)
if (!fs.statSync(src).isDirectory()) continue
const dest = path.join(NM, scope, pkg)
if (fs.existsSync(dest)) continue
if (junction(dest, src)) created++
}
}
// 2) vue 必须指向 uni-app 打过补丁的运行时。
// 官方 npm vue 3.x 不导出 isInSSRComponentSetup,而 @dcloudio/uni-app
// 从 'vue' 导入它 —— 用原版 vue 构建会直接失败在 rollup 解析阶段。
const patchedVue = path.join(HBX_TC, '@dcloudio', 'uni-h5-vue')
const vueDest = path.join(NM, 'vue')
if (fs.existsSync(patchedVue) && !fs.existsSync(vueDest)) {
if (junction(vueDest, patchedVue)) created++
}
// 3) 顶层工具包(vite / sass / rollup / esbuild ...)。
// sass 在另一个插件目录里。
for (const base of [HBX_TC, HBX_SASS]) {
if (!fs.existsSync(base)) continue
for (const pkg of fs.readdirSync(base)) {
if (pkg.startsWith('@') || pkg.startsWith('.')) continue
const src = path.join(base, pkg)
let stat
try { stat = fs.statSync(src) } catch { continue }
if (!stat.isDirectory()) continue
// 有些包用符号链接指向同目录其它包,跳过避免自指
const dest = path.join(NM, pkg)
if (fs.existsSync(dest)) continue
if (junction(dest, src)) created++
}
}
return { ok: true, created }
}
// ── 构建 ────────────────────────────────────────────────────
function dirSize(dir) {
let total = 0
const stack = [dir]
while (stack.length) {
const d = stack.pop()
let entries
try { entries = fs.readdirSync(d, { withFileTypes: true }) } catch { continue }
for (const e of entries) {
const p = path.join(d, e.name)
if (e.isDirectory()) stack.push(p)
else {
try { total += fs.statSync(p).size } catch {}
}
}
}
return total
}
function countFiles(dir, ext) {
let n = 0
const stack = [dir]
while (stack.length) {
const d = stack.pop()
let entries
try { entries = fs.readdirSync(d, { withFileTypes: true }) } catch { continue }
for (const e of entries) {
if (e.isDirectory()) stack.push(path.join(d, e.name))
else if (e.name.endsWith(ext)) n++
}
}
return n
}
function report(outputDir) {
if (!fs.existsSync(outputDir)) {
console.log(' (无产物目录)')
return
}
const fmt = (n) => (n / 1024).toFixed(1) + ' KB'
console.log(` 产物目录:${path.relative(ROOT, outputDir).replace(/\\/g, '/')}`)
const entries = fs.readdirSync(outputDir, { withFileTypes: true })
.map((e) => {
const p = path.join(outputDir, e.name)
return { name: e.name + (e.isDirectory() ? '/' : ''), size: e.isDirectory() ? dirSize(p) : fs.statSync(p).size }
})
.sort((a, b) => b.size - a.size)
for (const e of entries.slice(0, 10)) {
console.log(` ${e.name.padEnd(24)} ${fmt(e.size)}`)
}
const total = entries.reduce((s, e) => s + e.size, 0)
console.log(` ${'合计'.padEnd(22)} ${fmt(total)}`)
}
function main() {
const argv = process.argv.slice(2)
const linkOnly = argv.includes('--link-only')
const doctorOnly = argv.includes('--doctor')
const wantReport = argv.includes('--report')
const pIdx = argv.indexOf('-p')
const platform = pIdx >= 0 ? argv[pIdx + 1] : 'h5'
console.log('')
console.log('═══════════════════════════════════════════════')
console.log(' 军歌嘹亮 · HBuilderX CLI 构建')
console.log('═══════════════════════════════════════════════')
console.log('')
console.log(` HBuilderX ${HBX}`)
console.log(` 平台 ${platform}`)
// 环境体检:任一缺失都会导致产物异常或构建失败
const hbxNode = HBX_NODE_CANDIDATES.find((p) => fs.existsSync(p))
const issues = []
if (!fs.existsSync(HBX_TC)) issues.push(`缺失工具链目录:${HBX_TC}`)
if (!fs.existsSync(UNI_JS)) issues.push(`缺失 uni CLI:${UNI_JS}`)
if (!hbxNode) issues.push('未找到 HBuilderX 自带 node(plugins/node/node.exe 或 node18)')
const patchedVue = path.join(HBX_TC, '@dcloudio', 'uni-h5-vue')
if (!fs.existsSync(patchedVue)) {
issues.push('未找到 @dcloudio/uni-h5-vue(uni-app 打过补丁的 Vue 运行时)')
}
console.log(` 构建 node ${hbxNode || '(未找到)'}`)
if (issues.length) {
console.log('')
for (const i of issues) console.log(` ✗ ${i}`)
console.log('')
console.log(' 请确认 HBUILDERX_HOME 指向 HBuilderX 安装目录。')
process.exit(1)
}
if (doctorOnly) {
// 顺带验证补丁版 vue 是否真的导出 isInSSRComponentSetup
const vueEs = path.join(patchedVue, 'dist', 'vue.runtime.esm.js')
let patched = false
try {
patched = fs.readFileSync(vueEs, 'utf8').includes('isInSSRComponentSetup')
} catch {}
console.log(` Vue 运行时 ${patched ? '补丁版 OK(含 isInSSRComponentSetup)' : '异常:未检出 isInSSRComponentSetup'}`)
if (!patched) process.exit(1)
console.log('')
console.log('✓ 环境体检通过(--doctor,未执行构建)')
process.exit(0)
}
const { ok, created } = linkAll()
if (!ok) process.exit(1)
console.log(` 工具链联接 新建 ${created} 个(已存在的跳过)`)
console.log('')
if (linkOnly) {
console.log('✓ 联接完成(--link-only,未执行构建)')
process.exit(0)
}
const outputDir = path.join(ROOT, PLATFORM_OUTPUT[platform] || path.join('unpackage/dist/build', platform))
fs.mkdirSync(outputDir, { recursive: true })
const env = {
...process.env,
HBUILDERX_HOME: HBX,
// 缺 HX_APP_ROOT 时 uni-cli-shared 不会启用 HBuilderX 模块解析路径,
// 构建会"成功"但产出 H5 空壳(没有 app.json / 业务分包)。
HX_APP_ROOT: HBX,
UNI_HBUILDERX_PLUGINS: path.join(HBX, 'plugins'),
UNI_INPUT_DIR: ROOT,
UNI_OUTPUT_DIR: outputDir
}
delete env.UNI_PLATFORM // 由 CLI 的 -p 参数决定,预设会干扰平台判定
console.log('编译中…')
const r = spawnSync(hbxNode, [UNI_JS, 'build', '-p', platform], {
cwd: ROOT,
env,
stdio: 'inherit'
})
console.log('')
if (r.status !== 0) {
console.log('✗ 构建失败')
process.exit(r.status || 1)
}
// 产物校验:构建退出码为 0 不代表产物可用。
// 缺 HX_APP_ROOT 时 H5 会输出 index.html 空壳,mp 会缺 app.json。
const mustHave = platform.startsWith('mp-')
? ['app.json', 'app.js', 'app.wxss']
: ['index.html']
const missing = mustHave.filter((f) => !fs.existsSync(path.join(outputDir, f)))
const jsCount = countFiles(outputDir, '.js')
if (missing.length) {
console.log(`✗ 构建报成功但产物不完整,缺少:${missing.join(', ')}`)
console.log(' 多半是 HX_APP_ROOT 未生效(工具链退化为 H5 空壳输出)。')
process.exit(1)
}
if (jsCount === 0) {
console.log('✗ 产物中没有 JS 文件,构建未真正执行。')
process.exit(1)
}
console.log(`✓ 构建完成(${jsCount} 个 JS 文件)`)
if (wantReport) report(outputDir)
console.log('')
}
main()