vitePlugin.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. /* eslint-disable no-eval */
  2. /* eslint-disable import/extensions */
  3. /* eslint-disable @typescript-eslint/no-var-requires */
  4. import { PageContext, SubPageMetaDatum, PageMetaDatum } from '@uni-helper/vite-plugin-uni-pages'
  5. import { Plugin } from 'vite'
  6. import enums from './src/enums/index'
  7. const common = require('./scripts/common.js')
  8. const { files, cl } = common
  9. const config = {
  10. /** 打包路径 */
  11. mpBuildPath: '',
  12. /** 原始路径 */
  13. rootPath: '',
  14. env: process.env,
  15. }
  16. const convert = {
  17. /** 已整体瘦身 */
  18. slimed: false,
  19. /** 瘦身过的文件信息 */
  20. slimeds: {},
  21. /** 引用接口所有文件信息 */
  22. alleics: [] as any[],
  23. /** 瘦身接口配置信息 */
  24. slimInterfaceConfig({ filePath = '' } = {}) {
  25. // 路径获取失败 跳出
  26. if (!(config.mpBuildPath && config.rootPath)) return
  27. /** 打包路径 */
  28. const mpBuildPath = config.mpBuildPath
  29. /** 原始路径 */
  30. const rootPath = config.rootPath
  31. // 接口配置目录
  32. const interfacePath = 'utils/config/interFaces'
  33. // const path = 'D:\\cl\\work\\pro\\mobile\\dist\\build\\mp-weixin'
  34. // console.log(path, pfs)
  35. // 所有接口配置信息文件路径
  36. const ifs = files.path.resolve(rootPath, interfacePath)
  37. const iffs = files.getAllFile(ifs) // .filter((f) => f.includes(`utils\\config\\interFaces`))
  38. // 接口信息内容
  39. const ifcontents = iffs
  40. .filter((f) => !['index.ts'].some((s) => f.includes(s)))
  41. .map((m) => {
  42. let fsc = files.read(m)
  43. fsc = fsc
  44. // 删除realUrl后面的配置
  45. // .replace(/(realUrl.+),(?:(?!realUrl)[\s\S])+},/g, '$1},')
  46. // 删除reqType、resType配置信息
  47. .replace(/reqType(?:(?!,\n)[\s\S])+,\n/g, '')
  48. .replace(/resType(?:(?!,\n)[\s\S])+,\n/g, '')
  49. .replace(/\n/g, '')
  50. .replaceAll(' as const', '')
  51. // const fscm = fsc.match(/^(.+\.)(.+)=(.+)(;+\s*)*$/)
  52. // 反序列化最外层的对象
  53. const fscm = fsc.match(/\{[\s\S]+\}/)
  54. const groupName = files.basename(m, false)
  55. let gifs = {}
  56. if (fscm && fscm.length === 1) {
  57. try {
  58. gifs = (0, eval)(`(${fscm[0]})`)
  59. } catch (e) {
  60. cl.loge(m, e)
  61. }
  62. }
  63. return {
  64. /** 原始文件路径 */
  65. filePath: m,
  66. /** 打包后的文件路径 */
  67. buildFilePath: m.replace(rootPath, mpBuildPath).replace('.ts', '.js'),
  68. /** 分组名称 */
  69. groupName,
  70. /** 分组内的接口信息 */
  71. gifs,
  72. }
  73. })
  74. // 排除接口配置信息之外的所有js文件内容
  75. /** 引用接口所有文件内容 */
  76. let alleiConts = ''
  77. // 指定某个文件变化,替换公共存储内的本文件内容
  78. if (filePath) {
  79. cl.logs(`变化来源:${filePath}`)
  80. const filePathEnd = filePath.replace(rootPath, '')
  81. const curEic = convert.alleics.find(
  82. (f) =>
  83. f.filePath.replace(mpBuildPath, '').replace(/\..*$/, '') ===
  84. filePathEnd.replace(/\..*$/, ''),
  85. )
  86. if (curEic) {
  87. curEic.content = files.read(filePath)
  88. }
  89. // 整体瘦身时
  90. } else {
  91. const pfs = files.getAllFile(mpBuildPath)
  92. convert.alleics = pfs
  93. .filter(
  94. (f) => f.endsWith('.js') && !f.includes('node-modules') && !f.includes(interfacePath),
  95. )
  96. .map((m) => {
  97. return {
  98. filePath: m,
  99. content: files.read(m),
  100. }
  101. })
  102. }
  103. alleiConts = convert.alleics.map((m) => m.content).join('')
  104. alleiConts = alleiConts.replaceAll(' ', '').replaceAll('\n', '').replaceAll('\r', '')
  105. // 删除未用到的接口配置信息
  106. ifcontents.forEach((f) => {
  107. const curGifs = f.gifs
  108. Object.keys(f.gifs).forEach((fk) => {
  109. // 使用痕迹
  110. const UsageTraces = [
  111. `${f.groupName}.${fk}.`,
  112. `${f.groupName}.${fk}(`,
  113. `${f.groupName}.${fk}<`,
  114. ]
  115. // 删除 未使用的接口信息
  116. if (!UsageTraces.some((s) => alleiConts.includes(s))) {
  117. delete curGifs[fk]
  118. } else {
  119. // 删除类型信息对象
  120. delete curGifs[fk].reqType
  121. delete curGifs[fk].resType
  122. }
  123. })
  124. const curSD = convert.slimeds[f.groupName]
  125. if (curSD) {
  126. if (JSON.stringify(curSD.gifs) !== JSON.stringify(f.gifs)) {
  127. curSD.gifs = f.gifs
  128. cl.logs('变化的文件:' + curSD.buildFilePath)
  129. }
  130. } else {
  131. // 记录瘦身信息
  132. convert.slimeds[f.groupName] = f
  133. }
  134. })
  135. // 瘦身目标目录的接口配置信息
  136. if (!convert.slimed) {
  137. cl.logw('整体瘦身中...')
  138. ifcontents.forEach((f) => {
  139. files.write(
  140. f.buildFilePath,
  141. `"use strict";exports.${f.groupName}=${JSON.stringify(f.gifs)};`,
  142. )
  143. cl.logs('瘦身文件:' + f.buildFilePath)
  144. })
  145. cl.logs('整体瘦身结束')
  146. }
  147. },
  148. /** 生成页面配置文件 */
  149. generateFileOper(type, callBack) {
  150. const pagesPath = files.getPath(`src/utils/config/${type === 0 ? 'pages' : 'subPages'}.ts`)
  151. let pagesContent = files.read(pagesPath)
  152. pagesContent = pagesContent.replace('export default', '')
  153. pagesContent = pagesContent.replaceAll(' as ayPage', '')
  154. /** 读取的旧的配置信息-可手动修改 */
  155. let pagesContentObj
  156. try {
  157. pagesContentObj = (0, eval)(`(${pagesContent})`)
  158. } catch (e) {
  159. pagesContentObj = {}
  160. }
  161. callBack(pagesContentObj)
  162. let pageMetaDataStr = JSON.stringify(pagesContentObj, null, 2)
  163. // 去掉"
  164. pageMetaDataStr = pageMetaDataStr.replace(/\"(.+)\":/g, '$1:').replaceAll('"', `'`)
  165. // pageMetaDataStr = pageMetaDataStr.replaceAll('},', `} as ayPage,`)
  166. // 追加 ayPage 类型
  167. // pageMetaDataStr = pageMetaDataStr.replace(/(isPager:.+\s+\})([\n,])/g, '$1 as ayPage$2')
  168. pageMetaDataStr = pageMetaDataStr.replace(/(\})(\n\}|,)/g, '$1 as ayPage$2')
  169. files.write(pagesPath, `export default ${pageMetaDataStr}`)
  170. },
  171. /** 生成页面配置信息 */
  172. generatePageConfig({
  173. ctx,
  174. spmd,
  175. pagesContentObj,
  176. }: {
  177. ctx?: PageContext
  178. /** 子包页面 */
  179. spmd?: SubPageMetaDatum
  180. /** 最终的配置信息-可手动修改,对应utils/config/pages.ts 或 utils/config/subPages.ts */
  181. pagesContentObj: AnyObject
  182. }) {
  183. /** 新生成的配置 */
  184. const pageMetaDataObj = {}
  185. const getPageType = (f) => {
  186. let rv = enums.PageType.page
  187. if (ctx?.pagesGlobConfig?.tabBar?.list.some((s) => s?.pagePath === f.path)) {
  188. rv = enums.PageType.tabPage
  189. }
  190. // if (ctx.options.homePage.includes(f.path)) {
  191. // rv = 'home'
  192. // }
  193. return rv
  194. }
  195. let cusPages = [] as PageMetaDatum[]
  196. if (spmd?.pages) {
  197. cusPages = spmd.pages.map<PageMetaDatum>((m) => {
  198. return {
  199. ...m,
  200. path: `${spmd.root}/${m.path}`,
  201. }
  202. })
  203. }
  204. const curPages = ctx?.pageMetaData || cusPages
  205. curPages.forEach((f) => {
  206. let key = f.path.replaceAll('/', '_')
  207. if (!/^[$A-Z_][0-9A-Z_$]*$/i.test(key)) {
  208. cl.loge(`${f.path}名称不合法,只能包含字母数字下划线`)
  209. return
  210. }
  211. // key值移除包名前缀
  212. // 主包页面
  213. if (ctx) {
  214. key = key.replace('pages_', '')
  215. }
  216. // 分包页面
  217. if (spmd?.root) {
  218. key = key.replace(spmd.root + '_', '')
  219. }
  220. pageMetaDataObj[key] = {
  221. // 为了方便小程序跳转,这里拼上/
  222. _url: '/' + f.path,
  223. _type: getPageType(f),
  224. title: key,
  225. // 登录不需要身份
  226. identity: !f.path.includes('login/index'),
  227. isPager: false,
  228. }
  229. // 手动设置了style,赋值给PageMetaDatum
  230. if (pagesContentObj[key]?.style) {
  231. f.style = pagesContentObj[key].style
  232. }
  233. })
  234. // 删除配置-因:删掉的目录
  235. Object.keys(pagesContentObj).forEach((k) => {
  236. if (!pageMetaDataObj[k]) {
  237. cl.logw(`删除的页面:${pagesContentObj[k]._url}`)
  238. delete pagesContentObj[k]
  239. }
  240. })
  241. /** 增量生成页面配置信息 */
  242. for (const k in pageMetaDataObj) {
  243. // 页面已存在
  244. if (pagesContentObj[k]) {
  245. const cur = pagesContentObj[k]
  246. // 重置只读属性(_开头的)值
  247. Object.keys(cur).forEach((f) => {
  248. if (f.startsWith('_')) delete cur[f]
  249. })
  250. pagesContentObj[k] = {
  251. ...pageMetaDataObj[k],
  252. ...pagesContentObj[k],
  253. }
  254. } else {
  255. // 页面不存在
  256. pagesContentObj[k] = pageMetaDataObj[k]
  257. cl.logs(`新增的页面:${pageMetaDataObj[k]._url}`)
  258. }
  259. }
  260. },
  261. }
  262. /** 小程序瘦身插件 */
  263. function slimMPPlugin(): Plugin {
  264. if (config.env.UNI_PLATFORM === 'h5') {
  265. return {
  266. name: 'slimMP-plugin',
  267. }
  268. } else {
  269. return {
  270. name: 'slimMP-plugin',
  271. // apply: 'build',
  272. // renderStart(outputOptions, inputOptions) {
  273. // },
  274. // renderChunk() {
  275. // },
  276. writeBundle1(options, bundle) {
  277. for (const filename in bundle) {
  278. // 获取文件内容
  279. const chunk = bundle[filename]
  280. delete bundle[filename]
  281. // try {
  282. // // 内容未变化的文件,不进行磁盘写入
  283. // const fileContent = files.read(options.dir + '/' + filename)
  284. // let codekn = 'code'
  285. // if (chunk.type === 'asset') {
  286. // codekn = 'source'
  287. // }
  288. // if (chunk[codekn] === fileContent) {
  289. // console.log(filename)
  290. // delete bundle[filename]
  291. // }
  292. // } catch {}
  293. }
  294. },
  295. // 输出生成阶段的第一个钩子是 outputOptions,
  296. /** 输出生成阶段最后一个钩子 */
  297. generateBundle(options, bundle) {
  298. // 开发环境减少每次编译后的文件写入
  299. if (config.env.NODE_ENV !== 'development') return
  300. // files.write(files.getPath('temp/generateBundle.json'), JSON.stringify(arguments))
  301. // 遍历所有的打包文件
  302. for (const filename in bundle) {
  303. // 获取文件内容
  304. const chunk = bundle[filename]
  305. if (chunk.type === 'chunk') {
  306. // 写入瘦身后的 接口配置信息
  307. if (filename.includes('config/interFaces')) {
  308. const groupName = files.basename(filename, false)
  309. const curSD = convert.slimeds[groupName]
  310. if (curSD) {
  311. chunk.code = `"use strict";exports.${groupName}=${JSON.stringify(curSD.gifs)};`
  312. }
  313. }
  314. }
  315. try {
  316. // 内容未变化的文件,不进行磁盘写入
  317. const fileContent = files.read(options.dir + '/' + filename)
  318. let codekn = 'code'
  319. if (chunk.type === 'asset') {
  320. codekn = 'source'
  321. }
  322. if (chunk[codekn] === fileContent) {
  323. delete bundle[filename]
  324. }
  325. } catch {}
  326. }
  327. },
  328. /** 监听文件修改 */
  329. watchChange(filePath) {
  330. // h5环境 跳出
  331. // if (config.env.UNI_PLATFORM === 'h5') return
  332. // 已整体瘦身 & 有路径 & 非\utils\config目录下的 & filePath包含\\(小程序路径)
  333. if (
  334. convert.slimed &&
  335. filePath &&
  336. !filePath.includes('\\utils\\config') &&
  337. filePath.includes('\\')
  338. ) {
  339. convert.slimInterfaceConfig({ filePath })
  340. }
  341. },
  342. buildStart(options) {
  343. // files.write(files.getPath('temp/buildStart.json'), JSON.stringify(options))
  344. // return
  345. if (options.plugins) {
  346. const unimp: any = options.plugins.find((f) => ['uni:mp'].includes(f.name))
  347. if (unimp) {
  348. config.mpBuildPath = unimp.uni.copyOptions.targets[0].dest
  349. config.rootPath = unimp.uni.compilerOptions.root
  350. }
  351. }
  352. },
  353. closeBundle() {
  354. if (convert.slimed) {
  355. return
  356. }
  357. convert.slimInterfaceConfig()
  358. convert.slimed = true
  359. },
  360. }
  361. }
  362. }
  363. /** 生成页面配置信息 */
  364. function generatePageConfig(ctx: PageContext) {
  365. const pagesContentObj = {}
  366. convert.generateFileOper(0, (pagesContentObj) => {
  367. convert.generatePageConfig({ ctx, pagesContentObj })
  368. })
  369. if (ctx.subPageMetaData.length) {
  370. const pagesContentObj = {}
  371. convert.generateFileOper(1, (pagesContentObj) => {
  372. ctx.subPageMetaData.forEach((f) => {
  373. if (!pagesContentObj[f.root]) {
  374. pagesContentObj[f.root] = {}
  375. }
  376. convert.generatePageConfig({
  377. spmd: f,
  378. pagesContentObj: pagesContentObj[f.root],
  379. })
  380. })
  381. })
  382. }
  383. }
  384. export { slimMPPlugin, generatePageConfig }