/* eslint-disable no-useless-escape */ const os = require('os'); const path = require('path'); const crypto = require('crypto'); const fs = require('fs-extra'); const UNI_PLATFORM = process.env.UNI_PLATFORM || 'h5'; const NODE_ENV = process.env.NODE_ENV || 'production'; /** * 完整版本号,例如:3.9.0-16751-d1195b1d-20240605.0345 * 通过环境变量传入 * */ const VUE_APP_LONG_VERSION_NAME = process.env.VUE_APP_LONG_VERSION_NAME || ''; /** * 环境,编译生产包时值为 prod * 通过环境变量传入 * */ const VUE_APP_STAGE = process.env.VUE_APP_STAGE || ''; const __PROD__ = NODE_ENV === 'production'; const CDN_URL = process.env.CDN_URL; /** * yarn dev:mp-weixin 时,需要在本地启动 http server 来承载写在 css 中的静态资源,此变量为 http server 的端口。 * * 通过环境变量传入,不传时默认值为 5002 * * 如果提示端口被占用(很大可能是之前的dev server异常退出了) * 可以用 lsof -i:5000 查看是哪个进程占用, * 然后用 kill -9 [pid] 杀掉该进程 */ const IMAGES_SERVER_PORT = process.env.IMAGES_SERVER_PORT || '5002'; /** yarn dev:mp-weixin 时用到的本地 http server 地址 */ const localPath = `http://${getLocalIP()}:${IMAGES_SERVER_PORT}/`; /** * CDN 地址,build 时用到。 * 如无需使用CDN(比如H5平台有时会不适用CDN),则打包时使用类似 CDN_URL= yarn build:h5 的命令 * */ const publicPath = CDN_URL !== undefined ? CDN_URL : VUE_APP_STAGE === 'prod' ? 'https://static.kerryprops.com.cn/kip/temporary-parking-frontend/' : 'https://static-le.kerryprops.com.cn/kip/temporary-parking-frontend/'; const isWin = /^win/.test(process.platform); const normalizePath = (pathIn) => (isWin ? pathIn.replace(/\\/g, '/') : pathIn); /** 临时文件夹,编译生产包时,静态资源会被拷贝到该文件夹中,Jenkins 会将该文件中的文件上传到 CDN */ const CDN_UPLOAD_FOLDER = 'temp'; const REGEX_IMG = /\.(gif|jpe?g|png|svg)$/; const REGEX_FONT = /\.(woff2?|eot|ttf|otf)$/; const DIR_ASSET = 'static'; const DIR_IMG = 'img'; const DIR_FONTS = 'fonts'; /** 获取本级 IP */ function getLocalIP() { const ifaces = Object.values(os.networkInterfaces()); for (const iface of ifaces) { for (const alias of iface) { if (alias.internal || alias.family !== 'IPv4') continue; return alias.address; } } } /** * 计算文件的 hash 值,返回包含 hash 值的文件路径 * 注意:只能处理这几类文件:png|jpe?g|gif|svg|ttf * @param {string} resourcePath 文件在磁盘上的物理路径 * @param {string} outputPath 待替换的输出路径,比如 static/img/abc.png * @returns 替换过的输出路径,比如 static/img/abc.65d956cc.png */ function getFilePathWithHash(resourcePath, outputPath) { const buffer = fs.readFileSync(resourcePath); const hash = crypto .createHash('md5') .update(buffer) .digest('hex') .slice(0, 8); return outputPath.replace(/\.(png|jpe?g|gif|svg|ttf)$/, `.${hash}.$1`); } function formatImageUrl(url, filePath) { if (UNI_PLATFORM === 'app') { return url; } // console.log(filePath); const origin = __PROD__ ? publicPath : localPath; const absolutePath = url.startsWith('/') ? path.resolve('src', url.slice(1)) : path.resolve(path.dirname(filePath), url); const href = origin + path.relative('src', absolutePath).replace(/\\/g, '/'); // console.log(absolutePath); return __PROD__ ? getFilePathWithHash(absolutePath, href) : href; } /** * 返回当前git repo的header的commit id * @returns commit id,例如 fe40cff4c4a9b55eaf215ac79a0c37c079b067cd * @see https://stackoverflow.com/a/56975550/196519 */ function gitHash() { try { const rev = fs .readFileSync('.git/HEAD') .toString() .trim() .split(/.*[: ]/) .slice(-1)[0]; if (rev.indexOf('/') === -1) { return rev; } else { return fs .readFileSync('.git/' + rev) .toString() .trim(); } } catch (e) { // eslint-disable-next-line no-console console.log(e); return 'git_hash_error'; } } /** * 根据配置替换文件内容 * @param {{path: string, replace:[{reg: RegExp, replacement: string}]}} config */ const fileContentReplace = function (config) { let content = fs.readFileSync(config.path).toString(); config.replace.forEach((n) => { content = content.replace(n.reg, n.replacement); }); // fs.writeFileSync(gradlePath + '2', gradle); fs.writeFileSync(config.path, content); }; /** * 1. 在 ios app 的 AppInfo.xcconfig 中写入 versionName 和 versionCode * 2. 在 android app 的 build.gradle 中写入 versionName、versionCode 和 fileName */ const updateVersionCode = function () { if (!VUE_APP_LONG_VERSION_NAME) { // eslint-disable-next-line no-console console.log('NO VUE_APP_LONG_VERSION_NAME, skip updateVersionCode()'); return; } const arr = VUE_APP_LONG_VERSION_NAME.split('-'); const versionName = arr[0]; const versionCode = arr[1]; if (!versionName || !versionCode) { // eslint-disable-next-line no-console console.log('missing versionName or versionCode, skip updateVersionCode()'); return; } const env = VUE_APP_STAGE == 'prod' ? 'PROD' : 'DEV'; const fileName = `KIPUI-${env}-${VUE_APP_LONG_VERSION_NAME}`; let config = { path: 'android/KIP-UI/simpleDemo/build.gradle', replace: [ // { reg: /applicationId\s"(.*)"/, replacement: 'applicationId "' + buildConfigs[option.configuration].appId + '"' }, { reg: /versionCode\s([\d\.]{1,})/, replacement: 'versionCode ' + versionCode, }, { reg: /versionName\s"([\d\.]{1,})"/, replacement: 'versionName "' + versionName + '"', }, { reg: /outputFileName\s=\s"(.*)"/, replacement: 'outputFileName = "' + fileName + '.apk"', }, ], }; fileContentReplace(config); // eslint-disable-next-line no-console console.log('updateVersionCode(): build.gradle DONE !'); config = { path: 'ios/HBuilder-Hello/AppInfo.xcconfig', replace: [ // { reg: /PRODUCT_NAME\s=\s(.*)/, replacement: 'PRODUCT_NAME = ' + buildConfigs[option.configuration].appName }, // { reg: /PRODUCT_BUNDLE_IDENTIFIER\s=\s(.*)/, replacement: 'PRODUCT_BUNDLE_IDENTIFIER = ' + buildConfigs[option.configuration].appId }, { reg: /MARKETING_VERSION\s=\s([\d\.]{1,})/, replacement: 'MARKETING_VERSION = ' + versionName, }, { reg: /CURRENT_PROJECT_VERSION\s=\s([\d\.]{1,})/, replacement: 'CURRENT_PROJECT_VERSION = ' + versionCode, }, ], }; fileContentReplace(config); // eslint-disable-next-line no-console console.log('updateVersionCode(): AppInfo.xcconfig DONE !'); }; /** * 开发时,将【images文件夹下的图片】和【字体文件】的 url 转换为本地 http server 的地址 * * build 时,将图片和字体的 url 转换为 CDN 地址,同时把文件 copy 到 temp 文件夹 * @param {string} url url * @param {string} resourcePath 绝对路径 * @returns 转换后的 url */ const filePathHandler = (url, resourcePath) => { // 01. 处理 images 文件夹下的图片,包括 src/images/ 文件夹和 node_modules/@kip/ui-mobile 各组件中的 images 文件夹 if (/images\//.test(resourcePath)) { if (!__PROD__) { // 开发时,用本地的 http server 来 host 静态资源 // http://localhost:5000/src/images/abc/efg/hij.png return localPath + normalizePath(path.relative(__dirname, resourcePath)); } else { // build 时,返回 CDN 地址,同时把文件 copy 到 temp 文件夹, // jenkins 会上传这些图片 const tempPath = '/static/img/' + path.basename(resourcePath); const tempPathHashed = getFilePathWithHash(resourcePath, tempPath); fs.copy(resourcePath, './' + CDN_UPLOAD_FOLDER + tempPathHashed); // https://cdn.abc.com/static/img/hij.3677dgae.png return publicPath + tempPathHashed; } // 02. 处理字体文件 } else if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(resourcePath)) { if (!__PROD__) { // 开发时,用本地的 http server 来 host 静态资源 // http://localhost:5000/node_modules/@kip/ui-mobile/components/k-icon/kuiicons.ttf return localPath + normalizePath(path.relative(__dirname, resourcePath)); } else { // build 时,返回 CDN 地址,同时把文件 copy 到 temp 文件夹, // jenkins 会上传这些字体 const tempPath = '/static/fonts/' + path.basename(resourcePath); const tempPathHashed = getFilePathWithHash(resourcePath, tempPath); fs.copy(resourcePath, './' + CDN_UPLOAD_FOLDER + tempPathHashed); // https://cdn.abc.com/static/fonts/kuiicons.65d956cc.ttf return publicPath + tempPathHashed; } } // 03. 其他情况返回相对路径 return ( '/' + normalizePath(path.relative(process.env.UNI_INPUT_DIR, resourcePath)) ); }; /** * 开发时,将 css 中的 url 转换为本地 http server 的地址 * * build 时,返回 CDN 地址,同时把文件 copy 到 temp 文件夹 * @param {string} resourcePath 绝对路径 * @param {string} url css 中的原始 url * @returns 转换后的 url */ const postcssUrlHandler = (resourcePath, url) => { // postcss-url 穿过来的 url 是 '@/images/home/css.png' 时, // resourcePath 会是 '/Users/joel.bh.zhang/Documents/Repo-template/uniapp-vue3-vite-clean/src/pages/index/@/images/home/css.png' // 需要处理一下 if (url.startsWith('@')) { resourcePath = path.join(__dirname, './src/' + url.substring(1)); console.log('after replace @ with ./src/ ==>', resourcePath); } // postcss-url 穿过来的 url: '/static/logo.png', 时, // resourcePath 会是 '/Users/joel.bh.zhang/Documents/Repo/ui-mobile/src/pages/index/static/logo.png', // 需要处理一下 if (url.startsWith('/')) { resourcePath = path.join(__dirname, './src' + url); console.log('after replace start / with ./src/ ==>', resourcePath); } // 01. 处理 images 文件夹下的图片,包括 src/images/ 文件夹和 node_modules/@kip/ui-mobile 各组件中的 images 文件夹 if (/\.(gif|jpe?g|png|svg)(\?.*)?$/.test(resourcePath)) { if (!__PROD__) { // 开发时,用本地的 http server 来 host 静态资源 // http://localhost:5000/src/images/abc/efg/hij.png return localPath + normalizePath(path.relative(__dirname, resourcePath)); } else { // build 时,返回 CDN 地址,同时把文件 copy 到 temp 文件夹, // jenkins 会上传这些图片 const tempPath = '/static/img/' + path.basename(resourcePath); const tempPathHashed = getFilePathWithHash(resourcePath, tempPath); fs.copy(resourcePath, './' + CDN_UPLOAD_FOLDER + tempPathHashed); // https://cdn.abc.com/static/img/hij.3677dgae.png return publicPath + tempPathHashed; } // 02. 处理字体文件 } else if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(resourcePath)) { if (!__PROD__) { // 开发时,用本地的 http server 来 host 静态资源 // http://localhost:5000/node_modules/@kip/ui-mobile/components/k-icon/kuiicons.ttf return localPath + normalizePath(path.relative(__dirname, resourcePath)); } else { // build 时,返回 CDN 地址,同时把文件 copy 到 temp 文件夹, // jenkins 会上传这些字体 const tempPath = '/static/fonts/' + path.basename(resourcePath); const tempPathHashed = getFilePathWithHash(resourcePath, tempPath); fs.copy(resourcePath, './' + CDN_UPLOAD_FOLDER + tempPathHashed); // https://cdn.abc.com/static/fonts/kuiicons.65d956cc.ttf return publicPath + tempPathHashed; } } // 03. 其他情况返回相对路径 return ( '/' + normalizePath(path.relative(process.env.UNI_INPUT_DIR, resourcePath)) ); }; /** * 处理 vite 在编译的时候出现 use-window-z-index.mjs 被 * Could not resolve "../composables/use-window-z-index.mjs" from "node_modules/vant/es/config-provider/ConfigProvider.mjs" * file: /Users/sysadmin/code/kerry_project/temporary-parking-frontend/node_modules/vant/es/config-provider/ConfigProvider.mjs * 这个问题主要是在 vite.config.js 中配置了 define: {global: 'window'} 导致的 * @param {string} params.scanDir 相对路径: node_modules/vant * @returns */ function handleGlobalFiles({ scanDir }) { return { name: 'handle-global-files', buildStart() { if(scanDir) { const rootDir = `${scanDir}`; // 指定你的源代码根目录 // 递归函数,遍历所有子目录 function traverseDirectory(dir) { const files = fs.readdirSync(dir); files.forEach(file => { const fullPath = path.join(dir, file); const stat = fs.statSync(fullPath); if (stat.isDirectory()) { // 如果是目录,递归处理 traverseDirectory(fullPath); } else if (file.includes('global') && !fs.existsSync(file)) { // 如果文件名包含 'global' 并且还没有被创建的话 const content = fs.readFileSync(fullPath, 'utf-8'); const newContent = content.replace(/global/g, 'window'); const newFileName = file.replace('global', 'window'); const newFilePath = path.join(dir, newFileName); // 拷贝一份新的文件 fs.writeFileSync(newFilePath, newContent, 'utf-8'); } }); } // 开始遍历根目录 traverseDirectory(rootDir); } } }; } module.exports = { IMAGES_SERVER_PORT, getLocalIP, getFilePathWithHash, formatImageUrl, gitHash, updateVersionCode, localPath, publicPath, filePathHandler, postcssUrlHandler, REGEX_IMG, REGEX_FONT, DIR_ASSET, DIR_IMG, DIR_FONTS, CDN_UPLOAD_FOLDER, handleGlobalFiles, };