@wiajs/core 1.1.29 → 1.1.31

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