@wiajs/core 1.1.28 → 1.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,978 @@
1
+ /**
2
+ * 将rspack、swc等编译生成的less、html、js文件打包到一个js文件并zip压缩
3
+ * 开发模式与生产模式输出的代码差异很大:
4
+ * - 入口文件直接放最后调用,没有封装到代码数组中
5
+ * - __webpack_require__ 被压缩成不同的字母,无法跨文件调用
6
+ * - 奇怪的是,local模式生产打包,保留了 __webpack_require__,未保留入口文件
7
+ * - local模式时,router 使用了 __webpack_require__,如果被压缩改名,将无法运行
8
+ */
9
+ import path from 'node:path'
10
+ import util from 'node:util'
11
+ import crypto from 'node:crypto'
12
+ import zlib from 'node:zlib'
13
+ import fs from 'fs-extra'
14
+ import ld from 'lodash'
15
+ // const {minify, minify_sync} = require('terser')
16
+ import {parseSync, Compiler, minify} from '@swc/core' // acorn 替换为 swc
17
+
18
+ import cfg from './config.js'
19
+
20
+ const gzip = util.promisify(zlib.gzip)
21
+
22
+ /**
23
+ * @typedef {object} Opts
24
+ * @prop {string} owner - 应用所有者
25
+ * @prop {string} name - 应用名称
26
+ * @prop {string} ver - 版本号
27
+ * @prop {string} [dir] - 代码根路径
28
+ * @prop {string} [out] - 发布打包输出路径
29
+ * @prop {string} [cos] - 资源托管网址
30
+ * @prop {string[]} [load] - 启动时需加载的模块,启动器需要,一般加载wia公用模块
31
+ * @prop {boolean} [press] - 压缩代码
32
+ * @prop {boolean} [gzip] - gzip打包
33
+ */
34
+
35
+ /**
36
+ * @typedef {object} Opt
37
+ * @prop {string} owner
38
+ * @prop {string} name
39
+ * @prop {string} ver
40
+ * @prop {string} cos
41
+ * @prop {string[]} [load] - 启动器需要,一般加载wia公用模块
42
+ * @prop {string} dir
43
+ * @prop {string} out
44
+ * @prop {boolean} press - 压缩代码
45
+ * @prop {boolean} gzip - gzip打包
46
+ */
47
+
48
+ const def = {
49
+ owner: '',
50
+ name: '',
51
+ ver: '1.0.0',
52
+ cos: 'https://cos.wia.pub', // 资源主机
53
+ dir: cfg.code.dir,
54
+ out: 'dist', // 输出路径
55
+ press: true,
56
+ gzip: true,
57
+ }
58
+
59
+ export default class WiaPack {
60
+ /** @type {Opt} */
61
+ opt
62
+ /** @type {Object.<string, string>} */
63
+ files
64
+
65
+ /**
66
+ *
67
+ * @param {Opts} opts
68
+ */
69
+ constructor(opts) {
70
+ /** @type {Opt} */
71
+ const opt = {...def, ...opts}
72
+ this.opt = opt
73
+ }
74
+
75
+ /**
76
+ * 调用rspack/webpack进行编译打包
77
+ * @param {{js: Object.<*, string>}} uf - 更新文件对象
78
+ * {js: {'page/login': './dist/page/login.js'}}
79
+ * @param {*} pf - page目录下,所有改变、未改变js文件,pf为空,表示不生成page.js
80
+ */
81
+ async work(uf, pf) {
82
+ let R = {}
83
+ const _ = this
84
+ const {opt} = _
85
+ const {owner, name, ver, dir} = opt
86
+
87
+ try {
88
+ _.files = uf
89
+
90
+ console.log('work...', {uf, pf})
91
+
92
+ // 处理编译后的代码
93
+ R = await _.process(uf.js, pf)
94
+ } catch (e) {
95
+ console.log(`work exp:${e.message}`)
96
+ }
97
+
98
+ return R
99
+ }
100
+
101
+ /**
102
+ * 处理webpack生成的文件,分离wia、index、page,并将页面的js、css、html 合并成一个js文件
103
+ * @param {*} uf - 更新文件对象:{'page/login': './dist/page/login.js'}
104
+ * @param {*} pf - 为页面对象,包含所有改变、未改变js文件,pf为空,表示不生成page.js
105
+ * @returns {Promise<*[]>} - 对象数组[{'mall/page/login':'./dist/mall/page/login.js'}]
106
+ */
107
+ async process(uf, pf) {
108
+ const R = []
109
+ const _ = this
110
+ const {opt} = _
111
+ const {owner, name, ver, dir, out} = opt
112
+
113
+ console.log(`${owner}/${name} process ...`)
114
+
115
+ try {
116
+ let index // index中包含的模块
117
+
118
+ // 模块引用次数,查找仅引用一次的模块,包含到页面,不作为共用
119
+ /** @type {Object.<*,string>} */
120
+ let rf = {}
121
+
122
+ // 针对每个变化源文件生成的打包文件进行处理,提取所有页面公共部分到 page.js 文件
123
+ const ufk = Object.keys(uf)
124
+ for (const k of ufk) {
125
+ let f = path.resolve(dir, `dist/${k}.js`)
126
+ if (path.sep !== '/') f = f.replace(/\\/gim, '/') // 统一路径为/,方便处理
127
+
128
+ if (fs.existsSync(f)) {
129
+ const ms = await _.getModule(f)
130
+ if (ms) {
131
+ for (const m of ms) {
132
+ if (m.org) for (const n of ms) n.code = n.code.replaceAll(m.org, m.name)
133
+
134
+ if (!cfg.module?.includes(m.name)) rf[m.name] = (rf[m.name] ?? 0) + 1
135
+ }
136
+ console.log('%s modules:%d file:%s', k, ms.length, uf[k])
137
+ } else console.error('%s modules:%d file:%s', k, 0, uf[k])
138
+
139
+ // 保存模块,稍后处理
140
+ uf[k] = {name: uf[k], ms, f}
141
+ // 首页
142
+ if (/\/dist\/index.js$/.test(f)) index = ms
143
+
144
+ // index.js与页面js各自打包,页面公共部分,不在index中的,打入page.js,wia引用部分打入wia.js
145
+ // const wf = this.procFile(f, uf[k], ms, index, page, appcfg.load);
146
+ // if (wf) R.push({[k]: wf});
147
+ }
148
+ }
149
+
150
+ // 提取未修改page文件的引用模块,重新生成page.js
151
+ const fk = ld.difference(Object.keys(pf), Object.keys(uf))
152
+
153
+ const pfile = path.resolve(`${dir}/${out}`, './page.js')
154
+ let lp // lastPage
155
+ const lpf = {} // lastPageFiles 未更改页面文件引用模块
156
+ // 获取当前页面共用模块
157
+ if (fs.existsSync(pfile)) {
158
+ const tx = await fs.readFile(pfile, 'utf8')
159
+ try {
160
+ lp = JSON.parse(tx)
161
+ } catch (e) {}
162
+ } else console.warn('process %s.%s page.js not found!', owner, name)
163
+
164
+ // 未更新的页面,提取引用部分模块,排除index,保存到page中,代码从page.js获取
165
+ // 如page.js 不存在,则需清除wiafile.yml 文件中的页面js,重新编译生成page.js!!!
166
+ if (lp && fk && fk.length > 0) {
167
+ // 提取未修改page文件的引用模块,使用现有page.js中的代码
168
+ for (const k of fk) {
169
+ const f = path.resolve(`${dir}/dist`, `${k}.js`)
170
+
171
+ if (fs.existsSync(f)) {
172
+ try {
173
+ const tx = await fs.readFile(f, 'utf8')
174
+ const p = JSON.parse(tx)
175
+ lpf[k] = {f, name: pf[k], pf: p, ms: []}
176
+ const {ms} = lpf[k]
177
+ // 还原页面引用模块,排除wia模块
178
+ if (p && p.M) {
179
+ p.M.forEach(m => {
180
+ if (!cfg.module?.includes(m)) ms.push({name: m, code: lp[m]})
181
+ })
182
+ // 增加模块引用计数
183
+ if (ms.length) {
184
+ ms.forEach(m => {
185
+ rf[m.name] = (rf[m.name] ?? 0) + 1
186
+ })
187
+ }
188
+ console.log('%s modules:%d', k, ms.length)
189
+ }
190
+ } catch (e) {
191
+ console.error(`process last page:${k} exp:${e.message}`)
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ // 所有page共用的js
198
+ const page = {
199
+ R: {
200
+ name: `${owner}/${name}/page`,
201
+ ver,
202
+ time: +new Date(),
203
+ },
204
+ M: [],
205
+ }
206
+
207
+ // 共用的wia模块
208
+ const wia = {
209
+ R: {
210
+ name: `${owner}/${name}/wia`,
211
+ ver,
212
+ time: +new Date(),
213
+ },
214
+ M: [],
215
+ }
216
+
217
+ // 首页模块
218
+ index = {js: {}}
219
+
220
+ // 唯一引用,页面唯一引用包含到页面
221
+ const rfs = []
222
+ Object.keys(rf).forEach(k => {
223
+ if (rf[k] === 1) rfs.push(k)
224
+ })
225
+ rf = rfs
226
+
227
+ // 处理更新文件
228
+ for (const k of Object.keys(uf)) {
229
+ // index.js与页面js各自打包,页面公共部分,不在index中的,打入page.js,wia引用部分打入wia.js
230
+ const wf = await _.procFile(uf[k], rf, index, wia, page)
231
+ if (wf) R.push({[k]: wf})
232
+ }
233
+
234
+ // 处理未更新页面文件
235
+ for (const k of Object.keys(lpf)) {
236
+ // index.js与页面js各自打包,页面公共部分,不在index中的,打入page.js,wia引用部分打入wia.js
237
+ const wf = await _.procPgfile(lpf[k], rf, index, wia, page)
238
+ if (wf) R.push({[k]: wf})
239
+ }
240
+
241
+ // 最后处理 index,页面共享代码,并入 index
242
+ if (index.name && !ld.isEmpty(index.js)) {
243
+ const wf = await _.pack(index.name, index.f, index.js, index.pms)
244
+ if (wf) R.push({[index.name]: wf})
245
+ }
246
+
247
+ // 非页面自身的引用文件,写入 page
248
+ if (page?.M.length) {
249
+ const wf = await _.procPage(page)
250
+ if (wf) R.push({page: wf})
251
+ }
252
+
253
+ // 非共用wia,写入 wia
254
+ if (wia?.M.length) {
255
+ const wf = await _.procWia(wia)
256
+ if (wf) R.push({wia: wf})
257
+ }
258
+ } catch (e) {
259
+ console.log(`process exp:${e.message}`)
260
+ }
261
+
262
+ return R
263
+ } // process
264
+
265
+ /**
266
+ * 处理更新的js文件,当前页面的js、html、css 打包压缩成一个文件
267
+ * @param {*} u 更新文件对象,{f, ms, name}
268
+ * f: 文件在硬盘中的绝对文件名
269
+ * rf: 相对文件名称 mall/page/shopList.js
270
+ * ms: 该文件包含的模块数组
271
+ * 模块名称:“./wia/store/src/app.js” 或 “./code/wia/store/src/app.js” 取决于运行路径
272
+ * @param {*} rf 当前页面唯一引用模块,需打包到当前页面文件
273
+ * @param {*} index 首页模块,页面共用模块放入index中加载,所有页面加载之前,需加载index
274
+ * @param {*} wia wia 共用wia中未包含,放入当前应用wia,便于生成共用wia
275
+ * @param {*} page 非页面模块(包括共用和非共用模块),全部放入 page 保存,加快未变更页面模块分析速度
276
+ */
277
+ async procFile(u, rf, index, wia, page) {
278
+ let R = ''
279
+ const _ = this
280
+ const {opt} = _
281
+
282
+ try {
283
+ const js = {}
284
+ let ps
285
+
286
+ // 页面引用模块,排除自己
287
+ const pms = u.ms.filter(m => m.name !== u.name).map(m => m.name)
288
+ const rgwia = new RegExp(cfg.code.wia.join('|'))
289
+
290
+ // index.js 应用入口代码,wia.js 除外的模块,全部保留在index.js中
291
+ if (/\/dist\/index.js$/.test(u.f)) {
292
+ // 符合wia规则,未包含在共用wia中的,存入应用wia,便于更新wia包
293
+ ps = u.ms.filter(m => !cfg.module?.includes(m.name) && rgwia.test(m.name))
294
+ for (const m of ps) {
295
+ if (!wia[m.name]) {
296
+ wia[m.name] = m.code
297
+ wia.M.push(m.name)
298
+ }
299
+ }
300
+
301
+ index.pms = pms // 引用模块
302
+ index.f = u.f
303
+ index.name = u.name
304
+ index.js = {}
305
+
306
+ // ps = u.ms.filter(m => !rg.test(m.name));
307
+ // 未包含在共用wia中,不符合wia规则,放入 index,跟page共用模块合并后生成index.js
308
+ ps = u.ms.filter(m => !cfg.module?.includes(m.name) && !rgwia.test(m.name))
309
+ for (const m of ps) {
310
+ // index.push(m.name);
311
+ // js[m.name] = m.code;
312
+ if (m && m.code && m.name) {
313
+ // 压缩代码, 生产模式ast效率低,使用eval开发模式编译,代码为字符串,效率高
314
+ index.js[m.name] = opt.press ? await pressCode(m.code) : m.code
315
+ }
316
+ }
317
+ } else {
318
+ // 页面共用模块存入 page
319
+ // 非页面自身,非共用wia
320
+ ps = u.ms.filter(m => m.name !== u.name && !cfg.module?.includes(m.name))
321
+ // 页面引用模块写入统一的page对象
322
+ for (const m of ps) {
323
+ // 非唯一引用的共用模块,放入 index
324
+ if (!rf.includes(m.name)) {
325
+ // 符合wia规则,非共用wia,存入应用wia,便于更新wia包
326
+ if (rgwia.test(m.name)) {
327
+ if (!wia[m.name]) {
328
+ wia[m.name] = m.code
329
+ wia.M.push(m.name)
330
+ }
331
+ } else if (!index.js?.[m.name]) index.js[m.name] = m.code
332
+ }
333
+
334
+ // 所有引用模块(非页面自身),全部放入 page 保存,加快未变更页面的模块分析速度
335
+ if (!page[m.name]) {
336
+ page[m.name] = m.code
337
+ page.M.push(m.name)
338
+ }
339
+ }
340
+
341
+ // 页面自身 和 唯一引用,存于当前页面
342
+ ps = u.ms.filter(m => m.name === u.name || rf.includes(m.name))
343
+ for (const m of ps) {
344
+ if (rgwia.test(m.name)) {
345
+ if (!wia[m.name]) {
346
+ wia[m.name] = m.code
347
+ wia.M.push(m.name)
348
+ }
349
+ } else if (m && m.code && m.name) {
350
+ // 压缩代码, 生产模式ast效率低,使用eval开发模式编译,代码为字符串,效率高
351
+ js[m.name] = opt.press ? await pressCode(m.code) : m.code
352
+ }
353
+ }
354
+ }
355
+
356
+ // 当前页面的js、html、css 打包压缩成一个文件
357
+ if (!ld.isEmpty(js)) await _.pack(u.name, u.f, js, pms)
358
+
359
+ R = u.f
360
+ } catch (e) {
361
+ console.error(`procfile exp:${e.message}`)
362
+ }
363
+ return R
364
+ }
365
+
366
+ /**
367
+ * 处理未变更页面代码
368
+ * 1、共享代码打包到index
369
+ * 2、wia 代码,非缓存,打包到 wia.js,方便监测、收集独立的共享wia包
370
+ * 3、原来共享的,可能变为独享,需加入到页面js
371
+ * @param {*} u 更新文件对象,{f, ms, name, pf}
372
+ * f: 文件在硬盘中的绝对文件名
373
+ * name: 相对文件名称 mall/page/shopList.js
374
+ * ms: 该文件包含的模块数组
375
+ * pf:页面文件 对象
376
+ * 模块名称:“./wia/store/src/app.js” 或 “./code/wia/store/src/app.js” 取决于运行路径
377
+ * @param {*} rf 引用模块次数
378
+ * @param {*} index 首页模块
379
+ * @param {*} wia wia模块
380
+ * @param {*} load 运行依赖,需提前加载的模块,需修改加载版本号
381
+ * @returns {Promise<*>}
382
+ */
383
+ async procPgfile(u, rf, index, wia, page, load) {
384
+ let R = ''
385
+
386
+ try {
387
+ const {owner, name: app} = this
388
+ // 页面引用模块,排除自己
389
+ const rg = new RegExp(cfg.code.wia.join('|'))
390
+ const ljs = u.pf.js
391
+ const js = {}
392
+ js[u.name] = ljs[u.name]
393
+
394
+ // index.js 应用入口代码,wia.js 除外的模块,全部保留在index.js中
395
+ // 页面共用模块存入 page
396
+ // 排除自己,排除 wia
397
+ const ps = u.ms.filter(m => m.name !== u.name && !cfg.module?.includes(m.name))
398
+ // 页面引用模块写入统一的page对象
399
+ ps.forEach(m => {
400
+ // 共用模块,放入 index
401
+ if (!rf.includes(m.name) && !index[m.name]) index[m.name] = m.code
402
+
403
+ // 非页面模块,全部放入 page 保存,加快未变更页面的模块分析速度
404
+ if (!page[m.name]) {
405
+ page[m.name] = m.code
406
+ page.M.push(m.name)
407
+ }
408
+
409
+ // 符合wia规则,未包含在wia中的,存入wia,便于更新wia包
410
+ if (rg.test(m.name) && !wia[m.name]) {
411
+ wia[m.name] = m.code
412
+ wia.M.push(m.name)
413
+ }
414
+
415
+ // 唯一引用,包含到页面代码
416
+ if (rf.includes(m.name)) {
417
+ // 非唯一依赖由于其他页面变化,可能会变成唯一依赖
418
+ if (!js[m.name]) {
419
+ js[m.name] = m.code
420
+ }
421
+ }
422
+ })
423
+
424
+ // 有变化,则重新保存
425
+ if (!ld.isEmpty(ld.difference(Object.keys(js), Object.keys(ljs)))) {
426
+ u.pf.R = {
427
+ name: u.name.replace(/\.js$/i, ''),
428
+ ver: this.ver,
429
+ time: +new Date(),
430
+ }
431
+ u.pf.js = js
432
+ const wf = JSON.stringify(u.pf)
433
+ await fs.writeFile(u.f, wf, e => {
434
+ if (e) console.log(`save ${u.f} exp:${e.message}`)
435
+ R = u.f
436
+ })
437
+ }
438
+ } catch (e) {
439
+ console.error(`procPgFile exp:${e.message}`)
440
+ }
441
+
442
+ return R
443
+ }
444
+
445
+ /**
446
+ * js、html、css 打包保存
447
+ * @param {*} name - 相对路径文件js文件
448
+ * @param {*} f - 绝对路径js文件
449
+ * @param {*} js - 引用模块
450
+ * @param {*} pms
451
+ */
452
+ async pack(name, f, js, pms) {
453
+ const _ = this
454
+ const {opt} = _
455
+ const {owner, name: appName, ver, dir, out} = opt
456
+
457
+ try {
458
+ // 将页面文件打包到一个文件中
459
+ const w = {
460
+ R: {
461
+ name: name.replace(/\.js$/i, ''),
462
+ ver: ver,
463
+ time: +new Date(),
464
+ },
465
+ html: '',
466
+ css: '',
467
+ js: '',
468
+ }
469
+
470
+ // 加入 html,index.html 不打入包中!!!
471
+ if (!/\/dist\/index\.js$/.test(f)) {
472
+ const htmlf = f.replace(/\.js$/i, '.html')
473
+ if (fs.existsSync(htmlf)) {
474
+ const html = await fs.readFile(htmlf, 'utf8')
475
+ if (html) w.html = html
476
+ }
477
+ }
478
+
479
+ // 加入 css
480
+ const cssf = f.replace(/\.js$/i, '.css')
481
+ if (fs.existsSync(cssf)) {
482
+ const css = await fs.readFile(cssf, 'utf8')
483
+ if (css) w.css = css
484
+ }
485
+
486
+ w.js = js
487
+ w.M = pms
488
+
489
+ let wf = '' // 写入文件内容
490
+ // 配置了load参数(如 wiastore),index 作为wia应用启动器,需使用$.M 加载 index 及 wia 模块
491
+ // 未配置load参数,只有模块,由其他启动器动态加载运行,wia应用由wiastore 加载运行
492
+ if (opt.load && /\/dist\/index\.js$/.test(f)) {
493
+ // 更改 load中的版本号,确保客户端加载新发布的版本
494
+ let load = opt.load.map(v => `"${v}"`).join(',')
495
+ // 替换 ?v=${ver} 为当前版本
496
+ load = load.replace(/\?v=\$\{ver\}/gim, `?v=${ver}`)
497
+
498
+ // 获取并加载 wia.js,加载index.js中的所有模块,调用index模块
499
+ wf =
500
+ `(function(){var app=${JSON.stringify(w)};\r\n` +
501
+ `$.M.get("${opt.cos}", [${load}])\r\n` +
502
+ `.then(function(){$.M.add(app.js);$.M("${owner}/${appName}/index.js");})})();\r\n`
503
+
504
+ // 未压缩
505
+ f = f.replace(/([\\\/])dist[\\\/]/, `$1${out}$1`)
506
+ // await fs.ensureDir(path.dirname(f))
507
+ // fs.writeFile(f, wf)
508
+ } else wf = JSON.stringify(w)
509
+
510
+ // 未压缩,生产需屏蔽
511
+ // f = f.replace(/([\\\/])dist[\\\/]/, `$1${out}$1`)
512
+ // await fs.ensureDir(path.dirname(f))
513
+ // fs.writeFile(f, wf)
514
+
515
+ // 压缩
516
+ f = f.replace(/([\\\/])dist[\\\/]/, `$1${out}$1`).replace(/\.js$/, '.zip') // write file
517
+ // @ts-ignore
518
+ const buf = await gzip(wf)
519
+ await fs.ensureDir(path.dirname(f))
520
+ fs.writeFile(f, buf)
521
+ console.log(
522
+ `Build ${name} gzipped(${(wf.length / 1024).toFixed(2)}KB -> ${(buf.length / 1024).toFixed(2)}KB)`
523
+ )
524
+ } catch (e) {
525
+ console.error(`pack exp:${e.message}`)
526
+ }
527
+ }
528
+
529
+ /**
530
+ * 页面模块按名称排序
531
+ */
532
+ sortPage(ms) {
533
+ const R = {}
534
+
535
+ if (ld.isEmpty(ms)) return {}
536
+ R.R = ms.R
537
+ const ks = ld.sortBy(ld.keys(ms))
538
+ ld.forIn(ks, k => {
539
+ if (k !== 'R') R[k] = ms[k]
540
+ })
541
+
542
+ return R
543
+ }
544
+
545
+ /**
546
+ * 生成指定文件的 MD5值,用于判断文件内容是否变化
547
+ * @param {string} tx
548
+ * @returns {string}
549
+ */
550
+ hash(tx) {
551
+ let R = ''
552
+ const hash = crypto.createHash('md5')
553
+ hash.update(tx)
554
+ R = hash.digest('hex')
555
+ return R
556
+ }
557
+
558
+ /**
559
+ * 将首页之外的页面共用模块,保存到page.js,便于分析
560
+ * 该部分模块已包含在index.js,
561
+ * 由于index加载前,需要加载page,因此合并page到index,不单独加载page
562
+ * @param {*} page 首页之外的模块
563
+ */
564
+ async procPage(page) {
565
+ let R
566
+ const _ = this
567
+ const {opt} = _
568
+ const {owner, name, dir, out} = opt
569
+
570
+ try {
571
+ if (!page?.M.length) return
572
+
573
+ console.log(`${owner}/${name} procPage(M:${page.M.length}) ...`)
574
+
575
+ // 压缩代码
576
+ if (opt.press) {
577
+ for (const k of Object.keys(page)) {
578
+ if (k !== 'R' && k !== 'M') page[k] = await pressCode(page[k])
579
+ }
580
+ }
581
+
582
+ const f = path.resolve(`${dir}/${out}`, './page.js')
583
+
584
+ // 添加自动合并模块 代码
585
+ // const wf = `(function(){var js=${JSON.stringify(wia)};\r\n`
586
+ // + '$._m.add(js);})();\r\n';
587
+ const wf = JSON.stringify(page)
588
+
589
+ // console.log('Module:%s%s', JSON.stringify(p), '\r\n');
590
+ await fs.ensureDir(path.dirname(f))
591
+ await fs.writeFile(f, wf, e => {
592
+ if (e) console.error(`save ${f} error:${e.message}`)
593
+ })
594
+ R = f
595
+ } catch (e) {
596
+ console.error(`procPage exp:${e.message}`)
597
+ }
598
+
599
+ return R
600
+ }
601
+
602
+ /**
603
+ * 处理分离的 wia 包,共享缓存的wia已被排除
604
+ * @param {*} wia wia模块
605
+ * @returns {Promise<string>}
606
+ */
607
+ async procWia(wia) {
608
+ let R = ''
609
+ const _ = this
610
+ const {opt} = _
611
+ const {owner, name, dir, out} = opt
612
+
613
+ try {
614
+ if (!wia?.M.length) return
615
+
616
+ console.log(`${owner}/${name} procWia(M:${wia.M.length}) ...`)
617
+
618
+ // 压缩代码
619
+ if (opt.press) {
620
+ for (const k of Object.keys(wia)) {
621
+ if (k !== 'R' && k !== 'M') wia[k] = await pressCode(wia[k])
622
+ }
623
+ }
624
+
625
+ const f = path.resolve(`${dir}/${out}`, './wia.js')
626
+
627
+ // 添加自动合并模块 代码
628
+ // const wf = `(function(){var js=${JSON.stringify(wia)};\r\n`
629
+ // + '$._m.add(js);})();\r\n';
630
+ const wf = JSON.stringify(wia)
631
+
632
+ // console.log('Module:%s%s', JSON.stringify(p), '\r\n');
633
+ await fs.ensureDir(path.dirname(f))
634
+ fs.writeFile(f, wf)
635
+ R = f
636
+ } catch (e) {
637
+ console.log(`procWia exp:${e.message}`)
638
+ }
639
+
640
+ return R
641
+ }
642
+
643
+ /**
644
+ * page.js 未修改,则恢复原来的版本,无需更新!
645
+ * @param {*} pfile
646
+ * @param {*} lastP
647
+ */
648
+ async loadPageVer(pfile, lastP) {
649
+ if (ld.isEmpty(lastP)) {
650
+ if (fs.existsSync(pfile)) {
651
+ const tx = await fs.readFile(pfile, 'utf8')
652
+ lastP = JSON.parse(tx)
653
+ }
654
+ }
655
+
656
+ const appf = pfile.replace(/page\.js$/i, 'app.js')
657
+ let tx = await fs.readFile(appf, 'utf8')
658
+ tx = tx.replace(`/mall/page.js?v=${this.ver}`, `/mall/page.js?v=${lastP.R.ver}`)
659
+ await fs.writeFile(appf, tx, e => e && console.log(`save app.js ${pfile} exp:${e.message}`))
660
+ }
661
+
662
+ /**
663
+ * 通过AST解析webpack开发模式打包文件中的__webpack_modules__,获取模块,返回数组
664
+ * webpack/rspack 打包输出参数:mode: 'development', devtool: 'eval', 代码用字符串封装,ast 解析快,并且 入口文件也封装了
665
+ * 生产模式,不同文件压缩后函数名称不统一
666
+ * swc ast 让人迷惑的事情:
667
+ * 1. 代码中有中文注释,按swc ast 解析位置获取代码,多获取中文字数的双倍
668
+ * 2. 回车换行符,是一个字符,在文本编辑器中是两个字符,swc ast,好像是按两个字符计算,
669
+ * 但是 使用 slice 获取的 字符串又是对的
670
+ * 3. 注意,swc 是从1 开始计数,不是0!
671
+ * 原因:
672
+ * SWC 的 span 是基于 UTF-8 字节偏移量
673
+ * 中文字符在 UTF-8 编码中占用 2~3 个字节。
674
+ * 例如,"中文" 在字符串中占用 2 个字符,但在 UTF-8 中占用 6 个字节。
675
+ * 因此,使用buffer字节数组获取代码片段,转换为字符串,buffer 比字符串快,swc内部使用的buffer
676
+ * @param {string} f - 注意是utf8字节数组,不是字符串
677
+ * @returns {Promise<{name:string, org: string, code:string}[]>}
678
+ */
679
+ async getModule(f) {
680
+ /** @type {{name:string, org: string, code:string}[]} */
681
+ let R // 模块列表
682
+ const _ = this
683
+ const {opt} = _
684
+
685
+ try {
686
+ const buf = await fs.readFile(f)
687
+
688
+ console.log('getModule read file:%s len:%d', f, buf.length)
689
+ const ast = await parseSync(buf.toString(), {
690
+ syntax: 'ecmascript',
691
+ })
692
+
693
+ // const compile = new Compiler()
694
+ // const ast = compile.parseSync(
695
+ // buf.toString(),
696
+ // {
697
+ // syntax: 'ecmascript', // "ecmascript" | "typescript"
698
+ // // comments: false,
699
+ // // script: true, // parsed as a script instead of module.
700
+ // // isModule: true, // 强制重新初始化
701
+ // // sourceFileName: f, // 不同的源文件名,Span 独立
702
+ // // sourceFileName: undefined, // 让 span 从 0 开始
703
+ // },
704
+ // f
705
+ // )
706
+ let offset = ast.span.start
707
+
708
+ // console.log(util.inspect(ast, true, 5, true), '【ast】')
709
+
710
+ // getEntry(fn, ast) // devtool: 'eval' 入口函数已打包,直接获取
711
+
712
+ let ms // = await ps
713
+ // 从根节点 查找 __webpack_modules__
714
+ // let ps = new Promise((res, rej) => {
715
+ walkNode(ast, (n, parent) => {
716
+ if (n.type === 'VariableDeclarator' && n.id?.value === '__webpack_modules__') {
717
+ console.log('find __webpack_modules__')
718
+ ms = n
719
+ return true // 找到终止遍历
720
+ }
721
+ })
722
+ // })
723
+
724
+ // console.log(util.inspect(ms, true, 5, true), '【ms】')
725
+
726
+ // 从根节点开始检查每个代码节点
727
+ // ps = new Promise((res, rej) => {
728
+ // let rs
729
+ walkNode(ms, (n, parent) => {
730
+ // require 我们认为是一个函数调用,并且函数名为 require,参数只有一个,且必须是字面量
731
+ /*
732
+ {"./src/index.js":(function (module, __webpack_exports__, __webpack_require__) {
733
+ eval("");
734
+ })
735
+ // no static exports found
736
+ (function(module, exports) {
737
+ eval()
738
+ })
739
+ */
740
+ // console.log({type: n.type})
741
+ // 根据模块特征,筛选模块节点
742
+ if (n.type === 'ObjectExpression' && Array.isArray(n.properties)) {
743
+ const {properties: props} = n
744
+ const [prop] = props
745
+
746
+ console.log({
747
+ len: n.properties?.length,
748
+ ktype: prop.key?.type,
749
+ kvalue: prop.key.value,
750
+ })
751
+
752
+ if (
753
+ prop?.type === 'KeyValueProperty' &&
754
+ prop.key?.type === 'StringLiteral' && // 属性名称为字面量
755
+ /\.js$|.mjs$|.cjs$|.ts$|\.less$|\.css$|\.html$|\.json$|\.css\$$/.test(prop.key.value) // 属性名称包含 .js .less
756
+ ) {
757
+ for (const prop of props) {
758
+ // console.log(util.inspect(prop, true, 20, true))
759
+ let name = prop.key.value
760
+ const {value: v} = prop
761
+ const start = v.span.start - offset // - _astSpan - 1
762
+ const end = v.span.end - offset // _astSpan - 1
763
+ // 获取依赖的相关信息
764
+ // console.log({name, span: v.span}, 'getModule-0')
765
+ let code = buf.subarray(start, end).toString()
766
+ // code = code.replaceAll('__webpack_require__', '__m__') // 全局替换
767
+
768
+ let pos = code.indexOf('eval("')
769
+ if (pos !== -1) {
770
+ let head = code.slice(0, pos)
771
+ let body = code.slice(
772
+ code.indexOf('eval("') + 6,
773
+ code.lastIndexOf('//# sourceURL=webpack:') // devtool: eval 才有
774
+ // code.lastIndexOf('")' + 1)) // eval的结尾
775
+ )
776
+
777
+ // 将eval中的代码字符串做了转义,需将转义字符还原成真实字符,否则代码压缩报错
778
+ body = JSON.parse(`{"m":"${body}"}`).m
779
+ if (head.startsWith('(')) head = head.slice(1) // 去掉(),带()压缩无效
780
+ code = `${head}${body}}`
781
+ }
782
+
783
+ console.log(
784
+ {name, offset, span: v.span, start: code.slice(0, 50), tail: code.slice(-50)},
785
+ 'getModule'
786
+ )
787
+
788
+ // 路径替换,以最后一个 node_modules 路径为准
789
+ pos = name.lastIndexOf('/node_modules/')
790
+ const org = name
791
+ if (pos !== -1) {
792
+ // ../../node_modules/.pnpm/@wiajs+core@1.1.28/node_modules/@wiajs/core/dist/core.mjs
793
+ /** @type {string} */
794
+ const pre = name.slice(0, pos)
795
+ if (name.endsWith('.less') && pre.includes('/node_modules/less-loader/'))
796
+ name = `~~/${name.slice(pos + 14)}`
797
+ else name = `~/${name.slice(pos + 14)}`
798
+ } else if (/^\.\/src\//.test(name)) {
799
+ name = name.replace(/^\.\/src\//, `${opt.owner}/${opt.name}/`)
800
+ } else if (/^\.\/wia\.config\.js/.test(name)) {
801
+ name = name.replace(
802
+ /^\.\/wia\.config\.js/,
803
+ `${opt.owner}/${opt.name}/wia.config.js`
804
+ )
805
+ }
806
+
807
+ // name = name.replace(/[.]{1,2}\/[./]*\/node_modules\//gi, '~/')
808
+ // // "~/.pnpm/@wiajs+core@1.1.28/node_modules/@wiajs/core/dist/core.mjs"
809
+ // name = name.replace(/~\/\S+\/node_modules\//gi, '~/')
810
+
811
+ // code = code.replace(/[.]{1,2}\/[./]*\/node_modules\//gi, '~/')
812
+ // code = code.replace(/~\/\S+\/node_modules\//gi, '~/')
813
+ if (!R) R = []
814
+ R.push({
815
+ name,
816
+ org, // 原始名称,需全代码替换为name,不在这里替换
817
+ code, // 由于存在很多重复模块,在生成唯一模块时做压缩,不在这里做压缩,
818
+ })
819
+ }
820
+ return true // 终止遍历
821
+ }
822
+ }
823
+ })
824
+
825
+ // console.log({R}, 'getModule')
826
+ } catch (e) {
827
+ console.error(`getModule exp:${e.message}`)
828
+ }
829
+
830
+ return R
831
+ }
832
+ }
833
+
834
+ /**
835
+ * 压缩代码
836
+ * @param {*} src 代码
837
+ * @returns {Promise<string>}
838
+ */
839
+ async function pressCode(src) {
840
+ let R = src
841
+
842
+ try {
843
+ // console.log({src}, '【src】')
844
+
845
+ // swc
846
+ const opts = {
847
+ compress: {
848
+ // booleans: true, // 优化布尔表达式
849
+ // conditionals: true, // 优化条件表达式
850
+ drop_console: true, // 删除 `console.log` 等调试信息
851
+ drop_debugger: true, // 删除 debugger
852
+ dead_code: true,
853
+ unused: true, // 删除未使用的变量和函数
854
+ },
855
+ mangle: {
856
+ // reserved: ['__unused_webpack_module', '__webpack_exports__', '__webpack_require__'],
857
+ // reserved: ['__m__'],
858
+ },
859
+ format: {
860
+ comments: false, // 仅保留特定注释,例如 /*! */ false
861
+ },
862
+ ecma: 6, // 5: ES5 6或2015: ES6 specify one of: 5, 2015, 2016, etc.
863
+ module: false, // 指定是否将代码视为模块
864
+ safari10: true, // Safari 10 的特定问题,如 for-of 迭代器兼容性
865
+ // toplevel: true, // 缺省 false,是否优化顶层作用域的变量和函数
866
+ sourceMap: false, // 为压缩后的代码生成 source map 文件,便于调试
867
+ // outputPath: undefined, // 指定压缩后文件的输出路径
868
+ // inlineSourcesContent: false, // 将源代码内容内联到生成的 source map 中
869
+ }
870
+
871
+ // terser
872
+ const opts2 = {
873
+ ecma: 6, // specify one of: 5, 2015, 2016, etc.
874
+ enclose: false, // or specify true, or "args:values"
875
+ keep_classnames: false,
876
+ keep_fnames: false,
877
+ ie8: false,
878
+ module: false,
879
+ nameCache: null, // or specify a name cache object
880
+ safari10: false,
881
+ toplevel: false,
882
+ mangle: {
883
+ toplevel: false, // 防止顶层作用域的变量和参数被混淆或重命名。
884
+ },
885
+ }
886
+
887
+ // const rs = await mini(src.replace('function (', 'function __xxx__('), opts2)
888
+ const rs = await minify(src.replace('function (', 'function __xxx__('), opts) // 没有函数名称无法压缩
889
+ // console.log({src, rs})
890
+ if (rs?.code) R = `${rs.code.replace('function __xxx__(', 'function (')}`
891
+ } catch (e) {
892
+ console.log(`pressCode exp:${e.message}`)
893
+ }
894
+
895
+ return R
896
+ }
897
+
898
+ /**
899
+ * 返回入口函数,devtool: false 时,入口函数未封装,ast 会很大,不用
900
+ * @param {string} fn
901
+ * @param {*} ast
902
+ * @returns {string}
903
+ */
904
+ function getEntry(fn, ast) {
905
+ let R
906
+
907
+ try {
908
+ let entry
909
+ // 从根节点 查找 __webpack_modules__
910
+ walkNode(ast, (n, parent) => {
911
+ if (n.type === 'VariableDeclarator' && n.id?.value === '__webpack_exports__') {
912
+ entry = n
913
+ return true // 找到终止遍历
914
+ }
915
+ })
916
+
917
+ entry = buf.subarray(entry.span.end).toString()
918
+ entry = entry.slice(0, entry.lastIndexOf('})()')).replace(/..\/[./]*\/node_modules\//gi, '~/')
919
+ // .replace(/__webpack_require__/gi, '__m__') // 全局替换
920
+
921
+ // This entry need to be wrapped in an IIFE because it need to be in strict mode.
922
+ const IIFE = entry.includes('wrapped in an IIFE')
923
+ if (IIFE) entry = entry.slice(entry.indexOf('(() => {') + 8, entry.lastIndexOf('})()'))
924
+
925
+ entry = `function (__unused_webpack_module, __webpack_exports__, __webpack_require__) {${entry}}`
926
+
927
+ console.log({fn, start: entry.slice(0, 80), tail: entry.slice(-80)}, 'getEntry')
928
+ R = entry
929
+ } catch (e) {
930
+ console.error(`getEntry exp:${e.message}`)
931
+ }
932
+
933
+ return R
934
+ }
935
+
936
+ /**
937
+ * 迭代遍历所有节点(包括子节点),回调函数返回 true,则终止剩余节点遍历
938
+ * @param {*} node
939
+ * @param {(node: *, parent: *) => boolean} cb
940
+ * @param {*} parent
941
+ * @returns {boolean}
942
+ */
943
+ function walkNode(node, cb, parent) {
944
+ if (!node || typeof node !== 'object') return
945
+
946
+ // 调用回调函数处理当前节点和父节点
947
+ if (cb(node, parent) === true) return true
948
+
949
+ // 有 type 字段的我们认为是一个节点
950
+ for (const k of Object.keys(node)) {
951
+ const item = node[k]
952
+ if (Array.isArray(item)) {
953
+ // 遍历数组中的每个子节点,返回成功则停止遍历剩余节点
954
+ for (const v of item) {
955
+ if (walkNode(v, cb, node)) return true
956
+ }
957
+ // } else if (item?.type) walkNode(item, cb, node) // 遍历单个子节点
958
+ } else if (item && typeof item === 'object') {
959
+ // 遍历单个子节点,返回成功则停止遍历剩余节点
960
+ if (walkNode(item, cb, node)) return true
961
+ }
962
+ }
963
+ }
964
+
965
+ function sortObj(obj) {
966
+ const R = {}
967
+ const ks = ld.sortBy(ld.keys(obj))
968
+
969
+ ld.forIn(ks, k => {
970
+ R[k] = obj[k]
971
+ })
972
+
973
+ return R
974
+ }
975
+
976
+ // test
977
+ // const pack = new Pack();
978
+ // pack.work({login: './src/mall/page/login.js'});