背景:
之前 vue.config.js 顶层 IIFE 在任何 require 场景都会跑 fixSubpackageModuleNames
+ runSyncCheck, 不区分平台。导致 mp-weixin / h5 编译时也会修改 5 个
mp-harmony 子包的 .json5 文件, 污染 git status (虽然不是破坏性修改,
但用户看不到为什么改了这些文件)。
改动:
vue.config.js 顶层 IIFE 入口加平台判断:
if (process.env.UNI_PLATFORM !== 'mp-harmony') {
return; // 非鸿蒙场景直接跳过
}
UNI_PLATFORM === 'mp-harmony' 是 uniapp 编译器在编译鸿蒙时设置的环境变量,
HBuilderX 选 "发行 → 鸿蒙元服务" 会设此变量, 编译 mp-weixin / h5 时不会设。
顺便清理了一个 dead code 块:
if (... && __TEST_GATE_BLOCKED__) return;
搜索全文件, __TEST_GATE_BLOCKED__ 没有任何地方被设置, 这个判断永远不成立。
端到端验证 (3 个测试场景):
1. UNI_PLATFORM=mp-weixin → 修子包逻辑跳过, name 保持 ""
2. UNI_PLATFORM=mp-harmony → 修子包 + 跑 sync check, 5 个 name 恢复正确
3. UNI_PLATFORM 不设 (默认) → 修子包逻辑跳过, name 保持 ""
影响:
- HBuilderX "发行 → 鸿蒙元服务" 行为不变 (修复 + sync check 仍自动跑)
- HBuilderX "发行 → 微信小程序" / "运行 → H5" 行为更干净
(不会无意义改 mp-harmony 子包文件)
- 命令行 vue-cli-service build 同理, 避免 mp-weixin / h5 编译后
git status 出现误改提示
776 lines
31 KiB
JavaScript
776 lines
31 KiB
JavaScript
const path = require('path');
|
||
const fs = require('fs');
|
||
const { execSync } = require('child_process');
|
||
const { zionUniMpLoadPackagePlugin } = require('zion-uniapp-mp-load-package/webpack');
|
||
|
||
/**
|
||
* 判断当前是否为"正式包发行"构建。
|
||
* 触发任一条件即视为生产构建,执行 test-gate 门禁:
|
||
* - NODE_ENV === 'production': vue-cli-service build / uni build --mode production
|
||
* - UNI_SCRIPT 已设置: HBuilderX 通过 package.json -> uni-app.scripts 触发的发行
|
||
* 两条信号覆盖后,无论走 HBuilderX 发行按钮、vue-cli-service build、还是 uni-cli,
|
||
* 都无法绕开门禁(因为都会 require 本文件)。
|
||
*/
|
||
function _isProductionRelease() {
|
||
if (process.env.NODE_ENV === 'production') return true;
|
||
if (process.env.UNI_SCRIPT) return true;
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 生产构建门禁: 同步执行 test-gate.js, 失败立即终止构建 (零产物)。
|
||
* 与 package.json 的 "prebuild": "npm run test" 互补 —— 那条只覆盖 npm run build,
|
||
* 本函数覆盖所有会 require vue.config.js 的入口 (HBuilderX / vue-cli-service / uni-cli)。
|
||
*
|
||
* 关键: 使用 process.execPath 而非字符串 "node",避免依赖 PATH 查找 node。
|
||
* 原因: HBuilderX 等 IDE 启动子进程时,PATH 中可能根本没有 node 条目 (本项目
|
||
* 就曾因 Machine PATH 残留指向 v14.21.3 的 Huawei node 而触发问题)。
|
||
* process.execPath 始终是"当前正在运行 vue.config.js 的那个 node 二进制",
|
||
* HBuilderX 自带 node (v22.22.2) ≥ 18,天然满足 test-gate 的版本要求。
|
||
*/
|
||
function _enforceTestGate() {
|
||
if (!_isProductionRelease()) return; // 开发态 / 运行态不拦截
|
||
console.log(`\n🛡️ [test-gate] 检测到生产构建,先执行测试门禁 (${process.versions.node})...`);
|
||
try {
|
||
execSync(`"${process.execPath}" scripts/test-gate.js`, {
|
||
cwd: __dirname,
|
||
stdio: 'inherit',
|
||
});
|
||
console.log('🛡️ [test-gate] ✅ 测试通过,放行生产构建\n');
|
||
} catch (e) {
|
||
console.error('\n❌ [test-gate] 测试未通过,已阻止生产构建');
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
_enforceTestGate();
|
||
|
||
console.log(`[vue.config] 已加载 | node ${process.versions.node} @ ${process.execPath}`);
|
||
|
||
// Build-time: 给所有 page 注入 diy componentPlaceholder
|
||
// 见 scripts/inject-placeholder.js
|
||
require('./scripts/inject-placeholder.js');
|
||
|
||
// 确保 UNI_APP_DOMAIN 有值(从 package.json 脚本配置中读取)
|
||
if (!process.env.UNI_APP_DOMAIN && process.env.UNI_SCRIPT) {
|
||
try {
|
||
const pkg = require('./package.json');
|
||
const scriptCfg = pkg['uni-app']?.scripts?.[process.env.UNI_SCRIPT];
|
||
if (scriptCfg?.env?.UNI_APP_DOMAIN) {
|
||
process.env.UNI_APP_DOMAIN = scriptCfg.env.UNI_APP_DOMAIN;
|
||
}
|
||
} catch (e) { }
|
||
}
|
||
|
||
// 输出当前构建的平台
|
||
console.log(`🏠 当前构建的平台: [${process.env.UNI_PLATFORM || '(未知)'}]\n`);
|
||
|
||
// 输出 H5 域名信息到控制台
|
||
if (process.env.UNI_PLATFORM === 'h5') {
|
||
console.log(`\n[H5 发行] 脚本: ${process.env.UNI_SCRIPT || '(默认)'}`);
|
||
console.log(`[H5 发行] 域名: ${process.env.UNI_APP_DOMAIN || '(未配置)'}\n`);
|
||
}
|
||
|
||
// 打印出process.env
|
||
// for (let key in process.env) {
|
||
// console.log(key, process.env[key]);
|
||
// }
|
||
|
||
// 定义UNI_OUTPUT_DIR环境变量(H5 自定义发行脚本使用/及其他构建处理)
|
||
try {
|
||
if (!process.env.NODE_ENV) {
|
||
console.error(`❌ NODE_ENV 环境变量未设置`);
|
||
throw new Error('NODE_ENV 环境变量未设置');
|
||
}
|
||
|
||
const buildDir = process.env.NODE_ENV === 'production' ? 'build' : 'dev';
|
||
const platform = process.env.UNI_PLATFORM || '';
|
||
const UNI_SCRIPT = process.env.UNI_SCRIPT ?? '';
|
||
|
||
// 针对鸿蒙元服务的发布,先清理构建输出目录
|
||
if (platform == 'mp-harmony') {
|
||
try {
|
||
const assetDir = path.join(__dirname, `unpackage/dist/${buildDir}/.mp-harmony`);
|
||
console.log(`🧹 清理构建目录 ${assetDir}\n`);
|
||
fs.rmSync(assetDir, { recursive: true, force: true });
|
||
} catch (e) {}
|
||
}
|
||
|
||
// 所有 H5 自定义脚本统一处理,无需在 vue.config.js 中硬编码脚本名
|
||
if (platform === 'h5' && UNI_SCRIPT) {
|
||
// 从 manifest.json 读取 h5.router.base,拼接到输出路径
|
||
let baseSuffix = '';
|
||
try {
|
||
const manifestPath = path.join(__dirname, 'manifest.json');
|
||
baseSuffix = _readManifestBase(manifestPath);
|
||
} catch (e) { }
|
||
// 从脚本 env 中获取域名(优先使用 UNI_APP_DOMAIN,否则从 UNI_SCRIPT 提取)
|
||
let domain = '';
|
||
if (process.env.UNI_APP_DOMAIN) {
|
||
try {
|
||
domain = new URL(process.env.UNI_APP_DOMAIN).hostname;
|
||
} catch (e) {
|
||
domain = process.env.UNI_APP_DOMAIN.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
|
||
}
|
||
} else {
|
||
domain = UNI_SCRIPT.replace(/^h5-/, '');
|
||
}
|
||
// 格式: {域名}-{base用.连接}
|
||
const outputDirName = baseSuffix ? `${domain}-${baseSuffix}` : domain;
|
||
process.env.UNI_OUTPUT_DIR = path.join(__dirname, `unpackage/dist/${buildDir}/h5/${outputDirName}`);
|
||
console.log('UNI_OUTPUT_DIR = ', process.env.UNI_OUTPUT_DIR);
|
||
}
|
||
|
||
} catch (e) {
|
||
console.log('NODE_ENV 环境变量未设置,默认使用开发模式');
|
||
}
|
||
|
||
/**
|
||
* 读取 manifest.json 中的 h5.router.base,去掉注释后解析
|
||
* 返回 base 路径的 . 连接形式(如 /hwappx/common/ → hwappx.common)
|
||
*/
|
||
function _readManifestBase(manifestPath) {
|
||
try {
|
||
const raw = fs.readFileSync(manifestPath, 'utf8');
|
||
const clean = raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
|
||
const manifest = JSON.parse(clean);
|
||
if (manifest?.h5?.router?.base) {
|
||
let base = manifest.h5.router.base;
|
||
if (!base.endsWith('/')) {
|
||
console.warn(`[manifest] h5.router.base "${base}" 不规范,已自动补全 /`);
|
||
base = base + '/';
|
||
}
|
||
return base.replace(/^\/+|\/+$/g, '').replace(/\//g, '.');
|
||
}
|
||
} catch (e) {
|
||
console.warn('[manifest] 读取 h5.router.base 失败:', e.message);
|
||
}
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* 读取 langConfig.source, 用于构建期判断是否启用 lang/ 排除策略
|
||
* 用 fs 静态解析, 避免 webpack 把整个 config-external.js 链入 vue.config 编译路径
|
||
*/
|
||
function _readLangSource() {
|
||
try {
|
||
const content = fs.readFileSync(path.join(__dirname, 'common/js/config-external.js'), 'utf-8');
|
||
const match = content.match(/source:\s*['"]([\w-]+)['"]/);
|
||
return match ? match[1] : null;
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 读取 themeConfig.overrideRemote.source, 用于构建期判断是否启用 style_color 排除策略
|
||
* remote-only 模式: style_color.js 替换为 empty-style-stub.js(只含 emergency fallback)
|
||
* 节省主包 ~75KB
|
||
*/
|
||
function _readThemeSource() {
|
||
try {
|
||
const content = fs.readFileSync(path.join(__dirname, 'common/js/config-external.js'), 'utf-8');
|
||
// 找到 overrideRemote 块中的 source 字段(可能有多个 source 字段,取 overrideRemote 块内的)
|
||
const orBlock = content.match(/overrideRemote\s*:\s*\{([\s\S]*?)\n\s*\}/);
|
||
if (!orBlock) return null;
|
||
const match = orBlock[1].match(/source:\s*['"]([\w-]+)['"]/);
|
||
return match ? match[1] : null;
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 通用Webpack的资源替换插件
|
||
*/
|
||
class NormalModuleReplacementPlugin {
|
||
constructor(resourceRegExp, newResource) {
|
||
this.resourceRegExp = resourceRegExp;
|
||
this.newResource = newResource;
|
||
}
|
||
|
||
apply(compiler) {
|
||
const resourceRegExp = this.resourceRegExp;
|
||
const newResource = this.newResource;
|
||
|
||
compiler.hooks.normalModuleFactory.tap(
|
||
"NormalModuleReplacementPlugin",
|
||
nmf => {
|
||
nmf.hooks.beforeResolve.tap("NormalModuleReplacementPlugin", result => {
|
||
if (!result) return;
|
||
if (resourceRegExp.test(result.request)) {
|
||
if (typeof newResource === "function") {
|
||
newResource(result);
|
||
} else {
|
||
result.request = newResource;
|
||
}
|
||
}
|
||
return result;
|
||
});
|
||
nmf.hooks.afterResolve.tap("NormalModuleReplacementPlugin", result => {
|
||
if (!result) return;
|
||
if (resourceRegExp.test(result.resource)) {
|
||
if (typeof newResource === "function") {
|
||
newResource(result);
|
||
} else {
|
||
result.resource = path.resolve(
|
||
path.dirname(result.resource),
|
||
newResource
|
||
);
|
||
}
|
||
}
|
||
return result;
|
||
});
|
||
}
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 自定义 webpack 插件:在通用产物(vendor.js)中内联注入版本数据
|
||
* 利用 emit 钩子在资源写入磁盘前处理,支持 HMR(每次重编译都会自动注入)
|
||
* 适用于所有平台(mp-weixin / h5 等)
|
||
*/
|
||
class VersionInjectPlugin {
|
||
constructor() {
|
||
this._cachedCommitId = null;
|
||
}
|
||
apply(compiler) {
|
||
compiler.hooks.emit.tapAsync('VersionInjectPlugin', (compilation, callback) => {
|
||
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
|
||
const platform = process.env.UNI_PLATFORM || '';
|
||
|
||
// 获取 Git 提交 ID(缓存,避免每次 HMR 都执行)
|
||
if (!this._cachedCommitId) {
|
||
try {
|
||
this._cachedCommitId = execSync('git rev-parse --short HEAD', { cwd: __dirname }).toString().trim();
|
||
} catch (e) {
|
||
this._cachedCommitId = 'unknown';
|
||
}
|
||
}
|
||
|
||
const versionData = { commitId: this._cachedCommitId, buildTime: new Date(new Date().getTime() + 8 * 3600 * 1000).toISOString().slice(0, 16).replace('T', ' ') };
|
||
const inlineVersion = JSON.stringify(versionData);
|
||
|
||
// 尝试注入到 vendor.js(mp-weixin / mp-* 等平台)
|
||
const vendorKey = 'common/vendor.js';
|
||
const vendorAsset = compilation.assets[vendorKey];
|
||
if (vendorAsset) {
|
||
let content = typeof vendorAsset.source === 'function' ? vendorAsset.source() : vendorAsset._value;
|
||
if (typeof content !== 'string') content = content.toString();
|
||
|
||
// 清理旧注入
|
||
content = content.replace(/^\s*const\s+site\s*=\s*require\s*\(['"]\.\.\/site(\.js)?['"]\);?\s*\r?\n/gm, '');
|
||
content = content.replace(/^\s*const\s+__app_version\s*=\s*(require\s*\(['"]\.\.\/version(\.json|\.js)?['"]\)|.+);?\s*\r?\n/gm, '');
|
||
|
||
// 只注入版本信息(通用),site.js require 仅在 mp-weixin/mp-harmony 生产构建时注入
|
||
const lines = [`const __app_version = ${inlineVersion};`];
|
||
if ((platform === 'mp-weixin' || platform === 'mp-harmony') && mode === 'production') {
|
||
lines.unshift('const site = require("../site.js");');
|
||
}
|
||
|
||
const eol = content.includes('\r\n') ? '\r\n' : '\n';
|
||
let prefix = '';
|
||
if (content.charCodeAt(0) === 0xfeff) {
|
||
prefix = '\uFEFF';
|
||
content = content.slice(1);
|
||
}
|
||
|
||
content = prefix + lines.join(eol) + eol + content;
|
||
|
||
compilation.assets[vendorKey] = {
|
||
source: () => content,
|
||
size: () => content.length
|
||
};
|
||
|
||
console.log(`[VersionInject] ✅ vendor.js 已注入 (platform=${platform}, mode=${mode}, commit=${this._cachedCommitId})`);
|
||
} else {
|
||
// H5 等平台可能没有 common/vendor.js,改为尝试注入到 index.html
|
||
// #ifdef H5
|
||
const htmlKey = 'index.html';
|
||
const htmlAsset = compilation.assets[htmlKey];
|
||
if (htmlAsset) {
|
||
let html = typeof htmlAsset.source === 'function' ? htmlAsset.source() : htmlAsset._value;
|
||
if (typeof html !== 'string') html = html.toString();
|
||
// 在 </head> 前注入 script 标签
|
||
const scriptTag = `<script>var __app_version = ${inlineVersion};</script>`;
|
||
if (!html.includes('__app_version')) {
|
||
html = html.replace('</head>', `${scriptTag}\n</head>`);
|
||
compilation.assets[htmlKey] = {
|
||
source: () => html,
|
||
size: () => html.length
|
||
};
|
||
console.log(`[VersionInject] ✅ index.html 已注入 (platform=${platform}, commit=${this._cachedCommitId})`);
|
||
}
|
||
}
|
||
// #endif
|
||
}
|
||
|
||
callback();
|
||
});
|
||
|
||
// 生产构建后处理:mp-weixin 拷贝 site.js + ZIP;H5 自动打包 ZIP
|
||
if (process.env.NODE_ENV === 'production') {
|
||
compiler.hooks.afterEmit.tapAsync('ProductionZipPack', (compilation, callback) => {
|
||
const platform = process.env.UNI_PLATFORM || '';
|
||
const UNI_SCRIPT = process.env.UNI_SCRIPT ?? '';
|
||
|
||
if (platform === 'mp-weixin') {
|
||
console.log('\n🚀 mp-weixin 生产构建完成,运行补丁脚本(site.js 拷贝 + ZIP 打包)...');
|
||
try {
|
||
execSync(`"${process.execPath}" scripts/mp-weixin.patch.js --mode production`, { cwd: __dirname, stdio: 'inherit' });
|
||
} catch (e) {
|
||
console.error('补丁脚本执行失败:', e.message);
|
||
}
|
||
} else if (platform === 'mp-harmony') {
|
||
console.log('\n🚀 mp-harmony 生产构建完成,运行补丁脚本(配置拷贝 + ZIP 打包)...');
|
||
try {
|
||
execSync(`"${process.execPath}" scripts/harmony.patch.js --mode production`, { cwd: __dirname, stdio: 'inherit' });
|
||
} catch (e) {
|
||
console.error('补丁脚本执行失败:', e.message);
|
||
}
|
||
// 拷贝构建产物到 ascf_src
|
||
console.log('\n🚀 mp-harmony 生产构建完成,拷贝产物到 ascf_src 目录...');
|
||
this._copyMpHarmonyBuild();
|
||
// 关键: _copyMpHarmonyBuild 会用构建产物的 app.json 覆盖
|
||
// stub ascf_src/app.json (因为产物里 app.json 是占位符版本)。
|
||
// 必须**再次**跑 syncPrivacyToStub, 把占位符替换为 site.js 里的真实值
|
||
try {
|
||
execSync(`"${process.execPath}" scripts/harmony.patch.js --sync-privacy-only`, { cwd: __dirname, stdio: 'inherit' });
|
||
} catch (e) {
|
||
console.error('[mp-harmony] 二次同步 privacy 失败:', e.message);
|
||
}
|
||
} else if (platform === 'h5' && UNI_SCRIPT) {
|
||
this._zipH5Build(compilation);
|
||
}
|
||
|
||
callback();
|
||
});
|
||
}
|
||
}
|
||
|
||
_computeH5OutputName() {
|
||
// 计算输出目录名: {域名}-{base用.连接}
|
||
let baseSuffix = _readManifestBase(path.join(__dirname, 'manifest.json'));
|
||
let domain = '';
|
||
if (process.env.UNI_APP_DOMAIN) {
|
||
try {
|
||
domain = new URL(process.env.UNI_APP_DOMAIN).hostname;
|
||
} catch (e) {
|
||
domain = process.env.UNI_APP_DOMAIN.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
|
||
}
|
||
} else {
|
||
domain = (process.env.UNI_SCRIPT || '').replace(/^h5-/, '');
|
||
}
|
||
const result = baseSuffix ? `${domain}-${baseSuffix}` : domain;
|
||
console.log('[H5Zip] 输出目录名:', result);
|
||
return result;
|
||
}
|
||
|
||
_zipH5Build(compilation) {
|
||
const outputDirName = this._computeH5OutputName();
|
||
const buildDir = process.env.NODE_ENV === 'production' ? 'build' : 'dev';
|
||
const h5Dir = path.join(__dirname, 'unpackage', 'dist', buildDir, 'h5');
|
||
|
||
// 实际构建输出目录(由 webpack 决定)
|
||
const actualOutput = compilation.compiler.outputPath;
|
||
const targetDir = path.join(h5Dir, outputDirName);
|
||
|
||
// 如果路径一致,跳过重命名
|
||
if (actualOutput !== targetDir) {
|
||
try {
|
||
if (fs.existsSync(targetDir)) {
|
||
fs.rmSync(targetDir, { recursive: true });
|
||
}
|
||
fs.renameSync(actualOutput, targetDir);
|
||
console.log(`[H5Zip] 输出目录已重命名为: ${outputDirName}`);
|
||
} catch (e) {
|
||
console.error('[H5Zip] 目录重命名失败:', e.message);
|
||
return;
|
||
}
|
||
} else {
|
||
console.log(`[H5Zip] 输出目录名正确: ${outputDirName}`);
|
||
}
|
||
|
||
// 标准时区时间戳格式: 20260511_1058
|
||
const fmt = new Intl.DateTimeFormat('en-CA', {
|
||
timeZone: 'Asia/Shanghai',
|
||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||
hour: '2-digit', minute: '2-digit', hour12: false
|
||
});
|
||
const parts = fmt.formatToParts(new Date());
|
||
const get = (t) => parts.find(p => p.type === t).value;
|
||
const ts = `${get('year')}${get('month')}${get('day')}_${get('hour')}${get('minute')}`;
|
||
|
||
const zipFileName = `${outputDirName}-${this._cachedCommitId}-${ts}.zip`;
|
||
const zipFilePath = path.join(h5Dir, zipFileName);
|
||
|
||
console.log(`\n📦 H5 ZIP: ${zipFileName}...`);
|
||
|
||
let command;
|
||
const sourcePath = targetDir.replace(/'/g, "''");
|
||
const destPath = zipFilePath.replace(/'/g, "''");
|
||
if (process.platform === 'win32') {
|
||
command = `powershell -Command "Compress-Archive -Path '${sourcePath}\\*' -DestinationPath '${destPath}' -Force"`;
|
||
} else {
|
||
const sourceBase = path.dirname(targetDir);
|
||
const sourceName = path.basename(targetDir);
|
||
command = `cd '${sourceBase}' && zip -r '${zipFilePath}' '${sourceName}' -x '*/node_modules/*' '*.git/*' '*.DS_Store'`;
|
||
}
|
||
|
||
try {
|
||
execSync(command, { stdio: 'inherit' });
|
||
console.log(`✅ H5 ZIP: ${zipFileName}`);
|
||
|
||
try {
|
||
const openDir = h5Dir.replace(/'/g, "''");
|
||
if (process.platform === 'win32') {
|
||
execSync(`explorer "${openDir}"`, { stdio: 'ignore' });
|
||
} else {
|
||
execSync(`open "${openDir}"`, { stdio: 'ignore' });
|
||
}
|
||
} catch (openErr) {
|
||
console.warn(`⚠️ 打开目录失败(不影响 ZIP 打包): ${openErr.message}`);
|
||
}
|
||
|
||
// 删除已打包的源目录
|
||
// fs.rmSync(targetDir, { recursive: true, force: true });
|
||
// console.log(`[H5Zip] 已删除源目录: ${outputDirName}`);
|
||
} catch (e) {
|
||
console.error('H5 ZIP 打包失败:', e.message);
|
||
}
|
||
}
|
||
|
||
_copyMpHarmonyBuild() {
|
||
const sourceDir = path.join(__dirname, 'unpackage', 'dist', 'build', '.mp-harmony');
|
||
const destDir = path.join(__dirname, 'stub-app-projects', 'mp-harmony', 'ascf', 'ascf_src');
|
||
|
||
console.log(`\n📋 拷贝鸿蒙构建产物...`);
|
||
console.log(` 源: ${sourceDir}`);
|
||
console.log(` 目标: ${destDir}`);
|
||
|
||
// 检查源目录是否存在
|
||
if (!fs.existsSync(sourceDir)) {
|
||
console.error(`❌ 源目录不存在: ${sourceDir}`);
|
||
throw new Error(`鸿蒙构建产物目录不存在: ${sourceDir}`);
|
||
}
|
||
|
||
try {
|
||
// 清空目标目录(失败不影响后续拷贝)
|
||
if (fs.existsSync(destDir)) {
|
||
try {
|
||
fs.rmSync(destDir, { recursive: true, force: true });
|
||
if (fs.existsSync(destDir)) {
|
||
fs.rmSync(destDir, { recursive: true, force: true });
|
||
}
|
||
console.log(` ✅ 已清空目标目录 ${destDir}`);
|
||
} catch (cleanErr) {
|
||
console.warn(` ⚠️ 清空目标目录 ${destDir} 失败,将尝试覆盖: ${cleanErr.message}`);
|
||
}
|
||
}
|
||
|
||
// 创建目标目录
|
||
fs.mkdirSync(destDir, { recursive: true });
|
||
|
||
// 拷贝函数
|
||
const copyDir = (src, dest) => {
|
||
if (!fs.existsSync(src)) return;
|
||
fs.mkdirSync(dest, { recursive: true });
|
||
const entries = fs.readdirSync(src, { withFileTypes: true });
|
||
for (const entry of entries) {
|
||
const srcPath = path.join(src, entry.name);
|
||
const destPath = path.join(dest, entry.name);
|
||
if (entry.isDirectory()) {
|
||
copyDir(srcPath, destPath);
|
||
} else {
|
||
fs.copyFileSync(srcPath, destPath);
|
||
}
|
||
}
|
||
};
|
||
|
||
copyDir(sourceDir, destDir);
|
||
console.log(' ✅ 拷贝完成');
|
||
|
||
// 同步修正 ascf_build 下 5 个子包 HAR 的 module.json5.module.name
|
||
// 原因: 模板里 module.name 经常被写成空串或不正确的值,
|
||
// 导致鸿蒙构建时 hvigor schema 校验失败 (必须 ^[a-zA-Z][0-9a-zA-Z_.]*$)
|
||
// 这里强制按包目录名覆盖, 保证产物正确。
|
||
this._fixSubpackageModuleNames();
|
||
} catch (e) {
|
||
console.error(` ❌ 拷贝失败: ${e.message}`);
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 修正 ascf_build/<pkg>/src/main/module.json5 的 module.name 字段
|
||
*
|
||
* 背景:
|
||
* mp-harmony 项目的子包 HAR (components_diy / pages_goods / pages_order /
|
||
* pages_promotion / pages_tool) 在 ascf/ascf_build/ 目录下各自有
|
||
* src/main/module.json5, 其中 module.name 必须等于包名才能通过 hvigor schema。
|
||
*
|
||
* 这个 module.json5 是项目源码里维护的模板, 经常因为:
|
||
* 1. uniapp 鸿蒙编译器输出空 name 字段
|
||
* 2. 模板里手动写错
|
||
* 3. 早期版本没这个文件, 后来手工补了一个错误的
|
||
* 导致 module.name 为空, 鸿蒙构建时 schema 校验失败:
|
||
* "Schema validate failed, must match pattern ^[a-zA-Z][0-9a-zA-Z_.]*$"
|
||
*
|
||
* 修复策略:
|
||
* 遍历 5 个子包, 用包目录名 (例: pages_tool) 覆盖 module.name,
|
||
* 确保构建产物始终正确。
|
||
*
|
||
* 注意:
|
||
* - 只覆盖 module.name, 其他字段保留 (deviceTypes / installationFree 等)
|
||
* - 如果 module.json5 不存在, 会按合理默认值新建一份
|
||
* - 如需新增/删除子包, 同步更新 SUBPACKAGE_NAMES 数组
|
||
*/
|
||
_fixSubpackageModuleNames() {
|
||
// 委托给模块级 fixSubpackageModuleNames, 行为一致
|
||
return fixSubpackageModuleNames();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 修正 ascf_build/<pkg>/src/main/module.json5 的 module.name 字段
|
||
*
|
||
* 实现已抽到独立模块 scripts/fix-subpackage-module-names.js,
|
||
* 这里仅 require + 转发, 避免 vue-cli-service 校验 module.exports 时报错。
|
||
*
|
||
* 背景:
|
||
* mp-harmony 项目的子包 HAR (components_diy / pages_goods / pages_order /
|
||
* pages_promotion / pages_tool) 在 ascf/ascf_build/ 目录下各自有
|
||
* src/main/module.json5, 其中 module.name 必须等于包名才能通过 hvigor schema。
|
||
*
|
||
* 这个 module.json5 是项目源码里维护的模板, 经常因为:
|
||
* 1. 模板里手动写错
|
||
* 2. 早期版本没这个文件, 后来手工补了一个错误的
|
||
* 3. 自动化脚本覆盖时丢失 name
|
||
* 导致 module.name 为空或错误, 鸿蒙构建时 schema 校验失败:
|
||
* "Schema validate failed, must match pattern ^[a-zA-Z][0-9a-zA-Z_.]*$"
|
||
*
|
||
* 使用:
|
||
* // vue.config.js 内部:
|
||
* const { fixSubpackageModuleNames } = require('./scripts/fix-subpackage-module-names');
|
||
* fixSubpackageModuleNames();
|
||
*
|
||
* // 独立跑:
|
||
* node scripts/fix-subpackage-module-names.js
|
||
*
|
||
* // CI 兜底校验:
|
||
* node stub-app-projects/mp-harmony/entry/scripts/check-har-deps.js --strict
|
||
*/
|
||
const { fixSubpackageModuleNames } = require('./scripts/fix-subpackage-module-names');
|
||
|
||
/**
|
||
* vue.config.js 顶层副作用: 任何入口 require 本文件时, 自动修正
|
||
* ascf_build/<pkg>/src/main/module.json5.module.name。
|
||
*
|
||
* 工作流覆盖:
|
||
* - HBuilderX 发行 → vue-cli-service build → require vue.config.js → 触发此钩子
|
||
* - 命令行 vue-cli-service build → 触发此钩子
|
||
* - 命令行 node -e "require('./vue.config.js')" → 触发此钩子
|
||
*
|
||
* 与 VersionInjectPlugin._copyMpHarmonyBuild 末尾的 _fixSubpackageModuleNames 重复
|
||
* 但更保险: 哪怕 webpack 钩子因为某些原因没触发, require 这层也会保证修复。
|
||
*/
|
||
(function _bootstrapFixSubpackageModuleNames() {
|
||
// 只在鸿蒙元服务场景下执行
|
||
// 关键判断: UNI_PLATFORM === 'mp-harmony' (uniapp 编译器在编译鸿蒙时设置)
|
||
// HBuilderX 选 "发行 → 鸿蒙元服务" 会设此环境变量, 编译 mp-weixin / h5 时不会设
|
||
// 目的: 避免在 mp-weixin / h5 编译时无意义地修改 mp-harmony 子包文件
|
||
if (process.env.UNI_PLATFORM !== 'mp-harmony') {
|
||
// 非鸿蒙平台, 跳过修复 (避免污染 mp-weixin / h5 编译时的 git status)
|
||
return;
|
||
}
|
||
try {
|
||
// 这里不传 silent, 让所有入口 (包括 HBuilderX / 命令行) 都能在 console 看到修复日志
|
||
fixSubpackageModuleNames();
|
||
} catch (e) {
|
||
console.error('[vue.config] bootstrap fixSubpackageModuleNames 失败:', e.message);
|
||
}
|
||
|
||
// 第二阶段: 跑子包配置一致性检查 (build-profile.json5 / oh-package.json5 / ascf_build 目录)
|
||
// 不抛错阻塞 vue-cli-service, 但 console 输出明确提示, 让 HBuilderX 用户立刻看到漏改的项
|
||
// 真正的硬性错误 (HAR 缺失) 由 DevEco ohpm install 在鸿蒙端抛出
|
||
try {
|
||
// 用 require 同步调用, 检查脚本会从 stdout 输出
|
||
// 注: 这里不 require ./scripts/check-subpackage-sync (避免循环依赖或重入), 而是在 fix-script
|
||
// 自身集成一个简化版的 sync check (复用同样的 SUBPACKAGE_NAMES + 同样 4 处检查)
|
||
const { runSyncCheck } = require('./scripts/fix-subpackage-module-names');
|
||
if (typeof runSyncCheck === 'function') {
|
||
runSyncCheck({ silent: false, exitOnError: false });
|
||
}
|
||
} catch (e) {
|
||
console.error('[vue.config] bootstrap sync check 失败:', e.message);
|
||
}
|
||
})();
|
||
|
||
// 导出配置
|
||
//
|
||
// 注意: 不要在这里 export 任何非白名单字段!
|
||
// vue-cli-service 会校验 vue.config.js 的导出 keys, 不允许出现未识别的字段
|
||
// (会报 "Invalid options in vue.config.js: \"<field>\" is not allowed")。
|
||
// 历史教训: 之前把 fixSubpackageModuleNames 放在 module.exports 上, 直接报这个错。
|
||
// 现在 fixSubpackageModuleNames 仅作为本文件内的顶层函数 + 顶层 IIFE 副作用,
|
||
// 不再 export。需要外部调用请走:
|
||
// node scripts/fix-subpackage-module-names.js
|
||
// 或:
|
||
// node -e "require('./scripts/fix-subpackage-module-names').fixSubpackageModuleNames()"
|
||
module.exports = {
|
||
productionSourceMap: false,
|
||
configureWebpack: config => {
|
||
console.log('[vue.config] config.mode = ', config.mode);
|
||
|
||
// lang/ 目录排除: 仅在 remote-only 模式下生效
|
||
// 原理: webpack 把 require(`@/lang/${lang}/${path}.js`) 解析为 ContextModule,
|
||
// 会扫描并打包 lang/ 下所有 .js. 替换为 stub 后, 真正的 lang 文件不进 bundle.
|
||
// 条件: source === 'remote-only' (由 common/js/config-external.js 配置)
|
||
const langSource = _readLangSource();
|
||
if (langSource === 'remote-only') {
|
||
try {
|
||
const webpack = require('webpack');
|
||
const stubPath = path.resolve(__dirname, 'common/js/empty-lang-stub.js');
|
||
if (webpack?.NormalModuleReplacementPlugin) {
|
||
config.plugins.push(
|
||
new NormalModuleReplacementPlugin(
|
||
// 匹配解析后的绝对路径: .../lang/xxx/yyy.js
|
||
/[\\/]lang[\\/].+\.js$/,
|
||
stubPath
|
||
)
|
||
);
|
||
console.log(`[vue.config] ✅ lang/ 目录已替换为 stub (source=remote-only) — 节省 lang 包体积`);
|
||
}
|
||
} catch (e) {
|
||
console.error('[vue.config] 配置 lang/ 排除失败:', e.message);
|
||
}
|
||
} else {
|
||
console.log(`[vue.config] lang/ 目录保留 (source=${langSource || 'unknown'})`);
|
||
}
|
||
|
||
// style_color.js 排除: 仅在 themeConfig.overrideRemote.source === 'remote-only' 模式下生效
|
||
// 原理: config-external.js 的 loadTheme() 用 require('@/common/js/style_color.js') 同步加载
|
||
// 整个文件. 替换为 stub 后, 主包只包含 1 个 default preset (~5KB), 其他 7 个 preset
|
||
// 由远端 /api/theme/presets 异步补齐. 节省主包 ~75KB.
|
||
const themeSource = _readThemeSource();
|
||
if (themeSource === 'remote-only') {
|
||
try {
|
||
const webpack = require('webpack');
|
||
const stubPath = path.resolve(__dirname, 'common/js/empty-style-stub.js');
|
||
if (webpack?.NormalModuleReplacementPlugin) {
|
||
config.plugins.push(
|
||
new NormalModuleReplacementPlugin(
|
||
// 精确匹配: 解析后的绝对路径是 .../common/js/style_color.js
|
||
// 不能匹配 style_color 目录里其它可能存在的同名文件
|
||
/[\\/]common[\\/]js[\\/]style_color\.js$/,
|
||
stubPath
|
||
)
|
||
);
|
||
console.log(`[vue.config] ✅ style_color.js 已替换为 stub (source=remote-only) — 节省 ~75KB`);
|
||
}
|
||
} catch (e) {
|
||
console.error('[vue.config] 配置 style_color 排除失败:', e.message);
|
||
}
|
||
} else {
|
||
console.log(`[vue.config] style_color.js 保留 (source=${themeSource || 'unknown'})`);
|
||
}
|
||
|
||
if (config.mode === 'production') {
|
||
console.log('[vue.config] 开始尝试配置极限压缩');
|
||
|
||
if (process.env.UNI_PLATFORM === 'mp-weixin') {
|
||
// 配置zion-uniapp-mp-load-package插件
|
||
config.plugins.push(new zionUniMpLoadPackagePlugin({
|
||
optimization: {
|
||
minimize: true,
|
||
moduleIds: 'named'
|
||
},
|
||
packages: [
|
||
'pages',
|
||
'pages_tool',
|
||
'pages_goods',
|
||
'pages_order',
|
||
'pages_promotion'
|
||
]
|
||
}));
|
||
}
|
||
|
||
|
||
// 确保minimizer数组存在
|
||
if (config.optimization && config.optimization.minimizer && Array.isArray(config.optimization.minimizer)) {
|
||
// 查找TerserWebpackPlugin实例,而不是依赖索引
|
||
const terserPluginIndex = config.optimization.minimizer.findIndex(plugin => {
|
||
return plugin.constructor && plugin.constructor.name === 'TerserWebpackPlugin' ||
|
||
(plugin.options && plugin.options.terserOptions);
|
||
});
|
||
|
||
if (terserPluginIndex !== -1) {
|
||
// 确保options对象存在
|
||
if (!config.optimization.minimizer[terserPluginIndex].options) {
|
||
config.optimization.minimizer[terserPluginIndex].options = {};
|
||
}
|
||
|
||
// 区分平台配置压缩选项
|
||
const enableKeepConsole = process.env.UNI_PLATFORM === 'mp-harmony';
|
||
console.log(`[vue.config] 是否保留console语句 = ${Boolean(enableKeepConsole)}`);
|
||
|
||
// 基础压缩配置(通用)
|
||
const terserOptions = {
|
||
compress: {
|
||
drop_console: !enableKeepConsole, // 鸿蒙保留 console 用于调试
|
||
drop_debugger: !enableKeepConsole,
|
||
dead_code: true,
|
||
unused: true,
|
||
toplevel: true,
|
||
passes: 10,
|
||
sequences: true,
|
||
evaluate: true,
|
||
reduce_vars: true,
|
||
join_vars: true,
|
||
conditionals: true,
|
||
comparisons: true,
|
||
booleans: true,
|
||
typeofs: true,
|
||
collapse_vars: true,
|
||
reduce_funcs: true,
|
||
inline: true,
|
||
side_effects: true,
|
||
keep_fargs: false,
|
||
keep_fnames: false,
|
||
pure_funcs: enableKeepConsole ? [] : ['console.log', 'console.warn', 'console.error', 'console.info'],
|
||
pure_getters: true,
|
||
unsafe_math: true,
|
||
},
|
||
format: {
|
||
ascii_only: true,
|
||
comments: false,
|
||
beautify: false,
|
||
ecma: 5,
|
||
wrap_func_args: false,
|
||
bracketize: true,
|
||
quote_style: 1
|
||
},
|
||
parse: {
|
||
ecma: 8,
|
||
bare_returns: true
|
||
},
|
||
mangle: {
|
||
toplevel: true,
|
||
keep_fnames: false,
|
||
keep_classnames: false
|
||
},
|
||
module: false,
|
||
sourceMap: false
|
||
};
|
||
|
||
config.optimization.minimizer[terserPluginIndex].options.terserOptions = terserOptions;
|
||
console.log('[vue.config] 配置极限压缩完成');
|
||
}
|
||
}
|
||
}
|
||
|
||
// 所有模式下添加版本注入插件(适用于所有平台)
|
||
config.plugins.push(new VersionInjectPlugin());
|
||
}
|
||
}; |