Files
lucky_shop/vue.config.js
ZF sun 0cb5c1a4df chore(mp-harmony): vue.config.js 顶层 IIFE 集成 runSyncCheck (4 处一致性自动校验)
背景:
  之前 vue.config.js 顶层 IIFE 只调 fixSubpackageModuleNames 修 5 个子包 name,
  但漏改 build-profile.json5 / entry/oh-package.json5 / ascf_build/ 目录时
  没有任何提示, 用户在 HBuilderX 看不到错误, 直到 DevEco ohpm 报错才发现。

改动:
  1. scripts/fix-subpackage-module-names.js 新增 runSyncCheck() 函数:
     - 读取 har-deps-required.json5 (whitelist) 作为单一事实源 (优先 json5 包,
       失败回退手写解析)
     - 扫描 3 个地方的一致性: build-profile.json5 modules / entry/oh-package.json5
       file: 依赖 / ascf_build/ 实际目录
     - 不一致时 console 打印明确错误 + 修复步骤 (1, 2, 3, 4 个文件)
     - 默认不抛错 (silent: false 仅 console 输出, exitOnError: false)
     - 暴露为 module.exports.runSyncCheck

  2. vue.config.js 顶层 IIFE 集成:
     - 修完 name 后调 runSyncCheck()
     - HBuilderX 触发 require vue.config.js 时立即执行
     - 不阻塞 vue-cli-service, 但 console 输出立刻告知用户漏改位置
     - 鸿蒙端的硬性错误 (ohpm "Field IS Empty") 仍由 DevEco 抛出

  端到端验证:
    1. 故意在 whitelist 加 pages_extra (未同步其他 3 处)
       → vue.config.js require 立即输出:
         ✘ build-profile.json5 缺失子包: [pages_extra]
         ✘ entry/oh-package.json5 缺失 file: 依赖: [pages_extra]
         ✘ ascf_build/ 目录缺失子包: [pages_extra]
         修复: 1. entry/har-deps-required.json5 ... 2. mp-harmony/build-profile.json5 ...
               3. entry/oh-package.json5 ... 4. ascf_build/<pkg>/ 实际目录
    2. 故意清空 pages_tool 的 name 字段
       → 修成 "pages_tool" + 跑 sync check, 输出 ✓ 5 个子包配置完全一致

  module.exports 仍只有白名单字段 (productionSourceMap, configureWebpack),
  vue-cli-service 不会报 "Invalid options" 错。

依赖: 项目已有 json5 包, 无需新装
2026-07-31 08:54:30 +08:00

772 lines
31 KiB
JavaScript
Raw 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.
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() {
// 跳过生产测试门禁期间 (test-gate 会 fail 时, 不希望顺手修复文件)
if (process.env.NODE_ENV === 'production' && process.env.UNI_SCRIPT && process.env.__TEST_GATE_BLOCKED__) {
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());
}
};