Files
niucloud-saas/uni-app/scripts/mp-diy-group-inline.cjs
niuzhiyun e4f780a152 v2.0.1
2026-08-04 14:27:05 +08:00

388 lines
15 KiB
JavaScript
Raw Permalink 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.
/**
* 合并 / 框架构建后:将微页面 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<diy-dynamic-bridge ... /> 或成对标签 */
const DIY_GROUP_BRIDGE_TAG =
/<diy-dynamic-bridge[^>]*(?:\/>|>\s*<\/diy-dynamic-bridge>)/
/** 旧版:外层 component.d + 内联行(升级时替换) */
const DIY_GROUP_INLINE_BLOCK_RE =
/<block wx:if="\{\{component\.d\}\}">[\s\S]*?<\/block>/
/** 新版:仅 component.q 内联行(无 resolveDiyMeta 门闩) */
const DIY_GROUP_INLINE_LINES_RE =
/( <[a-z0-9-]+ wx:if="\{\{component\.q==='[^']+'\}\}"[\s\S]*?\/>\n?)+/
/** 内联块唯一标记:用于重复 merge 时幂等替换(避免同名组件被累积渲染多次) */
const DIY_GROUP_INLINE_WRAP_RE = /<!--\s*DIY_INLINE_START\s*-->[\s\S]*?<!--\s*DIY_INLINE_END\s*-->/
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 `<!-- DIY_INLINE_START -->\n${lines.join('\n')}\n<!-- DIY_INLINE_END -->`
}
function replaceInlineWxmlBlock(wxml, inlineBlock) {
// 首次:替换 <diy-dynamic-bridge /> 占位符,并用标记包裹内联块
if (DIY_GROUP_BRIDGE_TAG.test(wxml)) {
return { wxml: wxml.replace(DIY_GROUP_BRIDGE_TAG, `\n${inlineBlock}\n`), patched: true }
}
// 已内联(带标记):直接替换标记区间,保证重复 merge 幂等、不累积
if (DIY_GROUP_INLINE_WRAP_RE.test(wxml)) {
return { wxml: wxml.replace(DIY_GROUP_INLINE_WRAP_RE, inlineBlock), patched: true }
}
// 旧版产物(无标记,可能因历史重复 merge 累积):清除全部历史内联行后重新写入
if (
DIY_GROUP_INLINE_BLOCK_RE.test(wxml) ||
wxml.includes(`component.${DIY_GROUP_INLINE_NAME_KEY}===`)
) {
let cleaned = wxml
.replace(DIY_GROUP_INLINE_BLOCK_RE, '')
.replace(/<[a-zA-Z0-9_-]+ wx:if="\{\{component\.q==='[^']+'\}\}"[^>]*\/>\n?/g, '')
// 在 diy-group 浮层视图component.a之后插入内联块
cleaned = cleaned.replace(
/(<view\s+wx:if="\{\{component\.a\}\}"[^>]*><\/view>)/,
`$1\n${inlineBlock}`
)
return { wxml: cleaned, 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<string, object>} 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
}