build-util.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. /* eslint-disable no-useless-escape */
  2. const os = require('os');
  3. const path = require('path');
  4. const crypto = require('crypto');
  5. const fs = require('fs-extra');
  6. const UNI_PLATFORM = process.env.UNI_PLATFORM || 'h5';
  7. const NODE_ENV = process.env.NODE_ENV || 'production';
  8. /**
  9. * 完整版本号,例如:3.9.0-16751-d1195b1d-20240605.0345
  10. * 通过环境变量传入
  11. * */
  12. const VUE_APP_LONG_VERSION_NAME = process.env.VUE_APP_LONG_VERSION_NAME || '';
  13. /**
  14. * 环境,编译生产包时值为 prod
  15. * 通过环境变量传入
  16. * */
  17. const VUE_APP_STAGE = process.env.VUE_APP_STAGE || '';
  18. const __PROD__ = NODE_ENV === 'production';
  19. const CDN_URL = process.env.CDN_URL;
  20. /**
  21. * yarn dev:mp-weixin 时,需要在本地启动 http server 来承载写在 css 中的静态资源,此变量为 http server 的端口。
  22. *
  23. * 通过环境变量传入,不传时默认值为 5002
  24. *
  25. * 如果提示端口被占用(很大可能是之前的dev server异常退出了)
  26. * 可以用 lsof -i:5000 查看是哪个进程占用,
  27. * 然后用 kill -9 [pid] 杀掉该进程
  28. */
  29. const IMAGES_SERVER_PORT = process.env.IMAGES_SERVER_PORT || '5002';
  30. /** yarn dev:mp-weixin 时用到的本地 http server 地址 */
  31. const localPath = `http://${getLocalIP()}:${IMAGES_SERVER_PORT}/`;
  32. /**
  33. * CDN 地址,build 时用到。
  34. * 如无需使用CDN(比如H5平台有时会不适用CDN),则打包时使用类似 CDN_URL= yarn build:h5 的命令
  35. * */
  36. const publicPath =
  37. CDN_URL !== undefined
  38. ? CDN_URL
  39. : VUE_APP_STAGE === 'prod'
  40. ? 'https://static.kerryprops.com.cn/kip/temporary-parking-frontend/'
  41. : 'https://static-le.kerryprops.com.cn/kip/temporary-parking-frontend/';
  42. const isWin = /^win/.test(process.platform);
  43. const normalizePath = (pathIn) => (isWin ? pathIn.replace(/\\/g, '/') : pathIn);
  44. /** 临时文件夹,编译生产包时,静态资源会被拷贝到该文件夹中,Jenkins 会将该文件中的文件上传到 CDN */
  45. const CDN_UPLOAD_FOLDER = 'temp';
  46. const REGEX_IMG = /\.(gif|jpe?g|png|svg)$/;
  47. const REGEX_FONT = /\.(woff2?|eot|ttf|otf)$/;
  48. const DIR_ASSET = 'static';
  49. const DIR_IMG = 'img';
  50. const DIR_FONTS = 'fonts';
  51. /** 获取本级 IP */
  52. function getLocalIP() {
  53. const ifaces = Object.values(os.networkInterfaces());
  54. for (const iface of ifaces) {
  55. for (const alias of iface) {
  56. if (alias.internal || alias.family !== 'IPv4') continue;
  57. return alias.address;
  58. }
  59. }
  60. }
  61. /**
  62. * 计算文件的 hash 值,返回包含 hash 值的文件路径
  63. * 注意:只能处理这几类文件:png|jpe?g|gif|svg|ttf
  64. * @param {string} resourcePath 文件在磁盘上的物理路径
  65. * @param {string} outputPath 待替换的输出路径,比如 static/img/abc.png
  66. * @returns 替换过的输出路径,比如 static/img/abc.65d956cc.png
  67. */
  68. function getFilePathWithHash(resourcePath, outputPath) {
  69. const buffer = fs.readFileSync(resourcePath);
  70. const hash = crypto
  71. .createHash('md5')
  72. .update(buffer)
  73. .digest('hex')
  74. .slice(0, 8);
  75. return outputPath.replace(/\.(png|jpe?g|gif|svg|ttf)$/, `.${hash}.$1`);
  76. }
  77. function formatImageUrl(url, filePath) {
  78. if (UNI_PLATFORM === 'app') {
  79. return url;
  80. }
  81. // console.log(filePath);
  82. const origin = __PROD__ ? publicPath : localPath;
  83. const absolutePath = url.startsWith('/')
  84. ? path.resolve('src', url.slice(1))
  85. : path.resolve(path.dirname(filePath), url);
  86. const href = origin + path.relative('src', absolutePath).replace(/\\/g, '/');
  87. // console.log(absolutePath);
  88. return __PROD__ ? getFilePathWithHash(absolutePath, href) : href;
  89. }
  90. /**
  91. * 返回当前git repo的header的commit id
  92. * @returns commit id,例如 fe40cff4c4a9b55eaf215ac79a0c37c079b067cd
  93. * @see https://stackoverflow.com/a/56975550/196519
  94. */
  95. function gitHash() {
  96. try {
  97. const rev = fs
  98. .readFileSync('.git/HEAD')
  99. .toString()
  100. .trim()
  101. .split(/.*[: ]/)
  102. .slice(-1)[0];
  103. if (rev.indexOf('/') === -1) {
  104. return rev;
  105. } else {
  106. return fs
  107. .readFileSync('.git/' + rev)
  108. .toString()
  109. .trim();
  110. }
  111. } catch (e) {
  112. // eslint-disable-next-line no-console
  113. console.log(e);
  114. return 'git_hash_error';
  115. }
  116. }
  117. /**
  118. * 根据配置替换文件内容
  119. * @param {{path: string, replace:[{reg: RegExp, replacement: string}]}} config
  120. */
  121. const fileContentReplace = function (config) {
  122. let content = fs.readFileSync(config.path).toString();
  123. config.replace.forEach((n) => {
  124. content = content.replace(n.reg, n.replacement);
  125. });
  126. // fs.writeFileSync(gradlePath + '2', gradle);
  127. fs.writeFileSync(config.path, content);
  128. };
  129. /**
  130. * 1. 在 ios app 的 AppInfo.xcconfig 中写入 versionName 和 versionCode
  131. * 2. 在 android app 的 build.gradle 中写入 versionName、versionCode 和 fileName
  132. */
  133. const updateVersionCode = function () {
  134. if (!VUE_APP_LONG_VERSION_NAME) {
  135. // eslint-disable-next-line no-console
  136. console.log('NO VUE_APP_LONG_VERSION_NAME, skip updateVersionCode()');
  137. return;
  138. }
  139. const arr = VUE_APP_LONG_VERSION_NAME.split('-');
  140. const versionName = arr[0];
  141. const versionCode = arr[1];
  142. if (!versionName || !versionCode) {
  143. // eslint-disable-next-line no-console
  144. console.log('missing versionName or versionCode, skip updateVersionCode()');
  145. return;
  146. }
  147. const env = VUE_APP_STAGE == 'prod' ? 'PROD' : 'DEV';
  148. const fileName = `KIPUI-${env}-${VUE_APP_LONG_VERSION_NAME}`;
  149. let config = {
  150. path: 'android/KIP-UI/simpleDemo/build.gradle',
  151. replace: [
  152. // { reg: /applicationId\s"(.*)"/, replacement: 'applicationId "' + buildConfigs[option.configuration].appId + '"' },
  153. {
  154. reg: /versionCode\s([\d\.]{1,})/,
  155. replacement: 'versionCode ' + versionCode,
  156. },
  157. {
  158. reg: /versionName\s"([\d\.]{1,})"/,
  159. replacement: 'versionName "' + versionName + '"',
  160. },
  161. {
  162. reg: /outputFileName\s=\s"(.*)"/,
  163. replacement: 'outputFileName = "' + fileName + '.apk"',
  164. },
  165. ],
  166. };
  167. fileContentReplace(config);
  168. // eslint-disable-next-line no-console
  169. console.log('updateVersionCode(): build.gradle DONE !');
  170. config = {
  171. path: 'ios/HBuilder-Hello/AppInfo.xcconfig',
  172. replace: [
  173. // { reg: /PRODUCT_NAME\s=\s(.*)/, replacement: 'PRODUCT_NAME = ' + buildConfigs[option.configuration].appName },
  174. // { reg: /PRODUCT_BUNDLE_IDENTIFIER\s=\s(.*)/, replacement: 'PRODUCT_BUNDLE_IDENTIFIER = ' + buildConfigs[option.configuration].appId },
  175. {
  176. reg: /MARKETING_VERSION\s=\s([\d\.]{1,})/,
  177. replacement: 'MARKETING_VERSION = ' + versionName,
  178. },
  179. {
  180. reg: /CURRENT_PROJECT_VERSION\s=\s([\d\.]{1,})/,
  181. replacement: 'CURRENT_PROJECT_VERSION = ' + versionCode,
  182. },
  183. ],
  184. };
  185. fileContentReplace(config);
  186. // eslint-disable-next-line no-console
  187. console.log('updateVersionCode(): AppInfo.xcconfig DONE !');
  188. };
  189. /**
  190. * 开发时,将【images文件夹下的图片】和【字体文件】的 url 转换为本地 http server 的地址
  191. *
  192. * build 时,将图片和字体的 url 转换为 CDN 地址,同时把文件 copy 到 temp 文件夹
  193. * @param {string} url url
  194. * @param {string} resourcePath 绝对路径
  195. * @returns 转换后的 url
  196. */
  197. const filePathHandler = (url, resourcePath) => {
  198. // 01. 处理 images 文件夹下的图片,包括 src/images/ 文件夹和 node_modules/@kip/ui-mobile 各组件中的 images 文件夹
  199. if (/images\//.test(resourcePath)) {
  200. if (!__PROD__) {
  201. // 开发时,用本地的 http server 来 host 静态资源
  202. // http://localhost:5000/src/images/abc/efg/hij.png
  203. return localPath + normalizePath(path.relative(__dirname, resourcePath));
  204. } else {
  205. // build 时,返回 CDN 地址,同时把文件 copy 到 temp 文件夹,
  206. // jenkins 会上传这些图片
  207. const tempPath = '/static/img/' + path.basename(resourcePath);
  208. const tempPathHashed = getFilePathWithHash(resourcePath, tempPath);
  209. fs.copy(resourcePath, './' + CDN_UPLOAD_FOLDER + tempPathHashed);
  210. // https://cdn.abc.com/static/img/hij.3677dgae.png
  211. return publicPath + tempPathHashed;
  212. }
  213. // 02. 处理字体文件
  214. } else if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(resourcePath)) {
  215. if (!__PROD__) {
  216. // 开发时,用本地的 http server 来 host 静态资源
  217. // http://localhost:5000/node_modules/@kip/ui-mobile/components/k-icon/kuiicons.ttf
  218. return localPath + normalizePath(path.relative(__dirname, resourcePath));
  219. } else {
  220. // build 时,返回 CDN 地址,同时把文件 copy 到 temp 文件夹,
  221. // jenkins 会上传这些字体
  222. const tempPath = '/static/fonts/' + path.basename(resourcePath);
  223. const tempPathHashed = getFilePathWithHash(resourcePath, tempPath);
  224. fs.copy(resourcePath, './' + CDN_UPLOAD_FOLDER + tempPathHashed);
  225. // https://cdn.abc.com/static/fonts/kuiicons.65d956cc.ttf
  226. return publicPath + tempPathHashed;
  227. }
  228. }
  229. // 03. 其他情况返回相对路径
  230. return (
  231. '/' + normalizePath(path.relative(process.env.UNI_INPUT_DIR, resourcePath))
  232. );
  233. };
  234. /**
  235. * 开发时,将 css 中的 url 转换为本地 http server 的地址
  236. *
  237. * build 时,返回 CDN 地址,同时把文件 copy 到 temp 文件夹
  238. * @param {string} resourcePath 绝对路径
  239. * @param {string} url css 中的原始 url
  240. * @returns 转换后的 url
  241. */
  242. const postcssUrlHandler = (resourcePath, url) => {
  243. // postcss-url 穿过来的 url 是 '@/images/home/css.png' 时,
  244. // resourcePath 会是 '/Users/joel.bh.zhang/Documents/Repo-template/uniapp-vue3-vite-clean/src/pages/index/@/images/home/css.png'
  245. // 需要处理一下
  246. if (url.startsWith('@')) {
  247. resourcePath = path.join(__dirname, './src/' + url.substring(1));
  248. console.log('after replace @ with ./src/ ==>', resourcePath);
  249. }
  250. // postcss-url 穿过来的 url: '/static/logo.png', 时,
  251. // resourcePath 会是 '/Users/joel.bh.zhang/Documents/Repo/ui-mobile/src/pages/index/static/logo.png',
  252. // 需要处理一下
  253. if (url.startsWith('/')) {
  254. resourcePath = path.join(__dirname, './src' + url);
  255. console.log('after replace start / with ./src/ ==>', resourcePath);
  256. }
  257. // 01. 处理 images 文件夹下的图片,包括 src/images/ 文件夹和 node_modules/@kip/ui-mobile 各组件中的 images 文件夹
  258. if (/\.(gif|jpe?g|png|svg)(\?.*)?$/.test(resourcePath)) {
  259. if (!__PROD__) {
  260. // 开发时,用本地的 http server 来 host 静态资源
  261. // http://localhost:5000/src/images/abc/efg/hij.png
  262. return localPath + normalizePath(path.relative(__dirname, resourcePath));
  263. } else {
  264. // build 时,返回 CDN 地址,同时把文件 copy 到 temp 文件夹,
  265. // jenkins 会上传这些图片
  266. const tempPath = '/static/img/' + path.basename(resourcePath);
  267. const tempPathHashed = getFilePathWithHash(resourcePath, tempPath);
  268. fs.copy(resourcePath, './' + CDN_UPLOAD_FOLDER + tempPathHashed);
  269. // https://cdn.abc.com/static/img/hij.3677dgae.png
  270. return publicPath + tempPathHashed;
  271. }
  272. // 02. 处理字体文件
  273. } else if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(resourcePath)) {
  274. if (!__PROD__) {
  275. // 开发时,用本地的 http server 来 host 静态资源
  276. // http://localhost:5000/node_modules/@kip/ui-mobile/components/k-icon/kuiicons.ttf
  277. return localPath + normalizePath(path.relative(__dirname, resourcePath));
  278. } else {
  279. // build 时,返回 CDN 地址,同时把文件 copy 到 temp 文件夹,
  280. // jenkins 会上传这些字体
  281. const tempPath = '/static/fonts/' + path.basename(resourcePath);
  282. const tempPathHashed = getFilePathWithHash(resourcePath, tempPath);
  283. fs.copy(resourcePath, './' + CDN_UPLOAD_FOLDER + tempPathHashed);
  284. // https://cdn.abc.com/static/fonts/kuiicons.65d956cc.ttf
  285. return publicPath + tempPathHashed;
  286. }
  287. }
  288. // 03. 其他情况返回相对路径
  289. return (
  290. '/' + normalizePath(path.relative(process.env.UNI_INPUT_DIR, resourcePath))
  291. );
  292. };
  293. /**
  294. * 处理 vite 在编译的时候出现 use-window-z-index.mjs 被
  295. * Could not resolve "../composables/use-window-z-index.mjs" from "node_modules/vant/es/config-provider/ConfigProvider.mjs"
  296. * file: /Users/sysadmin/code/kerry_project/temporary-parking-frontend/node_modules/vant/es/config-provider/ConfigProvider.mjs
  297. * 这个问题主要是在 vite.config.js 中配置了 define: {global: 'window'} 导致的
  298. * @param {string} params.scanDir 相对路径: node_modules/vant
  299. * @returns
  300. */
  301. function handleGlobalFiles({
  302. scanDir
  303. }) {
  304. return {
  305. name: 'handle-global-files',
  306. buildStart() {
  307. if(scanDir) {
  308. const rootDir = `${scanDir}`; // 指定你的源代码根目录
  309. // 递归函数,遍历所有子目录
  310. function traverseDirectory(dir) {
  311. const files = fs.readdirSync(dir);
  312. files.forEach(file => {
  313. const fullPath = path.join(dir, file);
  314. const stat = fs.statSync(fullPath);
  315. if (stat.isDirectory()) {
  316. // 如果是目录,递归处理
  317. traverseDirectory(fullPath);
  318. } else if (file.includes('global') && !fs.existsSync(file)) {
  319. // 如果文件名包含 'global' 并且还没有被创建的话
  320. const content = fs.readFileSync(fullPath, 'utf-8');
  321. const newContent = content.replace(/global/g, 'window');
  322. const newFileName = file.replace('global', 'window');
  323. const newFilePath = path.join(dir, newFileName);
  324. // 拷贝一份新的文件
  325. fs.writeFileSync(newFilePath, newContent, 'utf-8');
  326. }
  327. });
  328. }
  329. // 开始遍历根目录
  330. traverseDirectory(rootDir);
  331. }
  332. }
  333. };
  334. }
  335. module.exports = {
  336. IMAGES_SERVER_PORT,
  337. getLocalIP,
  338. getFilePathWithHash,
  339. formatImageUrl,
  340. gitHash,
  341. updateVersionCode,
  342. localPath,
  343. publicPath,
  344. filePathHandler,
  345. postcssUrlHandler,
  346. REGEX_IMG,
  347. REGEX_FONT,
  348. DIR_ASSET,
  349. DIR_IMG,
  350. DIR_FONTS,
  351. CDN_UPLOAD_FOLDER,
  352. handleGlobalFiles,
  353. };