/** * 组装最终可部署 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(/