/** * 在框架小程序目录内执行,自动扫描 mp-addons/ 并合并插件分包 * 纯拷贝 + router 补丁,无需 uni 源码与编译(支持 mp-core 独立分发) * * 用法(在 dist/build/mp-core/ 目录下): * node merge-mp-addon.cjs * node merge-mp-addon.cjs shop pintuan */ const fs = require('fs') const path = require('path') function resolveFrameworkDir() { const cwd = process.cwd() if (fs.existsSync(path.join(cwd, 'app.json'))) { return cwd } const arg = process.argv[2] if (arg && !arg.startsWith('-') && fs.existsSync(path.join(path.resolve(arg), 'app.json'))) { return path.resolve(arg) } return null } function loadMergeDeps(scriptDir) { const libDir = path.join(scriptDir, 'lib') if (fs.existsSync(path.join(libDir, 'mp-utils.cjs'))) { return { libDir, utils: require(path.join(libDir, 'mp-utils.cjs')), bridge: require(path.join(libDir, 'mp-diy-bridge-merge.cjs')), validateRequires: require(path.join(libDir, 'scan-mp-requires.cjs')).validateRequires, applyMpWeixinPost: require(path.join(libDir, 'mp-weixin-post.cjs')).applyMpWeixinPost, scanAllDiyComponents: null } } let dir = path.resolve(scriptDir) while (true) { if ( fs.existsSync(path.join(dir, 'package.json')) && fs.existsSync(path.join(dir, 'scripts', 'mp-utils.cjs')) ) { return { libDir: path.join(dir, 'scripts'), utils: require(path.join(dir, 'scripts/mp-utils.cjs')), bridge: require(path.join(dir, 'scripts/mp-diy-bridge-merge.cjs')), validateRequires: require(path.join(dir, 'scripts/scan-mp-requires.cjs')).validateRequires, applyMpWeixinPost: require(path.join(dir, 'scripts/mp-weixin-post.cjs')).applyMpWeixinPost, scanAllDiyComponents: require(path.join(dir, 'scripts/diy-component-scan.cjs')) .scanAllDiyComponents } } const parent = path.dirname(dir) if (parent === dir) break dir = parent } return null } function listAddonPackages(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((key) => fs.existsSync(path.join(pkgRoot, key, 'manifest.json'))) .sort() } function main() { const deps = loadMergeDeps(__dirname) if (!deps) { console.error('[merge-mp-addon] 缺少 lib/ 合并工具,请重新 build:mp-weixin:framework') process.exit(1) } const { utils, bridge, validateRequires, applyMpWeixinPost, scanAllDiyComponents } = deps const { MP_OUT_BUILD, MP_CORE_DIR, readJson, writeJson, readAppJson, rmDir, copyDir, mergeSubPackagesIntoApp, mergeCommonDir, mergeNodeModulesDir, mergeOverlayDir, patchVendorPropsStubs, copyMpRelativeFiles, getAppSubPackages, loadFrameworkNonAddonSubPackages, collectPreservedSubPackages, mergeMainComponentsDir, listMergedAddonKeysFromFramework } = utils const frameworkDir = resolveFrameworkDir() if (!frameworkDir) { console.error('[merge-mp-addon] 请在框架小程序目录执行(含 app.json),例如:') console.error(' cd dist/build/mp-core') console.error(' node merge-mp-addon.cjs') process.exit(1) } const pkgRoot = path.join(frameworkDir, 'mp-addons') const available = listAddonPackages(frameworkDir) const argvKeys = process.argv.slice(2).filter((a) => !a.startsWith('-')) const keys = argvKeys.length > 0 ? argvKeys.filter((k) => available.includes(k)) : available if (!available.length) { console.log('[merge-mp-addon] mp-addons/ 下未发现插件包(需含 manifest.json)') console.log(`[merge-mp-addon] 请将插件包放入: ${pkgRoot}/{key}/`) process.exit(0) } if (!keys.length) { console.error('[merge-mp-addon] 指定的插件不存在于 mp-addons/:', argvKeys.join(', ')) console.error('[merge-mp-addon] 可用插件:', available.join(', ')) process.exit(1) } console.log(`[merge-mp-addon] framework: ${frameworkDir}`) console.log(`[merge-mp-addon] merge keys: ${keys.join(', ')}`) console.log('[merge-mp-addon] mode: copy-only (no uni build)') function loadManifest(key) { return readJson(path.join(pkgRoot, key, 'manifest.json')) } function mergeOneAddon(key) { const pkgDir = path.join(pkgRoot, key) const manifest = loadManifest(key) for (const root of manifest.roots) { const src = path.join(pkgDir, root) const dest = path.join(frameworkDir, root) if (!fs.existsSync(src)) { console.error(`[merge-mp-addon] missing ${root} in ${key}`) process.exit(1) } rmDir(dest) copyDir(src, dest) console.log(`[merge-mp-addon] copied ${root}`) } const commonSrc = path.join(pkgDir, 'common') const mergedCommon = mergeCommonDir(frameworkDir, commonSrc) if (mergedCommon) { console.log(`[merge-mp-addon] merged ${mergedCommon} common file(s) from ${key}`) } const nodeModulesSrc = path.join(pkgDir, 'node-modules') const mergedNodeModules = mergeNodeModulesDir(frameworkDir, nodeModulesSrc, key) if (mergedNodeModules) { console.log(`[merge-mp-addon] merged ${mergedNodeModules} node-modules file(s) from ${key}`) } if (manifest.deps?.requirePaths?.length) { const mergedDeps = copyMpRelativeFiles( pkgDir, frameworkDir, manifest.deps.requirePaths, manifest.roots ) if (mergedDeps) { console.log(`[merge-mp-addon] merged ${mergedDeps} main-package dep file(s) from ${key}`) } } for (const comp of manifest.sharedComponents || []) { const rel = `addon/components/${comp}` const src = path.join(pkgDir, rel) if (!fs.existsSync(src)) { console.warn(`[merge-mp-addon] missing shared component ${rel} in ${key}`) continue } const merged = mergeOverlayDir(frameworkDir, src, rel) if (merged) { console.log(`[merge-mp-addon] merged addon/components/${comp} (${merged} file(s))`) } } for (const dir of manifest.mainComponentDirs || []) { const src = path.join(pkgDir, dir) if (!fs.existsSync(src)) { console.warn(`[merge-mp-addon] missing main component dir ${dir} in ${key}`) continue } const merged = mergeMainComponentsDir(frameworkDir, pkgDir, dir) if (merged) { console.log(`[merge-mp-addon] merged ${dir} (${merged} file(s)) from ${key}`) } } return manifest } const appPath = path.join(frameworkDir, 'app.json') const appJson = readAppJson(frameworkDir) const manifests = keys.map((key) => mergeOneAddon(key)) const baselineNonAddon = loadFrameworkNonAddonSubPackages(frameworkDir) if (baselineNonAddon.length) { mergeSubPackagesIntoApp(appJson, baselineNonAddon) console.log(`[merge-mp-addon] preserved ${baselineNonAddon.length} framework non-addon subPackage(s)`) } const preserved = collectPreservedSubPackages(appJson, frameworkDir, keys, pkgRoot) if (preserved.length) { mergeSubPackagesIntoApp(appJson, preserved) console.log( `[merge-mp-addon] preserved ${preserved.length} existing subPackage(s): ${preserved.map((s) => s.root).join(', ')}` ) } for (const m of manifests) { mergeSubPackagesIntoApp(appJson, m.subPackages || []) } writeJson(appPath, appJson) console.log(`[merge-mp-addon] app.json subPackages: ${getAppSubPackages(appJson).length}`) // 以下步骤必须在 require 校验之前执行(校验失败 process.exit 会跳过后续逻辑): // 1. placeholder 页 + preloadRule + componentPlaceholder applyMpWeixinPost(frameworkDir) // 2. vendor.js props$XX stubs 注入 const propsStubCount = patchVendorPropsStubs(frameworkDir) if (propsStubCount) { console.log(`[merge-mp-addon] injected ${propsStubCount} props stubs (prevents addon component crashes)`) } // 3. DIY bridge router 合并 + diy-group 内联 const mergedAddonKeys = listMergedAddonKeysFromFramework(frameworkDir) try { const bridgeResult = bridge.syncMergedDiyBridgeRouter(frameworkDir, pkgRoot, mergedAddonKeys, { scanAllDiyComponents: scanAllDiyComponents || undefined }) if (bridgeResult.routerAddons.length) { console.log( `[merge-mp-addon] diy router patched: app + ${bridgeResult.routerAddons.join(', ')} (${bridgeResult.componentCount} addon diy components)` ) } if (bridgeResult.diyGroupInlineCount) { console.log( `[merge-mp-addon] diy-group inlined ${bridgeResult.diyGroupInlineCount} diy component(s) (app + merged addons)` ) } } catch (err) { console.error(String(err.message || err)) process.exit(1) } // 4. require 校验(仅警告,不退出;addon 单独合并时通常没有跨插件引用) const allRequirePaths = new Set() const allForbidden = new Set() for (const m of manifests) { for (const p of m.deps?.requirePaths || []) allRequirePaths.add(p) for (const p of m.deps?.forbiddenPaths || []) allForbidden.add(p) } const validation = validateRequires(frameworkDir, [...allRequirePaths], [...allForbidden]) if (!validation.ok) { if (validation.missing.length) { console.error('[merge-mp-addon] missing require targets:', validation.missing.slice(0, 20)) } if (validation.forbiddenPaths.length) { console.error('[merge-mp-addon] cross-addon requires:', validation.forbiddenPaths) } // 不 exit:DIY 组件内联、placeholder 等已完成,跨插件引用不影响已合并的 addon } else { console.log('[merge-mp-addon] require validation OK') } writeJson(path.join(frameworkDir, 'mp-merge-meta.json'), { time: new Date().toISOString(), mergedKeys: mergedAddonKeys, subPackageCount: getAppSubPackages(appJson).length, mergeMode: 'copy-only' }) const projectRoot = deps.scanAllDiyComponents ? path.dirname(path.dirname(deps.libDir === path.join(__dirname, 'lib') ? __dirname : deps.libDir)) : null const defaultMpCore = projectRoot ? MP_CORE_DIR : null if (!defaultMpCore || path.resolve(frameworkDir) !== path.resolve(defaultMpCore) || !MP_OUT_BUILD) { console.log('[merge-mp-addon] OK(未同步 dist/build/mp-weixin)') console.log('[merge-mp-addon] 导入微信开发者工具:', frameworkDir) return } rmDir(MP_OUT_BUILD) copyDir(frameworkDir, MP_OUT_BUILD) applyMpWeixinPost(MP_OUT_BUILD) console.log(`[merge-mp-addon] synced -> ${path.relative(projectRoot, MP_OUT_BUILD)}/`) console.log('[merge-mp-addon] 导入微信开发者工具: dist/build/mp-core/ 或 dist/build/mp-weixin/') } main()