/**
* 组装最终可部署 dist/
*
* 将 staging 目录合并为站点可访问结构:
* dist/.addons/* → dist/assets/addons/{key}/
* dist/.core/* → dist/(index.html、assets 等,跳过 addons 子目录)
* dist/.shared/* → dist/assets/shared/
*
* 并注入 Import Map、修正 /admin 路径、修复 admin-lang import、剥离多余 style import。
*
* 用法:npm run build:assemble(需先 build-shared + build:core + build-addon)
*/
const fs = require('fs')
const path = require('path')
const { ROOT, listAddonKeys } = require('./addon-utils.cjs')
const { IMPORT_MAP } = require('./shared-external.cjs')
const { stripBuiltAssets } = require('./strip-style-imports.cjs')
const CORE_DIR = path.join(ROOT, 'dist', '.core')
const ADDONS_STAGING = path.join(ROOT, 'dist', '.addons')
const OUT_DIR = path.join(ROOT, 'dist')
const SHARED_DIR = path.join(ROOT, 'dist', '.shared')
const REPORT_PATH = path.join(ROOT, 'build-report.json')
function toWinLongPath(dir) {
if (process.platform !== 'win32') return dir
if (dir.startsWith('\\\\?\\')) return dir
return `\\\\?\\${path.resolve(dir)}`
}
function rmDir(dir) {
if (!fs.existsSync(dir)) return
try {
fs.rmSync(toWinLongPath(dir), { recursive: true, force: true, maxRetries: 8, retryDelay: 300 })
} catch {
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 8, retryDelay: 300 })
}
}
/** Windows 下 rename 容易因文件锁失败,加重试 + 降级为 copy+delete */
function safeRename(src, dest, maxRetries = 5) {
for (let i = 0; i < maxRetries; i++) {
try {
fs.renameSync(src, dest)
return
} catch (e) {
if (e.code === 'EPERM' || e.code === 'EBUSY') {
if (i < maxRetries - 1) {
console.log(`[assemble] rename busy, retry ${i + 1}/${maxRetries}...`)
const s = Date.now(); while (Date.now() - s < 500) {}
} else {
// 最终降级:copy + delete
copyDir(src, dest)
rmDir(src)
console.log(`[assemble] rename failed, fallback to copy+delete`)
}
} else {
throw e
}
}
}
}
function copyDir(src, dest) {
fs.mkdirSync(dest, { recursive: true })
for (const name of fs.readdirSync(src)) {
const s = path.join(src, name)
const d = path.join(dest, name)
if (fs.statSync(s).isDirectory()) copyDir(s, d)
else fs.copyFileSync(s, d)
}
}
function copyTree(src, dest) {
fs.mkdirSync(path.dirname(dest), { recursive: true })
fs.cpSync(src, dest, { recursive: true, force: true })
}
/**
* 清理 dist/assets 下旧的 core 产物,保留 addons(shared 随后从 .shared 重拷)
* Vite 每次构建 chunk hash 会变,不清理会留下上一版同名不同 hash 的孤儿文件
*/
function cleanCoreAssetsOutput() {
const outAssets = path.join(OUT_DIR, 'assets')
if (!fs.existsSync(outAssets)) {
fs.mkdirSync(outAssets, { recursive: true })
return
}
const addonsSrc = path.join(outAssets, 'addons')
const addonsTmp = path.join(OUT_DIR, '.addons_staging')
if (fs.existsSync(addonsTmp)) rmDir(addonsTmp)
if (fs.existsSync(addonsSrc)) {
safeRename(addonsSrc, addonsTmp)
}
if (fs.existsSync(outAssets)) {
const staleAssets = path.join(ROOT, '.build', '.assets_stale_' + Date.now())
try { safeRename(outAssets, staleAssets) } catch { rmDir(outAssets) }
}
fs.mkdirSync(outAssets, { recursive: true })
if (fs.existsSync(addonsTmp)) {
safeRename(addonsTmp, path.join(outAssets, 'addons'))
}
}
/**
* 合并 core 产物到 dist 根目录
* 注意:跳过 core/assets/addons,避免覆盖已同步的插件目录
*/
function mergeCoreOutput() {
for (const name of ['index.html', 'manifest.json', 'niucloud.ico']) {
const src = path.join(CORE_DIR, name)
if (fs.existsSync(src)) {
fs.copyFileSync(src, path.join(OUT_DIR, name))
console.log(`[assemble] core ${name}`)
}
}
const ueditor = path.join(CORE_DIR, 'ueditor')
if (fs.existsSync(ueditor)) {
copyTree(ueditor, path.join(OUT_DIR, 'ueditor'))
console.log('[assemble] core ueditor/')
}
const coreAssets = path.join(CORE_DIR, 'assets')
const outAssets = path.join(OUT_DIR, 'assets')
if (!fs.existsSync(coreAssets)) return
fs.mkdirSync(outAssets, { recursive: true })
for (const name of fs.readdirSync(coreAssets)) {
if (name === 'addons') continue
const src = path.join(coreAssets, name)
const dest = path.join(outAssets, name)
if (fs.statSync(src).isDirectory()) {
copyDir(src, dest)
} else {
fs.copyFileSync(src, dest)
}
console.log(`[assemble] core assets/${name}`)
}
}
function copyLang(key, destAddonDir) {
const langSrc = path.join(ROOT, 'src', 'addon', key, 'lang')
if (!fs.existsSync(langSrc)) return
const langDest = path.join(destAddonDir, 'lang')
copyDir(langSrc, langDest)
}
function writeAddonManifest(key, destAddonDir) {
const manifest = {
key,
version: '1.0.0',
sharedVersion: 'admin-core-1.0.0',
entry: './index.js',
langBase: './lang/'
}
fs.writeFileSync(path.join(destAddonDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf-8')
}
/** Vite 默认相对路径 ./assets/ → 部署绝对路径 /admin/assets/ */
function fixIndexHtml() {
const fn = path.join(OUT_DIR, 'index.html')
if (!fs.existsSync(fn)) return
let text = fs.readFileSync(fn, 'utf-8')
text = text.replaceAll('./assets/', '/admin/assets/')
text = text.replace('./niucloud.ico', '/admin/niucloud.ico')
fs.writeFileSync(fn, text, 'utf-8')
}
/** 注入 Import Map,使 vue / @/lang 等解析到 shared 单例 */
function injectImportMap() {
const fn = path.join(OUT_DIR, 'index.html')
if (!fs.existsSync(fn)) return
let text = fs.readFileSync(fn, 'utf-8')
const script = ``
if (text.includes('type="importmap"')) {
text = text.replace(/