Files
lucky_shop/vue.config.js
ZF sun 1f1a642f99 fix(i18n): 修复审计脚本扫不到 modular 文件的盲区,接入命名规范检查到构建卡点
- i18n_audit.py: 同步扫描 lang/<locale>/<module>.js 多文件 + lang/<locale>.js 单文件
- i18n_check_naming.py (新增): 检测中文 key / 扁平 key / 行号式 locational key / 跨业务域同名 / 非 snake_case
- i18n_check.py: 加入 naming 子命令,默认全套审计包含 naming
- vue.config.js: I18nGuardPlugin 增加 naming 阻断
- full_migrate_i18n.py: 标 DEPRECATED,文档说明其输出违反 CLAUDE.md 1.2 铁律
- diagnose_keys.py (新增): 定位 orphan 调用 key

背景:之前审计'假阳性通过',实际 6018 处违规;现在审计能看到真实状态。

Fix: docs/i18n-rules.md 1.6 条铁律需补强 (modular 审计入口已加)

统计 (i18n_check_naming.py 当前输出):
  A. 中文 key: 2146
  B. 扁平 key: 188
  C. 行号式 locational key: 1612
  D. 跨业务域同名 tail: 1366
  E. 非 snake_case key 段: 2924
  TOTAL: 8236 处
2026-06-25 10:26:55 +08:00

497 lines
18 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');
console.log('[vue.config] 已加载');
// 确保 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) {}
}
// 输出 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) {
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 ?? '';
// 所有 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 '';
}
/**
* i18n 守卫:在 webpack emit 阶段同步执行 i18n 审计。
* 仅生产构建触发;命中 C 类(page 覆盖 common)则阻断构建。
* 配合 python scripts/i18n_check.py 形成多层卡点:
* - 本地手动: python scripts/i18n_check.py
* - 构建期: I18nGuardPlugin(当前)
*/
class I18nGuardPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('I18nGuardPlugin', (compilation, callback) => {
// dev 模式不阻断(避免开发体验过差);prod 模式失败直接阻断
if (process.env.NODE_ENV !== 'production') {
return callback();
}
// 跳过某些平台 / 配置(用户可通过 SKIP_I18N_GUARD=1 紧急绕过)
if (process.env.SKIP_I18N_GUARD === '1') {
console.warn('[i18n guard] SKIP_I18N_GUARD=1 已设置,跳过审计');
return callback();
}
const { execSync } = require('child_process');
// 构建期卡点:class_c(page 覆盖 common)+ naming(中文/扁平/行号式 key)
// 这两个失败就阻断构建;其它 audit(basic/strict/symmetry)只在手动跑 i18n_check.py 时触发
try {
execSync('python scripts/i18n_class_c.py', { stdio: 'inherit' });
execSync('python scripts/i18n_check_naming.py', { stdio: 'inherit' });
callback();
} catch (e) {
compilation.errors.push(new Error(
'[i18n guard] i18n audit failed - see docs/i18n-shadow-audit.json / docs/i18n-naming-audit.json'
));
callback();
}
});
}
}
/**
* 自定义 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().toISOString() };
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();
} 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 });
console.log(' ✅ 已清空目标目录');
} catch (cleanErr) {
console.warn(` ⚠️ 清空目标目录失败,将尝试覆盖: ${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(' ✅ 拷贝完成');
} catch (e) {
console.error(` ❌ 拷贝失败: ${e.message}`);
throw e;
}
}
}
// 导出配置
module.exports = {
productionSourceMap: false,
configureWebpack: config => {
console.log('config.mode = ', config.mode);
if (config.mode === 'production') {
console.log('开始尝试配置极限压缩');
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 isMpHarmony = process.env.UNI_PLATFORM === 'mp-harmony';
// 基础压缩配置(通用)
const terserOptions = {
compress: {
drop_console: !isMpHarmony, // 鸿蒙保留 console 用于调试
drop_debugger: !isMpHarmony,
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: isMpHarmony ? [] : ['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('配置极限压缩完成');
}
}
}
// 所有模式下添加版本注入插件(适用于所有平台)
config.plugins.push(new VersionInjectPlugin());
// i18n 守卫:构建期检查 lang 文件是否有重复定义 / 覆盖 / 同名异义
// 仅在生产构建 + 触发条件命中时执行;失败则 push 到 compilation.errors 阻断构建
config.plugins.push(new I18nGuardPlugin());
}
};