niucloud/uni-app/scripts/mp-utils.cjs
wangchen14709853322 6d9c8ff7f5 v2.0-beta-20260626
v2框架公测版测试流程请看v2.0-beta.md
2026-07-01 12:15:20 +08:00

1043 lines
36 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.

/**
* 微信小程序双产物构建:路径、分包映射、文件工具
*/
const fs = require('fs')
const path = require('path')
const crypto = require('crypto')
const { spawnSync } = require('child_process')
const { ROOT, BUILD_DIR, PAGES_JSON, parsePagesJson, listAddonKeys, walkFiles } = require('./addon-utils.cjs')
const MP_OUT_BUILD = path.join(ROOT, 'dist', 'build', 'mp-weixin')
/** 框架小程序源码(可直接导入开发者工具,无插件分包) */
const MP_CORE_DIR = path.join(ROOT, 'dist', 'build', 'mp-core')
const MP_FRAMEWORK_DIR = MP_CORE_DIR
const MP_FULL_DIR = path.join(ROOT, 'dist', '.mp-full')
/** 单插件打包产物 dist/mp-weixin-addons/{key}/ */
const MP_ADDONS_DIR = path.join(ROOT, 'dist', 'mp-weixin-addons')
const MP_LOCK_PATH = path.join(BUILD_DIR, 'mp-build-lock.json')
/** 含全部 addon 分包的 pages 快照generate-split-pages 产出;框架编译会 strip src/pages.json */
const PAGES_CORE = path.join(BUILD_DIR, 'pages.core.json')
/** 框架包保留的主包级 addon 目录diy-group 等) */
const MP_FRAMEWORK_ADDON_KEEP = new Set(['components'])
const MP_SHARED_BRIDGE_PAGE = 'app/pages/_mp_shared_bridge/index'
const MP_SHARED_BRIDGE_SRC = path.join(BUILD_DIR, 'mp-shared-bridge')
const ADDON_COMPONENT_IMPORT_RE = /@\/addon\/components\/([^/'"\s]+)(?:\/([^/'"\s]+))?/g
const MAIN_COMPONENT_IMPORT_RE = /@\/components\/([^/'"\s]+(?:\/[^/'"\s]+)*)/g
/** 仅组件、无页面的分包 root微信 app.json 允许 pages: [] */
const COMPONENT_ONLY_SUBPACKAGE_ROOTS = new Set(['app/components'])
/** 仅组件分包占位页(相对 app/components微信产物为 app/components/pages/_mp_placeholder.wxml */
const MP_COMPONENT_SUBPACKAGE_PLACEHOLDER = 'pages/_mp_placeholder'
/** 主包 diy-dynamic-bridge插件编译含 addon 微页面组件,合并时需覆盖框架 appOnly 版本) */
const DIY_DYNAMIC_BRIDGE_REL = 'components/diy-dynamic-bridge'
function rmDir(dir) {
if (!fs.existsSync(dir)) return
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
}
function copyDir(src, dest) {
fs.mkdirSync(path.dirname(dest), { recursive: true })
fs.cpSync(src, dest, { recursive: true, force: true })
}
function sha256File(filePath) {
const buf = fs.readFileSync(filePath)
return crypto.createHash('sha256').update(buf).digest('hex')
}
function hashDirFiles(dir, relativeTo = dir) {
const out = {}
if (!fs.existsSync(dir)) return out
for (const rel of walkJsAndJson(dir)) {
const abs = path.join(dir, rel)
out[path.relative(relativeTo, abs).replace(/\\/g, '/')] = sha256File(abs)
}
return out
}
function walkJsAndJson(rootDir, base = rootDir) {
const out = []
if (!fs.existsSync(rootDir)) return out
for (const name of fs.readdirSync(rootDir)) {
const full = path.join(rootDir, name)
const stat = fs.statSync(full)
if (stat.isDirectory()) {
out.push(...walkJsAndJson(full, base))
} else if (/\.(js|json)$/.test(name)) {
out.push(path.relative(base, full).replace(/\\/g, '/'))
}
}
return out
}
function getGitCommit() {
const r = spawnSync('git', ['rev-parse', 'HEAD'], {
cwd: ROOT,
encoding: 'utf-8'
})
if (r.status === 0) return (r.stdout || '').trim()
return null
}
function makeBuildId(label) {
const commit = getGitCommit()
const ts = new Date().toISOString()
const raw = `${label}|${commit || 'no-git'}|${ts}`
return crypto.createHash('sha256').update(raw).digest('hex').slice(0, 12)
}
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, 'utf-8'))
}
function writeJson(filePath, data) {
fs.mkdirSync(path.dirname(filePath), { recursive: true })
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8')
}
function readAppJson(mpDir) {
return readJson(path.join(mpDir, 'app.json'))
}
function hasAddonSubPackages(pagesJson) {
return (pagesJson.subPackages || []).some((s) => s.root?.startsWith('addon/'))
}
/** 完整 pages含 addon 分包);优先 .build/pages.core.json避免框架编译后 src/pages.json 已 strip addon */
function resolveFullPagesJson() {
if (fs.existsSync(PAGES_CORE)) {
const core = readJson(PAGES_CORE)
if (hasAddonSubPackages(core)) {
return core
}
}
return parsePagesJson()
}
/** 按插件 key 收集 pages.json 中的分包 root含 home_service 等多 root */
function getSubPackageRootsForKey(addonKey, pagesJson = resolveFullPagesJson()) {
const prefix = `addon/${addonKey}`
const roots = []
for (const sub of pagesJson.subPackages || []) {
const root = sub.root
if (!root || !root.startsWith('addon/')) continue
if (root === prefix || root.startsWith(`${prefix}/`)) {
roots.push(root)
}
}
return [...new Set(roots)].sort()
}
function getAllAddonSubPackageRoots(pagesJson = resolveFullPagesJson()) {
const map = {}
for (const sub of pagesJson.subPackages || []) {
const root = sub.root
if (!root || !root.startsWith('addon/')) continue
const parts = root.split('/')
const key = parts[1]
if (!key || key === 'components') continue
if (!map[key]) map[key] = []
if (!map[key].includes(root)) map[key].push(root)
}
for (const key of Object.keys(map)) {
map[key].sort()
}
return map
}
function filterSubPackages(appJson, rootsToKeep) {
const set = new Set(rootsToKeep)
const subPackages = (appJson.subPackages || []).filter((s) => set.has(s.root))
return subPackages
}
function collectCommonManifest(mpDir) {
const commonDir = path.join(mpDir, 'common')
const manifest = {}
if (!fs.existsSync(commonDir)) return manifest
for (const rel of walkJsAndJson(commonDir)) {
const abs = path.join(commonDir, rel)
manifest[`common/${rel}`.replace(/\\/g, '/')] = sha256File(abs)
}
return manifest
}
function runUniMpBuild() {
const r = spawnSync(
'npx',
['uni', 'build', '-p', 'mp-weixin'],
{
cwd: ROOT,
env: {
...process.env,
UNI_PLATFORM: 'mp-weixin',
NODE_OPTIONS: process.env.NODE_OPTIONS || '--max-old-space-size=4096'
},
stdio: 'inherit',
shell: process.platform === 'win32'
}
)
return r.status ?? 1
}
function snapshotMpBuild(srcDir, destDir, meta) {
rmDir(destDir)
copyDir(srcDir, destDir)
writeJson(path.join(destDir, 'mp-build-meta.json'), {
...meta,
time: new Date().toISOString(),
gitCommit: getGitCommit()
})
}
/**
* 框架编译会残留未注册分包的 addon 目录,删除除 addon/components 外的插件产物
*/
function stripOrphanAddonSubpackages(mpDir) {
const addonRoot = path.join(mpDir, 'addon')
if (!fs.existsSync(addonRoot)) return 0
let removed = 0
for (const name of fs.readdirSync(addonRoot)) {
if (MP_FRAMEWORK_ADDON_KEEP.has(name)) continue
const target = path.join(addonRoot, name)
if (!fs.statSync(target).isDirectory()) continue
rmDir(target)
removed++
console.log(`[mp-utils] stripped orphan addon/${name}/`)
}
return removed
}
function isAddonSubPackageRoot(root) {
return Boolean(root && root.startsWith('addon/'))
}
function getAppSubPackages(appJson) {
const upper = appJson.subPackages
const lower = appJson.subpackages
if (Array.isArray(upper) && upper.length) return upper
if (Array.isArray(lower) && lower.length) return lower
if (Array.isArray(upper)) return upper
if (Array.isArray(lower)) return lower
return []
}
function setAppSubPackages(appJson, subPackages) {
appJson.subPackages = subPackages
delete appJson.subpackages
return appJson
}
/** 框架 pages.framework.json 中的非 addon 分包(主包级分包) */
function loadFrameworkNonAddonSubPackages(frameworkDir) {
if (frameworkDir) {
const snap = path.join(frameworkDir, 'mp-framework-subpackages.json')
if (fs.existsSync(snap)) {
return readJson(snap)
}
}
const frameworkPages = path.join(BUILD_DIR, 'pages.framework.json')
if (!fs.existsSync(frameworkPages)) return []
const pages = readJson(frameworkPages)
return (pages.subPackages || []).filter((s) => s.root && !isAddonSubPackageRoot(s.root))
}
/** pages.json 分包项 → 微信 app.json 格式pages 为 string[] */
function normalizeSubPackageEntry(sub) {
if (!sub?.root) return null
const pages = (sub.pages || [])
.map((p) => (typeof p === 'string' ? p : p?.path))
.filter(Boolean)
if (!pages.length && !COMPONENT_ONLY_SUBPACKAGE_ROOTS.has(sub.root)) return null
const out = { root: sub.root, pages }
if (sub.name != null) out.name = sub.name
if (sub.independent != null) out.independent = sub.independent
return out
}
function normalizeAppSubPackages(subPackages) {
return subPackages.map((s) => normalizeSubPackageEntry(s)).filter(Boolean)
}
/** 框架编译后写入非 addon 分包,并规范化 app.json subPackages */
function applyFrameworkAppJsonSubPackages(mpDir) {
const appPath = path.join(mpDir, 'app.json')
if (!fs.existsSync(appPath)) return
const appJson = readAppJson(mpDir)
const baseline = loadFrameworkNonAddonSubPackages()
if (baseline.length) {
mergeSubPackagesIntoApp(appJson, baseline)
} else {
setAppSubPackages(appJson, normalizeAppSubPackages(getAppSubPackages(appJson)))
}
writeJson(appPath, appJson)
const nonAddon = getAppSubPackages(appJson).filter((s) => !isAddonSubPackageRoot(s.root))
if (nonAddon.length) {
console.log(
`[mp-utils] app.json non-addon subPackages: ${nonAddon.map((s) => s.root).join(', ')}`
)
}
}
/**
* 保留 app.json 中不应被本次合并覆盖的分包:
* - 非 addon 分包
* - 磁盘上仍存在、且不在本次合并范围内的 addon 分包
*/
function collectPreservedSubPackages(appJson, frameworkDir, mergingKeys, pkgRoot) {
const existing = getAppSubPackages(appJson)
const mergingRoots = new Set()
for (const key of mergingKeys) {
const manifestPath = path.join(pkgRoot, key, 'manifest.json')
if (!fs.existsSync(manifestPath)) continue
for (const root of readJson(manifestPath).roots || []) {
mergingRoots.add(root)
}
}
const preserved = []
for (const sub of existing) {
if (!sub?.root) continue
if (!isAddonSubPackageRoot(sub.root)) {
preserved.push(sub)
continue
}
if (mergingRoots.has(sub.root)) continue
if (fs.existsSync(path.join(frameworkDir, sub.root))) {
preserved.push(sub)
}
}
return preserved
}
/** 保留指定插件分包目录,删除其余 addon 插件目录 */
function stripAddonSubpackagesExcept(mpDir, keepKeys) {
const keepRoots = new Set()
for (const key of keepKeys) {
for (const root of getSubPackageRootsForKey(key)) {
keepRoots.add(root)
}
}
const addonRoot = path.join(mpDir, 'addon')
if (!fs.existsSync(addonRoot)) return 0
let removed = 0
for (const name of fs.readdirSync(addonRoot)) {
if (MP_FRAMEWORK_ADDON_KEEP.has(name)) continue
const target = path.join(addonRoot, name)
if (!fs.statSync(target).isDirectory()) continue
const isKept = [...keepRoots].some((root) => {
const rel = root.replace(/^addon\//, '')
return rel === name || rel.startsWith(`${name}/`)
})
if (isKept) continue
rmDir(target)
removed++
console.log(`[mp-utils] stripped addon/${name}/`)
}
return removed
}
function mergeSubPackagesIntoApp(appJson, newSubs) {
const existing = normalizeAppSubPackages(getAppSubPackages(appJson))
const byRoot = new Map(existing.map((s) => [s.root, s]))
for (const sub of normalizeAppSubPackages(newSubs)) {
byRoot.set(sub.root, sub)
}
setAppSubPackages(appJson, [...byRoot.values()])
return appJson
}
function getAddonPackageDir(key, _frameworkDir = MP_CORE_DIR) {
return path.join(MP_ADDONS_DIR, key)
}
function listAddonPackagesInFramework(frameworkDir) {
const pkgRoot = path.join(frameworkDir, 'mp-addons')
if (!fs.existsSync(pkgRoot)) return []
return fs
.readdirSync(pkgRoot, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name)
.filter((name) => fs.existsSync(path.join(pkgRoot, name, 'manifest.json')))
.sort()
}
function installMergeScriptToFramework(frameworkDir) {
const libDir = path.join(frameworkDir, 'lib')
fs.mkdirSync(libDir, { recursive: true })
const copyList = [
['mp-diy-bridge-merge.cjs', 'mp-diy-bridge-merge.cjs'],
['mp-merge-utils.cjs', 'mp-merge-utils.cjs'],
['mp-utils.cjs', 'mp-utils.cjs'],
['addon-utils.cjs', 'addon-utils.cjs'],
['diy-component-scan.cjs', 'diy-component-scan.cjs'],
['scan-mp-requires.cjs', 'scan-mp-requires.cjs'],
['mp-weixin-post.cjs', 'mp-weixin-post.cjs'],
['mp-diy-group-inline.cjs', 'mp-diy-group-inline.cjs'],
['mp-diy-registry-runtime.cjs', 'mp-diy-registry-runtime.cjs']
]
for (const [srcName, destName] of copyList) {
fs.copyFileSync(path.join(ROOT, 'scripts', srcName), path.join(libDir, destName))
}
fs.copyFileSync(path.join(ROOT, 'scripts', 'merge-mp-addon.cjs'), path.join(frameworkDir, 'merge-mp-addon.cjs'))
const tplSrc = path.join(ROOT, 'scripts', 'mp-templates')
const tplDest = path.join(libDir, 'mp-templates')
rmDir(tplDest)
copyDir(tplSrc, tplDest)
const pkgRoot = path.join(frameworkDir, 'mp-addons')
fs.mkdirSync(pkgRoot, { recursive: true })
const readme = path.join(pkgRoot, 'README.txt')
fs.writeFileSync(
readme,
[
'将插件包放在此目录下,每个插件一个子目录,例如:',
'',
' mp-addons/shop/components/diy-dynamic-bridge-shop/...',
' mp-addons/shop/manifest.json (含 diyBridge.registry)',
'',
'然后在框架根目录执行(无需 uni 源码,纯拷贝合并):',
' node merge-mp-addon.cjs',
' node merge-mp-addon.cjs shop pintuan',
''
].join('\n'),
'utf-8'
)
}
function mergePreloadRules(appJson, rules) {
if (!rules || !Object.keys(rules).length) return appJson
appJson.preloadRule = { ...(appJson.preloadRule || {}), ...rules }
return appJson
}
/** 使用 diy-group 的微页面主包路径 */
const DIY_MICRO_PAGE_PATHS = [
'app/pages/index/diy',
'app/pages/index/diy_form',
'app/pages/index/diy_form_result',
'app/pages/index/diy_form_detail'
]
/** 插件内带 diy-group 的常见页面(合并插件后写入 preloadRule */
const DIY_ADDON_PAGE_PATHS = {
shop: [
'addon/shop/pages/index',
'addon/shop/pages/member/index',
'addon/shop/pages/point/index',
'addon/shop/pages/goods/detail'
],
pintuan: [
'addon/pintuan/pages/index',
'addon/pintuan/pages/member/index',
'addon/pintuan/pages/goods/detail'
]
}
function buildDiyPreloadRules(packageIds, extraPagePaths = []) {
const packages = [...new Set((packageIds || []).filter(Boolean))]
if (!packages.length) return {}
const rule = { network: 'all', packages }
const rules = {}
for (const page of [...DIY_MICRO_PAGE_PATHS, ...(extraPagePaths || [])]) {
rules[page] = { ...rule }
}
return rules
}
/** pages.framework.json为 app/components 注入占位页 */
function ensureComponentSubpackagePlaceholderPagesSubPackages(subPackages) {
if (!Array.isArray(subPackages)) return
for (const sub of subPackages) {
if (sub?.root !== 'app/components') continue
const pages = sub.pages || []
const has = pages.some(
(p) => (typeof p === 'string' ? p : p?.path) === MP_COMPONENT_SUBPACKAGE_PLACEHOLDER
)
if (has) continue
sub.pages = [
...pages,
{
path: MP_COMPONENT_SUBPACKAGE_PLACEHOLDER,
style: { navigationStyle: 'custom', navigationBarTitleText: '' }
}
]
}
}
/** 编译产物:写入占位页四件套(扁平 path.wxml并 patch app.json subPackages */
function ensureComponentSubpackagePlaceholderMp(mpDir) {
const legacyDir = path.join(mpDir, 'app/components/_mp_placeholder')
if (fs.existsSync(legacyDir)) {
rmDir(legacyDir)
}
const pageBase = path.join(mpDir, 'app/components', MP_COMPONENT_SUBPACKAGE_PLACEHOLDER)
fs.mkdirSync(path.dirname(pageBase), { recursive: true })
fs.writeFileSync(`${pageBase}.js`, 'Page({})\n', 'utf-8')
fs.writeFileSync(
`${pageBase}.json`,
JSON.stringify({ navigationStyle: 'custom', navigationBarTitleText: '' }) + '\n',
'utf-8'
)
fs.writeFileSync(`${pageBase}.wxml`, '<view></view>\n', 'utf-8')
fs.writeFileSync(`${pageBase}.wxss`, '\n', 'utf-8')
const appPath = path.join(mpDir, 'app.json')
if (!fs.existsSync(appPath)) return false
const appJson = readAppJson(mpDir)
const subs = getAppSubPackages(appJson)
const sub = subs.find((s) => s.root === 'app/components')
if (!sub) return false
if (!Array.isArray(sub.pages)) sub.pages = []
sub.pages = sub.pages.filter((p) => p !== '_mp_placeholder')
if (!sub.pages.includes(MP_COMPONENT_SUBPACKAGE_PLACEHOLDER)) {
sub.pages.push(MP_COMPONENT_SUBPACKAGE_PLACEHOLDER)
}
writeJson(appPath, appJson)
return true
}
/** preloadRule.packages 只能引用 subPackages 中已有页面且 root/name 匹配的分包 */
function resolvePreloadPackageIds(appJson, requestedRoots) {
const subs = getAppSubPackages(appJson)
const out = []
for (const req of requestedRoots || []) {
const normalized = String(req).replace(/\/+$/, '')
for (const sub of subs) {
const root = String(sub.root || '').replace(/\/+$/, '')
const name = sub.name ? String(sub.name).replace(/\/+$/, '') : ''
const pages = (sub.pages || []).filter(Boolean)
if (!pages.length) continue
if (normalized === root || (name && normalized === name)) {
out.push(name || root)
break
}
}
}
return [...new Set(out)]
}
/**
* 微页面 DIY 组件在 app/components、addon/* 分包内,主包引用需 componentPlaceholder
* 必须 preload 对应分包,否则占位 view 不会替换为真实组件(无控制台报错)。
*/
function applyDiyPreloadRules(mpDir, options = {}) {
const appPath = path.join(mpDir, 'app.json')
if (!fs.existsSync(appPath)) return null
ensureComponentSubpackagePlaceholderMp(mpDir)
const appJson = readAppJson(mpDir)
const addonRoots = options.addonRoots || []
const extraPages = (options.addonKeys || []).flatMap((key) => DIY_ADDON_PAGE_PATHS[key] || [])
const requested = ['app/components', ...addonRoots]
const packages = resolvePreloadPackageIds(appJson, requested)
if (!packages.length) {
console.warn('[mp-utils] diy preloadRule skipped: no preloadable subPackages in app.json')
return appJson
}
mergePreloadRules(appJson, buildDiyPreloadRules(packages, extraPages))
writeJson(appPath, appJson)
console.log(`[mp-utils] diy preloadRule packages: ${packages.join(', ')}`)
return appJson
}
function mergeOverlayDir(targetMpDir, sourceDir, destName) {
if (!fs.existsSync(sourceDir)) return 0
const destDir = path.join(targetMpDir, destName)
fs.mkdirSync(destDir, { recursive: true })
let count = 0
function walkCopy(src, dest) {
for (const name of fs.readdirSync(src)) {
const s = path.join(src, name)
const d = path.join(dest, name)
if (fs.statSync(s).isDirectory()) {
fs.mkdirSync(d, { recursive: true })
walkCopy(s, d)
} else {
fs.copyFileSync(s, d)
count++
}
}
}
walkCopy(sourceDir, destDir)
return count
}
function mergeCommonDir(targetMpDir, sourceCommonDir) {
return mergeOverlayDir(targetMpDir, sourceCommonDir, 'common')
}
function mergeNodeModulesDir(targetMpDir, sourceNodeModulesDir) {
return mergeOverlayDir(targetMpDir, sourceNodeModulesDir, 'node-modules')
}
function readDiyBridgeComponentCount(bridgeDir) {
const jsonPath = path.join(bridgeDir, 'diy-dynamic-bridge.json')
if (!fs.existsSync(jsonPath)) return 0
try {
return Object.keys(readJson(jsonPath).usingComponents || {}).length
} catch {
return 0
}
}
function packageAddonDiyBridge(key, buildDir, destDir) {
const rel = `components/diy-dynamic-bridge-${key}`
const src = path.join(buildDir, rel)
if (!fs.existsSync(src)) return 0
copyDir(src, path.join(destDir, rel))
const jsonPath = fs.existsSync(path.join(src, 'index.json'))
? path.join(src, 'index.json')
: path.join(src, `diy-dynamic-bridge-${key}.json`)
if (!fs.existsSync(jsonPath)) return 0
try {
return Object.keys(readJson(jsonPath).usingComponents || {}).length
} catch {
return 0
}
}
/** @deprecated 单 bridge 模式,已由 packageAddonDiyBridge + router 补丁替代 */
function packageDiyDynamicBridge(buildDir, destDir) {
const src = path.join(buildDir, DIY_DYNAMIC_BRIDGE_REL)
if (!fs.existsSync(src)) return 0
copyDir(src, path.join(destDir, DIY_DYNAMIC_BRIDGE_REL))
return readDiyBridgeComponentCount(src)
}
/** 合并插件包 diy-dynamic-bridge 到框架主包(覆盖框架 appOnly 版 bridge */
function mergeDiyDynamicBridge(targetMpDir, sourcePkgDir) {
const src = path.join(sourcePkgDir, DIY_DYNAMIC_BRIDGE_REL)
if (!fs.existsSync(src)) return 0
const dest = path.join(targetMpDir, DIY_DYNAMIC_BRIDGE_REL)
rmDir(dest)
copyDir(src, dest)
return readDiyBridgeComponentCount(dest)
}
/** 从框架包磁盘扫描已合并的插件 keyaddon/components 除外) */
function listMergedAddonKeysFromFramework(frameworkDir) {
const addonRoot = path.join(frameworkDir, 'addon')
if (!fs.existsSync(addonRoot)) return []
return fs
.readdirSync(addonRoot)
.filter((name) => {
if (name === 'components') return false
const p = path.join(addonRoot, name)
return fs.existsSync(p) && fs.statSync(p).isDirectory()
})
.sort()
}
function resolveMpComponentDir(mpDir, bridgeJsonDir, relPath) {
return path.normalize(path.join(bridgeJsonDir, relPath))
}
/** 校验 bridge usingComponents 指向的编译产物是否存在 */
function validateDiyBridgePaths(frameworkDir) {
const bridgeJsonDir = path.join(frameworkDir, DIY_DYNAMIC_BRIDGE_REL)
const jsonPath = path.join(bridgeJsonDir, 'diy-dynamic-bridge.json')
if (!fs.existsSync(jsonPath)) return []
const using = readJson(jsonPath).usingComponents || {}
const missing = []
for (const [name, rel] of Object.entries(using)) {
const compDir = resolveMpComponentDir(frameworkDir, bridgeJsonDir, rel)
const hasJson = fs.existsSync(path.join(compDir + '.json'))
const hasJs = fs.existsSync(path.join(compDir + '.js'))
if (!hasJson && !hasJs) {
missing.push({ name, rel: rel.replace(/\\/g, '/') })
}
}
return missing
}
/**
* 按已合并插件重新编译 diy-dynamic-bridgeapp + 指定 addon避免引用未合并插件
*/
function rebuildMpMergedDiyBridge(frameworkDir, mergedAddonKeys) {
if (!mergedAddonKeys.length) return 0
const { generateDiyRegistry } = require('./generate-diy-registry.cjs')
const { generateDiyDynamicBridgeMp } = require('./generate-diy-dynamic-bridge-mp.cjs')
const { applyMpWeixinPost } = require('./mp-weixin-post.cjs')
generateDiyRegistry({ addonKeys: mergedAddonKeys })
generateDiyDynamicBridgeMp({ addonKeys: mergedAddonKeys })
console.log(
`[rebuild-mp-diy-bridge] uni build (app + ${mergedAddonKeys.join(', ')}) ...`
)
const code = runUniMpBuild()
if (code !== 0) {
console.error('[rebuild-mp-diy-bridge] FAILED')
process.exit(code)
}
if (!fs.existsSync(MP_OUT_BUILD)) {
console.error('[rebuild-mp-diy-bridge] missing output:', MP_OUT_BUILD)
process.exit(1)
}
const count = mergeDiyDynamicBridge(frameworkDir, MP_OUT_BUILD)
applyMpWeixinPost(frameworkDir)
console.log(`[rebuild-mp-diy-bridge] OK (${count} components)`)
return count
}
/** 框架级路径前缀:这些文件框架包一定提供,插件包不需要携带 */
const FRAMEWORK_PATH_PREFIXES = [
'common/',
'node-modules/',
'app/api/',
'app/stores/',
'hooks/',
'locale/',
'stores/',
'utils/'
]
function isFrameworkDep(relPath) {
return FRAMEWORK_PATH_PREFIXES.some((p) => relPath === p || relPath.startsWith(p))
}
function isUnderMpRoots(relPath, roots) {
return roots.some((root) => relPath === root || relPath.startsWith(`${root}/`))
}
/** 将 mp 根目录下的相对路径文件复制到目标目录(保留相对结构),跳过框架级依赖 */
function copyMpRelativeFiles(srcMpDir, destMpDir, relPaths, skipUnderRoots = []) {
let count = 0
let frameworkSkipped = 0
for (const rel of relPaths) {
if (skipUnderRoots.length && isUnderMpRoots(rel, skipUnderRoots)) continue
if (isFrameworkDep(rel)) { frameworkSkipped++; continue }
const src = path.join(srcMpDir, rel)
if (!fs.existsSync(src)) continue
const dest = path.join(destMpDir, rel)
fs.mkdirSync(path.dirname(dest), { recursive: true })
fs.copyFileSync(src, dest)
count++
}
if (frameworkSkipped) console.log(`[mp-utils] skipped ${frameworkSkipped} framework deps (already in framework)`)
return count
}
/** 扫描插件源码引用的共享 addon/components排除已在框架中的 diy/group */
function scanAddonSharedComponents(addonKey) {
const addonDir = path.join(ROOT, 'src', 'addon', addonKey)
if (!fs.existsSync(addonDir)) return []
const found = new Set()
for (const rel of walkFiles(addonDir, (f) => f.endsWith('.vue'), addonDir)) {
const content = fs.readFileSync(path.join(addonDir, rel), 'utf-8')
let m
ADDON_COMPONENT_IMPORT_RE.lastIndex = 0
while ((m = ADDON_COMPONENT_IMPORT_RE.exec(content)) !== null) {
let comp = m[1]
if (m[2] && !/^index(?:\.vue)?$/.test(m[2])) {
comp = `${m[1]}/${m[2]}`
}
if (comp === 'diy/group') continue
found.add(comp)
}
}
return [...found].sort()
}
function getMpSharedBridgeMetaPath(addonKey) {
return path.join(MP_SHARED_BRIDGE_SRC, addonKey, 'bridge-meta.json')
}
function readMpSharedBridgeMeta(addonKey) {
const metaPath = getMpSharedBridgeMetaPath(addonKey)
const legacyPath = path.join(MP_SHARED_BRIDGE_SRC, addonKey, 'components.json')
if (fs.existsSync(metaPath)) {
const meta = readJson(metaPath)
return {
sharedComponents: meta.sharedComponents || [],
mainComponents: meta.mainComponents || []
}
}
if (fs.existsSync(legacyPath)) {
return { sharedComponents: readJson(legacyPath), mainComponents: [] }
}
return { sharedComponents: [], mainComponents: [] }
}
function readMpSharedBridgeComponents(addonKey) {
return readMpSharedBridgeMeta(addonKey).sharedComponents
}
function mainComponentPathToDir(compPath) {
const parts = compPath.replace(/\.vue$/, '').split('/')
return `components/${parts[0]}`
}
/** 扫描插件引用的主包 src/components含 easycom 标签用法) */
function scanAddonMainComponents(addonKey) {
const addonDir = path.join(ROOT, 'src', 'addon', addonKey)
if (!fs.existsSync(addonDir)) return []
const componentsRoot = path.join(ROOT, 'src', 'components')
const tagToPath = new Map()
if (fs.existsSync(componentsRoot)) {
for (const name of fs.readdirSync(componentsRoot)) {
const full = path.join(componentsRoot, name)
if (!fs.statSync(full).isDirectory()) continue
const vueFile = path.join(full, `${name}.vue`)
const indexVue = path.join(full, 'index.vue')
if (fs.existsSync(vueFile)) tagToPath.set(name, `${name}/${name}`)
else if (fs.existsSync(indexVue)) tagToPath.set(name, `${name}/index`)
}
}
const found = new Set()
for (const rel of walkFiles(
addonDir,
(f) => f.endsWith('.vue') || f.endsWith('.js') || f.endsWith('.ts'),
addonDir
)) {
const content = fs.readFileSync(path.join(addonDir, rel), 'utf-8')
let m
MAIN_COMPONENT_IMPORT_RE.lastIndex = 0
while ((m = MAIN_COMPONENT_IMPORT_RE.exec(content)) !== null) {
let p = m[1]
if (/\.(js|ts)$/.test(p)) continue
p = p.replace(/\.vue$/, '')
if (p.endsWith('/index')) p = p.slice(0, -6)
found.add(p)
}
for (const [tag, compPath] of tagToPath) {
if (new RegExp(`<${tag}[\\s/>]`).test(content)) {
found.add(compPath)
}
}
}
return [...found].sort()
}
function resolveJsonComponentPath(mpRoot, jsonFile, relPath) {
if (!relPath.startsWith('.')) return null
const dir = path.dirname(jsonFile)
let resolved = path.normalize(path.join(dir, relPath))
if (!resolved.endsWith('.js') && !resolved.endsWith('.json')) {
if (fs.existsSync(resolved + '.js')) resolved += '.js'
else if (fs.existsSync(resolved + '.json')) resolved += '.json'
}
if (!resolved.startsWith(mpRoot)) return null
return path.relative(mpRoot, resolved).replace(/\\/g, '/')
}
function walkJsonFiles(dir, base = dir) {
const out = []
if (!fs.existsSync(dir)) return out
for (const name of fs.readdirSync(dir)) {
const full = path.join(dir, name)
if (fs.statSync(full).isDirectory()) {
out.push(...walkJsonFiles(full, base))
} else if (name.endsWith('.json')) {
out.push(path.relative(base, full).replace(/\\/g, '/'))
}
}
return out
}
/** 从编译产物 json 的 usingComponents 解析主包 components/ 目录 */
function scanMpMainComponentDirs(mpDir, roots) {
const dirs = new Set()
for (const root of roots) {
const absRoot = path.join(mpDir, root)
for (const rel of walkJsonFiles(absRoot)) {
const jsonFile = path.join(absRoot, rel)
let data
try {
data = readJson(jsonFile)
} catch {
continue
}
for (const spec of Object.values(data.usingComponents || {})) {
if (typeof spec !== 'string' || !spec.startsWith('.')) continue
const resolved = resolveJsonComponentPath(mpDir, jsonFile, spec)
if (!resolved || !resolved.startsWith('components/')) continue
const parts = resolved.split('/')
if (parts.length >= 2) dirs.add(`${parts[0]}/${parts[1]}`)
}
}
}
return [...dirs].sort()
}
function collectMainComponentDirs(buildDir, roots, bridgeMeta) {
const dirs = new Set(scanMpMainComponentDirs(buildDir, roots))
for (const comp of bridgeMeta.mainComponents || []) {
dirs.add(mainComponentPathToDir(comp))
}
return [...dirs].sort()
}
function writeMpSharedBridgePage(addonKey, sharedComponents, mainComponents = []) {
const bridgeDir = path.join(MP_SHARED_BRIDGE_SRC, addonKey)
fs.mkdirSync(bridgeDir, { recursive: true })
writeJson(getMpSharedBridgeMetaPath(addonKey), { sharedComponents, mainComponents })
const importLines = []
sharedComponents.forEach((comp, index) => {
importLines.push(`import SharedComp${index} from '@/addon/components/${comp}/index.vue'`)
})
mainComponents.forEach((comp, index) => {
importLines.push(`import MainComp${index} from '@/components/${comp}.vue'`)
})
const vue = `<template><view class="mp-shared-bridge"><diy-dynamic-bridge-${addonKey} v-if="false" componentName="" /></view></template>
<script setup lang="ts">
${importLines.join('\n')}
</script>
`
fs.writeFileSync(path.join(bridgeDir, 'index.vue'), vue, 'utf-8')
}
function packageMainComponents(buildDir, destDir, componentDirs) {
let count = 0
for (const dir of componentDirs) {
const src = path.join(buildDir, dir)
if (!fs.existsSync(src)) {
console.warn(`[mp-utils] missing main component dir ${dir}`)
continue
}
copyDir(src, path.join(destDir, dir))
count++
}
return count
}
function mergeMainComponentsDir(targetMpDir, sourceDir, componentDir) {
return mergeOverlayDir(targetMpDir, path.join(sourceDir, componentDir), componentDir)
}
function injectMpSharedBridgePage(pagesJson) {
if (!pagesJson.pages.some((p) => p.path === MP_SHARED_BRIDGE_PAGE)) {
pagesJson.pages.push({
path: MP_SHARED_BRIDGE_PAGE,
style: {
navigationStyle: 'custom',
navigationBarTitleText: ''
}
})
}
return pagesJson
}
function installMpSharedBridgePage(addonKey) {
const bridgeVue = path.join(MP_SHARED_BRIDGE_SRC, addonKey, 'index.vue')
if (!fs.existsSync(bridgeVue)) return false
const dest = path.join(ROOT, 'src', MP_SHARED_BRIDGE_PAGE + '.vue')
fs.mkdirSync(path.dirname(dest), { recursive: true })
fs.copyFileSync(bridgeVue, dest)
return true
}
function removeMpSharedBridgePage() {
const destDir = path.join(ROOT, 'src', 'app', 'pages', '_mp_shared_bridge')
if (fs.existsSync(destDir)) {
rmDir(destDir)
}
}
function packageSharedAddonComponents(buildDir, destDir, components) {
let count = 0
for (const comp of components) {
const rel = `addon/components/${comp}`
const src = path.join(buildDir, rel)
if (!fs.existsSync(src)) {
console.warn(`[mp-utils] missing shared component ${rel}`)
continue
}
copyDir(src, path.join(destDir, rel))
count++
}
return count
}
module.exports = {
ROOT,
BUILD_DIR,
PAGES_JSON,
PAGES_CORE,
MP_OUT_BUILD,
MP_CORE_DIR,
MP_FRAMEWORK_DIR,
MP_FULL_DIR,
MP_ADDONS_DIR,
MP_LOCK_PATH,
listAddonKeys,
parsePagesJson,
resolveFullPagesJson,
hasAddonSubPackages,
rmDir,
copyDir,
sha256File,
hashDirFiles,
walkJsAndJson,
getGitCommit,
makeBuildId,
readJson,
writeJson,
readAppJson,
getSubPackageRootsForKey,
getAllAddonSubPackageRoots,
filterSubPackages,
collectCommonManifest,
runUniMpBuild,
snapshotMpBuild,
stripOrphanAddonSubpackages,
stripAddonSubpackagesExcept,
getAppSubPackages,
setAppSubPackages,
isAddonSubPackageRoot,
loadFrameworkNonAddonSubPackages,
collectPreservedSubPackages,
normalizeSubPackageEntry,
normalizeAppSubPackages,
applyFrameworkAppJsonSubPackages,
mergeSubPackagesIntoApp,
mergePreloadRules,
applyDiyPreloadRules,
buildDiyPreloadRules,
ensureComponentSubpackagePlaceholderMp,
ensureComponentSubpackagePlaceholderPagesSubPackages,
resolvePreloadPackageIds,
MP_COMPONENT_SUBPACKAGE_PLACEHOLDER,
DIY_MICRO_PAGE_PATHS,
DIY_ADDON_PAGE_PATHS,
mergeOverlayDir,
mergeCommonDir,
mergeNodeModulesDir,
packageDiyDynamicBridge,
packageAddonDiyBridge,
mergeDiyDynamicBridge,
listMergedAddonKeysFromFramework,
validateDiyBridgePaths,
rebuildMpMergedDiyBridge,
DIY_DYNAMIC_BRIDGE_REL,
copyMpRelativeFiles,
scanAddonSharedComponents,
scanAddonMainComponents,
writeMpSharedBridgePage,
readMpSharedBridgeMeta,
readMpSharedBridgeComponents,
collectMainComponentDirs,
packageMainComponents,
mergeMainComponentsDir,
scanMpMainComponentDirs,
injectMpSharedBridgePage,
installMpSharedBridgePage,
removeMpSharedBridgePage,
packageSharedAddonComponents,
MP_SHARED_BRIDGE_PAGE,
getAddonPackageDir,
listAddonPackagesInFramework,
installMergeScriptToFramework,
isFrameworkDep,
FRAMEWORK_PATH_PREFIXES
}