/** * 微信小程序双产物构建:路径、分包映射、文件工具 */ 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`, '\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) } /** 从框架包磁盘扫描已合并的插件 key(addon/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-bridge(app + 指定 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 = ` ` 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 }