/**
* 合并 / 框架构建后:将微页面 DIY 组件内联进 diy-group(替换 diy-dynamic-bridge)
* 开发期源码仍用 diy-dynamic-bridge;仅 dist/build/mp-core 产物生效。
*/
const fs = require('fs')
const path = require('path')
const DIY_GROUP_REL = 'addon/components/diy/group'
/** dev / prod: 或成对标签 */
const DIY_GROUP_BRIDGE_TAG =
/]*(?:\/>|>\s*<\/diy-dynamic-bridge>)/
/** 旧版:外层 component.d + 内联行(升级时替换) */
const DIY_GROUP_INLINE_BLOCK_RE =
/[\s\S]*?<\/block>/
/** 新版:仅 component.q 内联行(无 resolveDiyMeta 门闩) */
const DIY_GROUP_INLINE_LINES_RE =
/( <[a-z0-9-]+ wx:if="\{\{component\.q==='[^']+'\}\}"[\s\S]*?\/>\n?)+/
const DIY_GROUP_BASE_USING = {
'copy-right': '../../../../components/copy-right/copy-right',
tabbar: '../../../../components/tabbar/tabbar',
'top-tabbar': '../../../../components/top-tabbar/top-tabbar',
'pop-ads': '../../../../components/pop-ads/pop-ads'
}
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 getDiyComponentUsingPath(meta) {
if (!meta?.folder) return null
if (meta.scope === 'app' || !meta.addonKey) {
return `../../../../app/components/diy/${meta.folder}/index`
}
return `../../../${meta.addonKey}/components/diy/${meta.folder}/index`
}
function resolveMpComponentDir(jsonDir, relPath) {
return path.normalize(path.join(jsonDir, relPath))
}
/** wx:for-item 上用于路由的内联字段(见 patchDiyGroupJs 注入的 q:o.componentName) */
const DIY_GROUP_INLINE_NAME_KEY = 'q'
function buildInlineWxmlBlock(registryMap, scopeId = 'data-v-ddfdffaf') {
const entries = Object.entries(registryMap || {}).sort(([a], [b]) => a.localeCompare(b))
const nameKey = DIY_GROUP_INLINE_NAME_KEY
const lines = entries.map(([vueName, meta]) => {
const tag = meta.folder
// component.i 是 uni-app u-p 序列化包,WXML 无法读取 component.i.componentName
return ` <${tag} wx:if="{{component.${nameKey}==='${vueName}'}}" class="r-i-f ${scopeId}" u-r="{{component.f}}" bindupdateComponentIsShow="{{component.g}}" u-i="{{component.h}}" bind:__l="__l" u-p="{{component.i}}" />`
})
if (!lines.length) return ''
return `${lines.join('\n')}\n`
}
function replaceInlineWxmlBlock(wxml, inlineBlock) {
if (DIY_GROUP_BRIDGE_TAG.test(wxml)) {
return { wxml: wxml.replace(DIY_GROUP_BRIDGE_TAG, inlineBlock), patched: true }
}
if (DIY_GROUP_INLINE_BLOCK_RE.test(wxml)) {
return { wxml: wxml.replace(DIY_GROUP_INLINE_BLOCK_RE, inlineBlock), patched: true }
}
if (DIY_GROUP_INLINE_LINES_RE.test(wxml)) {
return { wxml: wxml.replace(DIY_GROUP_INLINE_LINES_RE, inlineBlock), patched: true }
}
if (
wxml.includes('component.i.componentName===') ||
wxml.includes(`component.${DIY_GROUP_INLINE_NAME_KEY}===`)
) {
return { wxml, patched: false, error: 'inline block pattern not found' }
}
return { wxml, patched: false, error: 'diy-dynamic-bridge tag not found' }
}
function patchDiyGroupJs(jsContent) {
let out = jsContent
// prod(压缩)
out = out.replace(
/\+\(\(\)=>"\.\.\/\.\.\/\.\.\/\.\.\/components\/diy-dynamic-bridge\/diy-dynamic-bridge\.js"\)\+/g,
'+'
)
out = out.replace(/e\.resolveComponent\("diy-dynamic-bridge"\)\+/g, '')
out = out.replace(
/\(null==\(r=o\.getInnerRef\)\?void 0:r\.call\(o\)\)\?\?null/g,
'(null==(r=o.getInnerRef)?void 0:r.call(o))??o??null'
)
if (!out.includes('q:o.componentName')) {
out = out.replace(/,p:o\.id\}\)\),h:/g, ',p:o.id,q:o.componentName})),h:')
}
out = out.replace(
/\{d:e\.unref\(n\.resolveDiyMeta\)\(o\.componentName\)\},e\.unref\(n\.resolveDiyMeta\)\(o\.componentName\)\?/g,
'{d:!!o.componentName},o.componentName?'
)
// dev(未压缩)
out = out.replace(
/const _easycom_diy_dynamic_bridge2 = common_vendor\.resolveComponent\("diy-dynamic-bridge"\);\n/g,
''
)
out = out.replace(/\(_easycom_diy_dynamic_bridge2 \+ /g, '(')
out = out.replace(/const _easycom_diy_dynamic_bridge = \(\) => "[^"]+";\n/g, '')
out = out.replace(/\(topTabbar \+ popAds \+ _easycom_diy_dynamic_bridge \+ /g, '(topTabbar + popAds + ')
out = out.replace(
/\(\(_a = bridgeEl\.getInnerRef\) == null \? void 0 : _a\.call\(bridgeEl\)\) \?\? null/g,
'((_a = bridgeEl.getInnerRef) == null ? void 0 : _a.call(bridgeEl)) ?? bridgeEl ?? null'
)
if (!out.includes('q: component.componentName') && !out.includes('q:o.componentName')) {
out = out.replace(/p: component\.id\n(\s+)\}\)/g, 'q: component.componentName,\n$1p: component.id\n$1})')
}
out = out.replace(
/d: common_vendor\.unref\(utils_diyComponentRegistry\.resolveDiyMeta\)\(component\.componentName\)\n(\s+)\}, common_vendor\.unref\(utils_diyComponentRegistry\.resolveDiyMeta\)\(component\.componentName\) \? \{/g,
'd: !!component.componentName\n$1}, component.componentName ? {'
)
return out
}
function validateInlineRegistry(frameworkDir, registryMap) {
const jsonDir = path.join(frameworkDir, DIY_GROUP_REL)
const missing = []
for (const [name, meta] of Object.entries(registryMap || {})) {
const rel = getDiyComponentUsingPath(meta)
if (!rel) continue
const compDir = resolveMpComponentDir(jsonDir, rel)
if (!fs.existsSync(`${compDir}.json`) && !fs.existsSync(`${compDir}.js`)) {
missing.push({ name, rel: rel.replace(/\\/g, '/') })
}
}
return missing
}
function filterRegistryToExisting(frameworkDir, registryMap) {
const missing = validateInlineRegistry(frameworkDir, registryMap)
if (!missing.length) return registryMap
const missingNames = new Set(missing.map((m) => m.name))
const filtered = {}
for (const [name, meta] of Object.entries(registryMap || {})) {
if (!missingNames.has(name)) filtered[name] = meta
}
console.warn(
`[mp-diy-group-inline] skip ${missing.length} component(s) not in mp output: ${missing
.slice(0, 6)
.map((m) => m.name)
.join(', ')}`
)
return filtered
}
function buildFullDiyRegistryMap() {
const { scanAllDiyComponents } = require('./diy-component-scan.cjs')
const { all } = scanAllDiyComponents()
const map = {}
for (const entry of all) {
map[entry.vueComponentName] = {
scope: entry.scope,
folder: entry.folder,
addonKey: entry.addonKey,
scrollBool: entry.scrollBool,
formRef: entry.formRef
}
}
return map
}
function loadRegistryJsonMetaByName() {
const { ROOT, BUILD_DIR } = require('./addon-utils.cjs')
const jsonPath = path.join(BUILD_DIR, 'diy-component-registry.json')
const byName = {}
if (!fs.existsSync(jsonPath)) return byName
for (const entry of JSON.parse(fs.readFileSync(jsonPath, 'utf-8')).components || []) {
byName[entry.vueComponentName] = entry
}
return byName
}
/** 扫描 mp-weixin 产物中已编译的 app/addon DIY 目录(含 bridge 拉进来的 addon 组件) */
function scanMpOutputDiyEntries(mpDir) {
const { kebabToPascal } = require('./diy-component-scan.cjs')
const rootDir = path.resolve(mpDir)
const entries = []
function scanDir(diyDir, scope, addonKey) {
if (!fs.existsSync(diyDir)) return
for (const folder of fs.readdirSync(diyDir)) {
if (folder === 'group') continue
const compDir = path.join(diyDir, folder)
if (!fs.statSync(compDir).isDirectory()) continue
if (!fs.existsSync(path.join(compDir, 'index.json')) && !fs.existsSync(path.join(compDir, 'index.js'))) {
continue
}
entries.push({
vueComponentName: kebabToPascal(folder),
scope,
folder,
addonKey
})
}
}
scanDir(path.join(rootDir, 'app/components/diy'), 'app', undefined)
const addonRoot = path.join(rootDir, 'addon')
if (fs.existsSync(addonRoot)) {
for (const name of fs.readdirSync(addonRoot)) {
const addonPath = path.join(addonRoot, name)
if (!fs.statSync(addonPath).isDirectory()) continue
scanDir(path.join(addonPath, 'components/diy'), 'addon', name)
}
}
return entries
}
function buildMpWeixinDiyRegistryMap(mpDir) {
const metaByName = loadRegistryJsonMetaByName()
const map = {}
for (const entry of scanMpOutputDiyEntries(mpDir)) {
const meta = metaByName[entry.vueComponentName] || {}
map[entry.vueComponentName] = {
scope: entry.scope,
folder: entry.folder,
addonKey: entry.addonKey || meta.addonKey,
scrollBool:
meta.scrollBool ??
(entry.scope === 'addon' || entry.vueComponentName === 'CarouselSearch'),
formRef:
meta.formRef ??
(entry.vueComponentName.startsWith('Form') && entry.vueComponentName !== 'FormSubmit')
}
}
return map
}
function patchMpWeixinDistRouterBridges(mpDir) {
const componentsDir = path.join(path.resolve(mpDir), 'components')
if (!fs.existsSync(componentsDir)) return []
const { appendRouterAddonBlock, listRouterAddonKeys } = require('./mp-diy-bridge-merge.cjs')
const patched = []
for (const name of fs.readdirSync(componentsDir)) {
if (!name.startsWith('diy-dynamic-bridge-') || name === 'diy-dynamic-bridge-app') continue
const key = name.slice('diy-dynamic-bridge-'.length)
if (listRouterAddonKeys(mpDir).includes(key)) continue
if (appendRouterAddonBlock(mpDir, key)) patched.push(key)
}
if (patched.length) {
console.log(`[mp-diy-group-inline] patched dist router bridges: ${patched.join(', ')}`)
}
return patched
}
function writeMergedRegistryFile(mpDir, registryMap) {
const mergedPath = path.join(
path.resolve(mpDir),
'components/diy-dynamic-bridge/diy-registry.merged.js'
)
fs.mkdirSync(path.dirname(mergedPath), { recursive: true })
fs.writeFileSync(mergedPath, `module.exports = ${JSON.stringify(registryMap || {}, null, 2)}\n`, 'utf-8')
}
/**
* uni dev/build 产物(dist/dev|build/mp-weixin):按产物扫描 app+addon DIY 并内联 diy-group
* @param {string} mpDir dist/dev/mp-weixin 或 dist/build/mp-weixin
* @param {{ patchDistRouter?: boolean }} options
*/
function applyMpWeixinDiyGroupInline(mpDir, options = {}) {
if (options.patchDistRouter !== false) {
patchMpWeixinDistRouterBridges(mpDir)
}
const registryMap = buildMpWeixinDiyRegistryMap(mpDir)
const appCount = Object.values(registryMap).filter((m) => m.scope === 'app').length
const addonCount = Object.values(registryMap).filter((m) => m.scope === 'addon').length
console.log(
`[mp-diy-group-inline] mp output registry: app=${appCount}, addon=${addonCount}, total=${Object.keys(registryMap).length}`
)
if (!Object.keys(registryMap).length) {
console.warn('[mp-diy-group-inline] no diy components found in mp output')
return { count: 0, skipped: true }
}
writeMergedRegistryFile(mpDir, registryMap)
return patchDiyGroupInlineComponents(mpDir, registryMap)
}
/**
* @param {string} frameworkDir dist/build/mp-core
* @param {Record} registryMap vueComponentName -> meta
*/
function patchDiyGroupInlineComponents(frameworkDir, registryMap) {
const rootDir = path.resolve(frameworkDir)
const groupDir = path.join(rootDir, DIY_GROUP_REL)
const jsonPath = path.join(groupDir, 'index.json')
const wxmlPath = path.join(groupDir, 'index.wxml')
const jsPath = path.join(groupDir, 'index.js')
if (!fs.existsSync(jsonPath) || !fs.existsSync(wxmlPath) || !fs.existsSync(jsPath)) {
console.warn('[mp-diy-group-inline] skip: diy-group not found in', frameworkDir)
return { count: 0, skipped: true }
}
const missing = validateInlineRegistry(rootDir, registryMap)
if (missing.length) {
const sample = missing
.slice(0, 8)
.map((m) => `${m.name} -> ${m.rel}`)
.join(', ')
throw new Error(`[mp-diy-group-inline] missing diy components: ${sample}`)
}
const usingComponents = { ...DIY_GROUP_BASE_USING }
let count = 0
for (const meta of Object.values(registryMap || {})) {
const rel = getDiyComponentUsingPath(meta)
if (!rel || usingComponents[meta.folder]) continue
usingComponents[meta.folder] = rel
count++
}
const componentPlaceholder = {}
for (const key of Object.keys(usingComponents)) {
componentPlaceholder[key] = 'view'
}
writeJson(jsonPath, {
component: true,
usingComponents,
componentPlaceholder
})
let wxml = fs.readFileSync(wxmlPath, 'utf-8')
const scopeMatch = wxml.match(/data-v-[a-f0-9]+/)
const inlineBlock = buildInlineWxmlBlock(registryMap, scopeMatch ? scopeMatch[0] : 'data-v-ddfdffaf')
if (!inlineBlock) {
throw new Error('[mp-diy-group-inline] empty registry, nothing to inline')
}
const replaceResult = replaceInlineWxmlBlock(wxml, inlineBlock)
if (replaceResult.error) {
throw new Error(`[mp-diy-group-inline] ${replaceResult.error}`)
}
if (replaceResult.patched) {
fs.writeFileSync(wxmlPath, replaceResult.wxml, 'utf-8')
console.log(
`[mp-diy-group-inline] diy-group wxml inlined ${Object.keys(registryMap || {}).length} component(s) (component.q, no component.d gate)`
)
}
const jsPatched = patchDiyGroupJs(fs.readFileSync(jsPath, 'utf-8'))
fs.writeFileSync(jsPath, jsPatched, 'utf-8')
const { installMergedRegistryResolver } = require('./mp-diy-registry-runtime.cjs')
installMergedRegistryResolver(rootDir)
return { count, skipped: false }
}
module.exports = {
DIY_GROUP_REL,
DIY_GROUP_BASE_USING,
getDiyComponentUsingPath,
patchDiyGroupInlineComponents,
applyMpWeixinDiyGroupInline,
buildMpWeixinDiyRegistryMap,
buildFullDiyRegistryMap,
patchMpWeixinDistRouterBridges,
buildInlineWxmlBlock,
validateInlineRegistry
}