.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Wedecode
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | **微信 `wxapkg` 反编译工具,已经支持大多数小程序完美还原**
17 |
18 | **`Windows` `MacOS` `Linux` 跨平台支持**
19 |
20 | ### 支持功能
21 |
22 | SUPPORT
23 |
24 | - [x] **支持 `小程序` 还原**
25 | - [x] **支持 `小游戏` 还原**
26 | - [x] **支持分包代码和插件代码还原**
27 | - [x] **完美还原目录结构和源代码**
28 | - [x] **`JS` 代码还原**
29 | - [x] **`WXML` 代码还原**
30 | - [x] **`WXSS` 代码还原**
31 | - [x] **`WXS` 代码还原**
32 | - [x] **`JSON` 文件还原**
33 | - [x] **其他类型文件还原 ( 媒体资源,wasm, workers...等 )**
34 | - [x] **所有代码美化输出**
35 | - [x] **小程序包扫描**
36 |
37 | TODO
38 |
39 | - [ ] 小程序自动解密( 最近几年的电脑端包都不需要解密,以后看情况跟进 )
40 |
41 | ### 准备
42 |
43 | 该工具只能在有 `nodejs` 环境设备上运行, 如果您还没有 `nodejs` 环境,点这里 [去准备环境](https://nodejs.org/)
44 |
45 | ### 安装工具
46 |
47 | 全局安装, 安装完成后在任意终端都可使用
48 |
49 | ```shell
50 | # window
51 | npm i wedecode -g
52 | # mac
53 | sudo npm i wedecode -g
54 | ```
55 |
56 | ### 运行
57 |
58 | 命令行直接输入 wedecode 即可运行, 全程自动引导
59 |
60 | ```shell
61 | wedecode
62 | ```
63 |
64 | 命令行直接指定参数
65 |
66 | ```shell
67 | # 手动指定一个包
68 | wedecode ./name.wxapkg
69 | # 或者 编译当前命令行所在文件夹内的所有包
70 | wedecode ./
71 | # 或者 编译当前命令行所在文件夹下名为 dirname 文件夹的所有包
72 | wedecode ./dirname
73 | # 或者: 将编译结果输出到指定目录 --out 为输出目录
74 | wedecode ./ --out ./output_path
75 | # 你也可以预设任意命令行参数, 在交互时将不会向您提问, 例如
76 | wedecode --out output_path --clear --open-dir
77 | ```
78 |
79 | 使用源码运行
80 |
81 | ```shell
82 | git clone https://github.com/biggerstar/wedecode
83 | npm install # 如果 npm 安装很慢, 可以使用右侧命令换国内的淘宝源 npm config set registry https://registry.npmmirror.com
84 | npm run start
85 | ```
86 |
87 | ### 命令参数
88 |
89 | | 参数 | 作用 |
90 | |---------------------|----------------------------|
91 | | `` | 包所在路径,可以是文件或者目录 |
92 | | `-o, --out ` | 产物及输出路径, 未指定默认放到同级目录下的 OUTPUT |
93 | | `--open-dir` | 结束编译后打开查看产物目录 |
94 | | `--clear` | 是否清空旧产物 |
95 | | `--px` | 是否使用 px 像素单位解析 css, 默认使用的是 rpx 单位 |
96 | | `--unpack-only` | 是否只进行解包,不进行反编译 |
97 |
98 | ### polyfill
99 |
100 | 在编译过程中, 在包所在文件夹在创建一个 polyfill 目录,如果发现里面的模块和输出到产物中的模块路径名称一致,
101 | 将会使用自定义的js模块,忽略原本js模块的编译
102 |
103 | ```
104 | 小程序包所在位置目录结构
105 |
106 | ├── target_dir
107 | │ ├── xxx.wxapkg
108 | │ ├── xxx-sub.wxapkg
109 | │ └── polyfill/
110 | │ └── @babel/
111 | │ └── array.js
112 |
113 | ```
114 |
115 | ```
116 | 输出产物目录结构
117 |
118 | ├── OUTPUT
119 | │ ├─ app.json
120 | │ ├─ pages/
121 | │ ├─ components/
122 | │ ├─ @babel/
123 | │ └── array.js
124 | ```
125 |
126 | ### QA
127 |
128 | 1. Q: 为何编译出来好多文件只有默认模板?
129 | A: 这可能是缺失分包,你需要把分包放在一起编译, 你可以在 app.config.json 或者 app.json 文件中查看你依赖的分包信息,
130 | 在编译产物中出现默认模板是因为小程序会检查依赖,为了保证在缺失某些分包的情况下正常运行而生成的默认模板
131 |
132 | ### 免责声明
133 |
134 | 该工具仅限用于: 线上代码安全审计以便快速发现漏洞, 学习反编译原理,
135 | 请遵守国家法律, 严禁任何非法用途,
136 | 若你使用的范围不在国家法律允许的范围内, 造成的一切法律后果与作者无关。
137 |
--------------------------------------------------------------------------------
/decryption-tool/UnpackMiniApp.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biggerstar/wedecode/06f10b2fb8a529d4fb5ce3ad7dca2a46d6fce6df/decryption-tool/UnpackMiniApp.exe
--------------------------------------------------------------------------------
/decryption-tool/wxpack/这个是解密后的输出文件夹.txt:
--------------------------------------------------------------------------------
1 | This is the decrypted output folder
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wedecode",
3 | "version": "0.8.0-beta.3",
4 | "type": "module",
5 | "description": "微信小程序源代码还原工具, 线上代码安全审计",
6 | "bin": {
7 | "wedecode": "./dist/wedecode.js"
8 | },
9 | "scripts": {
10 | "bootstrap": "pnpm install",
11 | "start": "vite build && node dist/wedecode.js",
12 | "dev": "vite build --watch",
13 | "build": "vite build",
14 | "test:cmd": "wedecode",
15 | "test:cmd:dev": "DEV=true wedecode",
16 | "test:cmd:args": "DEV=true wedecode -o OUTPUT",
17 | "link": "vite build && pnpm link --dir= ./",
18 | "unlink": "pnpm unlink",
19 | "release:npm": "vite build && npm publish",
20 | "release:git": "vite build && git commit -am v$npm_package_version && git tag $npm_package_version && git push --tags ",
21 | "dev:unpack:dir": "DEV=true wedecode --clear -o OUTPUT pkg/fen-cao",
22 | "dev:unpack:subPack": "DEV=true wedecode --clear -o OUTPUT pkg/mt/_mobike_.wxapkg",
23 | "dev:unpack:game-dir": "DEV=true wedecode --clear -o OUTPUT-GAME pkg/weixin-dushu",
24 | "preview:unpack": "wedecode -ow true -o OUTPUT pkg/issues8-sxd"
25 | },
26 | "repository": {
27 | "type": "git",
28 | "url": "git+https://github.com/biggerstar/wedecode.git"
29 | },
30 | "license": "GPL-3.0-or-later",
31 | "bugs": {
32 | "url": "https://github.com/biggerstar/wedecode/issues"
33 | },
34 | "files": [
35 | "dist",
36 | "decryption-tool"
37 | ],
38 | "homepage": "https://github.com/biggerstar/wedecode#readme",
39 | "dependencies": {
40 | "@biggerstar/deepmerge": "^1.0.3",
41 | "@biggerstar/inquirer-selectable-table": "^1.0.12",
42 | "axios": "^1.7.4",
43 | "cheerio": "1.0.0-rc.12",
44 | "commander": "^12.1.0",
45 | "cssbeautify": "^0.3.1",
46 | "escodegen": "^1.14.3",
47 | "esprima": "^4.0.1",
48 | "figlet": "^1.7.0",
49 | "glob": "^10.4.1",
50 | "inquirer": "^9.2.23",
51 | "js-beautify": "^1.15.1",
52 | "jsdom": "^24.1.0",
53 | "open-file-explorer": "^1.0.2",
54 | "picocolors": "^1.0.1",
55 | "single-line-log": "^1.1.2",
56 | "update-check": "^1.5.4",
57 | "vm2": "^3.9.19"
58 | },
59 | "devDependencies": {
60 | "@types/cssbeautify": "^0.3.5",
61 | "@types/escodegen": "^0.0.10",
62 | "@types/esprima": "^4.0.6",
63 | "@types/figlet": "^1.5.8",
64 | "@types/inquirer": "^9.0.7",
65 | "@types/js-beautify": "^1.14.3",
66 | "@types/jsdom": "^21.1.7",
67 | "@types/node": "^20.14.2",
68 | "@types/open-file-explorer": "^1.0.2",
69 | "@types/single-line-log": "^1.1.2",
70 | "lerna": "^8.1.7",
71 | "rollup-plugin-copy": "^3.5.0",
72 | "vite": "^5.2.13"
73 | },
74 | "keywords": [
75 | "wxapkg",
76 | "Decompilation",
77 | "小程序",
78 | "反编译",
79 | "审计",
80 | "安全"
81 | ]
82 | }
83 |
--------------------------------------------------------------------------------
/src/bin/wedecode/common.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 | import {isWxAppid, printLog, sleep} from "@/utils/common";
3 | import {glob} from "glob";
4 | import colors from "picocolors";
5 | import path from "node:path";
6 | import pkg from "../../../package.json";
7 | import checkForUpdate from "update-check";
8 | import figlet from "figlet";
9 | import {CacheClearEnum} from "@/bin/wedecode/enum";
10 | import prompts from "@/bin/wedecode/inquirer";
11 | import process from "node:process";
12 | import axios from "axios";
13 |
14 |
15 | /**
16 | * 查询是否有新版本
17 | * */
18 | export function createNewVersionUpdateNotice(): {
19 | query(): void
20 | notice(): Promise
21 | } {
22 | let updateInfo: Record | null
23 | return {
24 | /** 进行查询 */
25 | query() {
26 | checkForUpdate(pkg).then(res => updateInfo = res).catch(() => void 0)
27 | },
28 | /**
29 | * 异步使用, 时间错开,因为查询需要时间, 如果查询到新版本, 则进行通知
30 | * 基于 update-check 如果本次查到更新但是没通知, 下次启动将会从缓存中获取版本信息并通知
31 | * */
32 | async notice() {
33 | await sleep(200)
34 | if (updateInfo && updateInfo.latest) {
35 | printLog(`
36 | 🎉 wedecode 有新版本: v${pkg.version} --> v${updateInfo.latest}
37 | 🎄 您可以直接使用 ${colors.blue(`npm i -g wedecode@${updateInfo.latest}`)} 进行更新
38 | 💬 npm地址: https://www.npmjs.com/package/wedecode
39 | \n`, {
40 | isStart: true,
41 | })
42 | } else {
43 | printLog(`
44 | 🎄 当前使用版本: v${pkg.version}
45 | \n`, {
46 | isStart: true,
47 | })
48 | }
49 | }
50 | }
51 | }
52 |
53 | /**
54 | * 创建 slogan 大字横幅
55 | * */
56 | export function createSlogan(str: string = ' wedecode'): string {
57 | const slogan = figlet.textSync(str, {
58 | horizontalLayout: "default",
59 | verticalLayout: "default",
60 | whitespaceBreak: true,
61 | })
62 | return colors.bold(colors.yellow(slogan))
63 | }
64 |
65 | /**
66 | * 询问是否清空旧产物
67 | * @param {Boolean} isClear 外部指定是否进行清空
68 | * @param inputPath
69 | * @param outputPath
70 | * */
71 | export async function startCacheQuestionProcess(isClear: boolean, inputPath: string, outputPath: string): Promise {
72 | const OUTPUT_PATH = path.resolve(outputPath)
73 | if (fs.existsSync(OUTPUT_PATH)) {
74 | const isClearCache = isClear ? CacheClearEnum.clear : (await prompts.isClearOldCache(OUTPUT_PATH))['isClearCache']
75 | if (isClearCache === CacheClearEnum.clear || isClear) {
76 | fs.rmSync(OUTPUT_PATH, {recursive: true})
77 | printLog(`\n \u25B6 移除旧产物成功 `)
78 | }
79 | }
80 | }
81 |
82 | export function checkExistsWithFilePath(targetPath: string, opt: {
83 | throw?: boolean,
84 | checkWxapkg?: boolean,
85 | showInputPathLog?: boolean
86 | }): boolean {
87 | const {throw: isThrow = true, checkWxapkg = true, showInputPathLog = true} = opt || {}
88 | const printErr = (log: string) => {
89 | if (showInputPathLog) {
90 | console.log('\n输入路径: ', colors.yellow(path.resolve(targetPath)));
91 | }
92 | isThrow && console.log(`${colors.red(`\u274C ${log}`)}\n`)
93 | }
94 | if (!fs.existsSync(targetPath)) {
95 | printErr('文件 或者 目录不存在, 请检查!')
96 | return false
97 | }
98 | if (checkWxapkg) {
99 | const isDirectory = fs.statSync(targetPath).isDirectory()
100 | if (isDirectory) {
101 | const wxapkgPathList = glob.globSync(`${targetPath}/*.wxapkg`)
102 | if (!wxapkgPathList.length) {
103 | console.log(
104 | '\n',
105 | colors.red('\u274C 文件夹下不存在 .wxapkg 包'),
106 | colors.yellow(path.resolve(targetPath)),
107 | '\n')
108 | return false
109 | }
110 | }
111 | }
112 | return true
113 | }
114 |
115 | export function stopCommander() {
116 | console.log(colors.red('\u274C 操作已主动终止!'))
117 | process.exit(0)
118 | }
119 |
120 | /**
121 | * 获取 win mac linux 路径分割列表
122 | * */
123 | export function getPathSplitList(_path: string) {
124 | let delimiter = '\\'
125 | let partList: string[]
126 | partList = _path.split('\\') // win
127 | if (partList.length <= 1) {
128 | delimiter = '/'
129 | partList = _path.split('/') // win 第二种路径或者 unix 路径
130 | }
131 | return {
132 | partList,
133 | delimiter
134 | }
135 | }
136 |
137 | export function findWxAppIdPath(_path: string) {
138 | const {partList, delimiter} = getPathSplitList(_path)
139 | let newPathList = [...partList]
140 | for (const index in partList.reverse()) {
141 | const dirName = partList[index]
142 | if (isWxAppid(dirName)) {
143 | break
144 | }
145 | newPathList.pop()
146 | }
147 | return newPathList.join(delimiter).trim()
148 | }
149 |
150 | export async function internetAvailable() {
151 | return axios
152 | .request({
153 | url: 'https://bing.com',
154 | maxRedirects: 0,
155 | timeout: 2000,
156 | validateStatus: ()=> true
157 | })
158 | .then(()=> true)
159 | .catch(()=> Promise.resolve(false))
160 | }
161 |
--------------------------------------------------------------------------------
/src/bin/wedecode/enum.ts:
--------------------------------------------------------------------------------
1 | import {PUBLIC_OUTPUT_PATH} from "@/constant";
2 | import process from "node:process";
3 |
4 | export const globPathList: string[] = [ // 末尾不要带 * 号
5 | /* macGlob */
6 | // 版本3+
7 | '/Users/*/Library/Containers/*/Data/.wxapplet/packages',
8 | // 版本4.0+
9 | '/Users/*/Library/Containers/*/Data/Documents/app_data/radium/Applet/packages',
10 |
11 | /* winGlob */
12 | 'C:\\Users\\weixin\\WeChat Files\\',
13 | 'D:\\Users\\weixin\\WeChat Files\\',
14 | 'E:\\Users\\weixin\\WeChat Files\\',
15 | 'F:\\Users\\weixin\\WeChat Files\\',
16 | 'C:\\Users\\*\\Documents\\WeChat Files\\Applet',
17 | 'D:\\Users\\*\\Documents\\WeChat Files\\Applet',
18 | 'E:\\Users\\*\\Documents\\WeChat Files\\Applet',
19 | 'F:\\Users\\*\\Documents\\WeChat Files\\Applet',
20 |
21 | /* linuxGlob */
22 | '/home/*/.config/WeChat/Applet'
23 | ]
24 |
25 |
26 | /**
27 | * 主包文件名特征
28 | * */
29 | export const AppMainPackageNames: string[] = ['__APP__.wxapkg', 'app.wxapkg']
30 |
31 | export enum CacheClearEnum {
32 | clear = '清空',
33 | notClear = '不清空',
34 | }
35 |
36 | export enum OperationModeEnum {
37 | autoScan = '\u25B6 自动扫描小程序包',
38 | manualScan = '\u25B6 手动设定扫描目录',
39 | manualDir = '\u25B6 直接指定包路径( 非扫描 )',
40 | }
41 |
42 | export enum StreamPathDefaultEnum {
43 | inputPath = './',
44 | publicOutputPath = PUBLIC_OUTPUT_PATH,
45 | defaultOutputPath = 'default',
46 | }
47 |
48 | export enum YesOrNoEnum {
49 | yes = '是',
50 | no = '否',
51 | }
52 |
53 | export const isDev = process.env.DEV === 'true'
54 |
--------------------------------------------------------------------------------
/src/bin/wedecode/inquirer.ts:
--------------------------------------------------------------------------------
1 | import inquirer from "inquirer";
2 | import path from "node:path";
3 | import colors from "picocolors";
4 | import {checkExistsWithFilePath, internetAvailable} from "@/bin/wedecode/common";
5 | import {PUBLIC_OUTPUT_PATH} from "@/constant";
6 | import {CacheClearEnum, YesOrNoEnum, OperationModeEnum} from "@/bin/wedecode/enum";
7 | // @ts-ignore
8 | import {SelectTableTablePrompt} from "@biggerstar/inquirer-selectable-table";
9 | import {clearScreen, sleep} from "@/utils/common";
10 | import process from "node:process";
11 | import {ScanTableOptions} from "@/type";
12 |
13 | inquirer.registerPrompt("table", SelectTableTablePrompt);
14 | process.stdout.setMaxListeners(200)
15 |
16 | async function onResize() {
17 | if (lastTableOptions) {
18 | await prompts.showScanPackTable(lastTableOptions)
19 | clearScreen()
20 | }
21 | }
22 |
23 | let lastTableOptions: ScanTableOptions = null
24 | let online: boolean = false
25 |
26 | async function checkOnline() {
27 | online = await internetAvailable()
28 | }
29 |
30 | setTimeout(checkOnline, 0)
31 |
32 | const prompts = {
33 | async selectMode() {
34 | const offlineTip: string = `( ${colors.yellow('联网可显示小程序信息')} )`
35 | const onlineTip: string = `( ${colors.green('网络正常')} )`
36 | await sleep(1000)
37 | return inquirer['prompt'](
38 | [
39 | {
40 | type: 'list',
41 | message: `请选择操作模式 ? ${!online ? offlineTip : onlineTip}`,
42 | name: 'selectMode',
43 | choices: [
44 | OperationModeEnum.autoScan,
45 | OperationModeEnum.manualScan,
46 | OperationModeEnum.manualDir,
47 | ],
48 | },
49 | ]
50 | )
51 | },
52 | async inputManualScanPath() {
53 | return inquirer['prompt'](
54 | [
55 | {
56 | type: 'input',
57 | message: `输入您要扫描的小程序包路径 ( ${colors.yellow('.')} 表示使用当前路径 ): `,
58 | name: 'manualScanPath',
59 | validate(input: any) {
60 | if (!input) return false
61 | return checkExistsWithFilePath(input, {throw: true, checkWxapkg: false, showInputPathLog: false});
62 | }
63 | },
64 | ]
65 | )
66 | },
67 | async showDangerScanPrompt(_path: string) {
68 | return inquirer['prompt'](
69 | [
70 | {
71 | type: 'list',
72 | message: `您指定的路径可能会花大量时间扫描文件系统, 确定继续 ? ${colors.yellow(_path)}`,
73 | name: 'dangerScan',
74 | choices: [
75 | YesOrNoEnum.no,
76 | YesOrNoEnum.yes,
77 | ],
78 | default: YesOrNoEnum.no,
79 | },
80 | ]
81 | )
82 | },
83 | async showScanPackTable(opt: ScanTableOptions) {
84 | lastTableOptions = opt
85 | if (!onResize['onResize']) {
86 | process.stdout.on('resize', onResize)
87 | onResize['onResize'] = true
88 | }
89 | await sleep(50)
90 | clearScreen()
91 | const part = process.stdout.columns / 10
92 | const result = await inquirer['prompt'](
93 | [
94 | {
95 | type: "table",
96 | name: "packInfo",
97 | message: "",
98 | pageSize: 6,
99 | showIndex: true,
100 | tableOptions: {
101 | // wordWrap: true,
102 | wrapOnWordBoundary: true,
103 | colWidths: [part / 2, part * 2, part * 2, part * 5.3].map(n => Math.floor(n))
104 | },
105 | columns: opt.columns || [],
106 | rows: opt.rows || []
107 | }
108 | ]
109 | )
110 | onResize['onResize'] = false
111 | process.stdout.off('resize', onResize)
112 | return result
113 | },
114 | async questionInputPath() {
115 | return inquirer['prompt'](
116 | [
117 | {
118 | type: 'input',
119 | message: `输入 ${colors.blue('wxapkg文件')} 或 ${colors.blue('目录')} 默认为( ${colors.yellow('./')} ): `,
120 | name: 'inputPath',
121 | validate(input: any, _): any {
122 | return checkExistsWithFilePath(path.resolve(input), {throw: true});
123 | },
124 | },
125 | ]
126 | )
127 | },
128 | async questionOutputPath() {
129 | return inquirer['prompt'](
130 | [
131 | {
132 | type: 'input',
133 | message: `输出目录, 默认为当前所在目录的( ${colors.yellow(PUBLIC_OUTPUT_PATH)} ): `,
134 | name: 'outputPath',
135 | },
136 | ]
137 | )
138 | },
139 | async isClearOldCache(cachePath = '') {
140 | return inquirer['prompt'](
141 | [
142 | {
143 | type: 'list',
144 | message: `输出目录中存在上次旧的编译产物,是否清空 ? \n ${colors.blue(`当前缓存路径( ${colors.yellow(cachePath)} )`)}`,
145 | name: 'isClearCache',
146 | choices: [
147 | CacheClearEnum.clear,
148 | CacheClearEnum.notClear,
149 | ],
150 | },
151 | ]
152 | )
153 | },
154 | async showFileExplorer() {
155 | return inquirer['prompt'](
156 | [
157 | {
158 | type: 'list',
159 | message: `\n 将打开文件管理器, 确定继续 ?`,
160 | name: 'showFileExplorer',
161 | choices: [
162 | YesOrNoEnum.no,
163 | YesOrNoEnum.yes,
164 | ],
165 | default: YesOrNoEnum.no,
166 | },
167 | ]
168 | )
169 | },
170 | }
171 | export default prompts
172 |
--------------------------------------------------------------------------------
/src/bin/wedecode/main-commander-process.ts:
--------------------------------------------------------------------------------
1 | import {ScanPackagesResultInfo} from "@/type";
2 | import {OperationModeEnum, StreamPathDefaultEnum} from "@/bin/wedecode/enum";
3 | import {startSacnPackagesProcess} from "@/bin/wedecode/scan";
4 | import {checkExistsWithFilePath, startCacheQuestionProcess} from "@/bin/wedecode/common";
5 | import path from "node:path";
6 | import openFileExplorer from "open-file-explorer";
7 | import {sleep} from "@/utils/common";
8 | import prompts from "@/bin/wedecode/inquirer";
9 | import {DecompilationController} from "@/decompilation-controller";
10 | import colors from "picocolors";
11 |
12 | /**
13 | * 通过命令行交互获取输入和输出路径
14 | * */
15 | async function setInputAndOutputPath(config: Record, opt: {
16 | hasInputPath: boolean,
17 | hasOutputPath: boolean
18 | }): Promise {
19 | const {hasInputPath = false, hasOutputPath = false} = opt || {}
20 | let packInfo: Partial
21 | if (!hasInputPath) {
22 | const {selectMode} = await prompts.selectMode()
23 | if (selectMode === OperationModeEnum.autoScan) {
24 | packInfo = await startSacnPackagesProcess()
25 | config.inputPath = packInfo.storagePath
26 | } else if (selectMode === OperationModeEnum.manualScan) {
27 | const {manualScanPath} = await prompts.inputManualScanPath()
28 | packInfo = await startSacnPackagesProcess(manualScanPath)
29 | config.inputPath = packInfo.storagePath
30 | } else {
31 | const {inputPath} = await prompts.questionInputPath()
32 | config.inputPath = inputPath || config.inputPath
33 | }
34 | }
35 | if (!hasOutputPath) {
36 | if (packInfo) { // 没手动指定路径并且发现路径中的 appId 存在,则自动指定输出到名为 appName 或 appId 的目录
37 | config.outputPath = packInfo.appName || packInfo.appId
38 | } else {
39 | const {outputPath} = await prompts.questionOutputPath()
40 | config.outputPath = outputPath || StreamPathDefaultEnum.defaultOutputPath
41 | }
42 | config.outputPath = path.resolve(StreamPathDefaultEnum.publicOutputPath, config.outputPath)
43 | }
44 | }
45 |
46 | /**
47 | * 执行主命令行程序流程
48 | * */
49 | export async function startMainCommanderProcess(args: string[], argMap: Record): Promise {
50 | const hasInputPath = !!args[0]
51 | const hasOutputPath = !!argMap.out
52 | const isClear = argMap.clear
53 | const config = {
54 | inputPath: args[0] || StreamPathDefaultEnum.inputPath,
55 | outputPath: argMap.out || StreamPathDefaultEnum.defaultOutputPath
56 | }
57 | await setInputAndOutputPath(config, {hasInputPath, hasOutputPath})
58 | if (!checkExistsWithFilePath(config.inputPath, {throw: true})) return false
59 | // 经过下面转换, 文件输出位置最终都会在改小程序包同级目录下的 __OUTPUT__ 文件夹中输出
60 | await startCacheQuestionProcess(isClear, config.inputPath, config.outputPath)
61 | const decompilationController = new DecompilationController(config.inputPath, config.outputPath)
62 | decompilationController.setState({
63 | usePx: argMap.px || false,
64 | unpackOnly: argMap.unpackOnly || false,
65 | })
66 | await decompilationController.startDecompilerProcess()
67 | if (argMap.openDir) {
68 | console.log('\n \u25B6 打开文件管理器: ', colors.yellow(path.resolve(config.outputPath)))
69 | openFileExplorer(config.outputPath, () => void 0)
70 | }else {
71 | console.log('\n \u25B6 输出路径: ', colors.yellow(path.resolve(config.outputPath)))
72 | }
73 | await sleep(500)
74 | return true
75 | }
76 |
--------------------------------------------------------------------------------
/src/bin/wedecode/scan.ts:
--------------------------------------------------------------------------------
1 | import {glob} from "glob";
2 | import {clearScreen} from "@/utils/common";
3 | import colors from "picocolors";
4 | import {PackageInfoResult, SacnPackagesPathItem, ScanPackagesResultInfo} from "@/type";
5 | import axios, {AxiosRequestConfig} from "axios";
6 | import path from "node:path";
7 | import prompts from "@/bin/wedecode/inquirer";
8 | import {AppMainPackageNames, globPathList, YesOrNoEnum} from "@/bin/wedecode/enum";
9 | import {findWxAppIdPath, getPathSplitList, stopCommander} from "@/bin/wedecode/common";
10 | import fs from "node:fs";
11 |
12 | /**
13 | * 判断是否是个可能扫描大量文件系统的路径
14 | * */
15 | function inDangerScanPathList(_path: string) {
16 | _path = path.resolve(_path)
17 | let partList: string[]
18 | if (_path.includes(':')) _path = _path.split(':')[1] // 去掉盘符
19 | partList = getPathSplitList(_path).partList
20 | return partList.map(s => s.trim()).filter(Boolean).length <= 1
21 | }
22 |
23 | /**
24 | * 通过指定的目录找到该目录下子目录中的所有小程序包
25 | * */
26 | function findWxMiniProgramPackDir(manualScanPath: string) {
27 | const foundPackageList: SacnPackagesPathItem[] = []
28 | glob.globSync(path.resolve(manualScanPath, '**/*.wxapkg'))
29 | .map((_path) => {
30 | const foundMainPackage = AppMainPackageNames.find(fileName => _path.endsWith(fileName))
31 | if (foundMainPackage) return _path
32 | return false
33 | })
34 | .filter(Boolean)
35 | .reduce((pre: string[], cur: string) => {
36 | if (pre.includes(cur)) return pre
37 | pre.push(cur)
38 | return pre
39 | }, [])
40 | .forEach(_path => {
41 | const foundPath = findWxAppIdPath(_path)
42 | const isFoundWxId = !!foundPath
43 | let appIdPath = path.dirname(_path)
44 | const {partList} = getPathSplitList(appIdPath)
45 | let appId = partList.filter(Boolean).pop() // 默认使用所在文件夹名称
46 | if (isFoundWxId) {
47 | appIdPath = foundPath
48 | appId = appIdPath.split('/').pop() // 如果有找到 appId 则使用其作为名称
49 | }
50 | foundPackageList.push({
51 | isAppId: isFoundWxId,
52 | appId: appId,
53 | path: isFoundWxId ? foundPath : appIdPath,
54 | storagePath: path.dirname(_path)
55 | })
56 | })
57 | return foundPackageList
58 | }
59 |
60 | /**
61 | * 扫描小程序包 TODO 做降级方案, 如果扫描不到第一次的包,则扩大扫描范围
62 | * @param manualScanPath 手动输入的 wxapkg 包存放目录,不能是文件
63 | * */
64 | async function sacnPackages(manualScanPath: string = ''): Promise {
65 | const foundPackageList = []
66 | let scanPathList: string[] = globPathList
67 | if (Boolean(manualScanPath.trim())) { // 这里空字符串的话将会使用默认 globPathList 列表去匹配
68 | const absolutePath = path.resolve(manualScanPath)
69 | if (inDangerScanPathList(absolutePath)) {
70 | const {dangerScan} = await prompts.showDangerScanPrompt(absolutePath)
71 | if (dangerScan === YesOrNoEnum.no) {
72 | stopCommander()
73 | }
74 | }
75 | scanPathList = [absolutePath]
76 | }
77 | if (scanPathList.length) {
78 | console.log(' 扫描中...')
79 | }
80 |
81 | scanPathList.forEach(matchPath => {
82 | const foundPList = findWxMiniProgramPackDir(matchPath)
83 | foundPList.forEach(item => foundPackageList.push(item))
84 | })
85 | // console.log(foundPackageList)
86 |
87 | if (foundPackageList.length === 0) {
88 | console.log(`
89 | ${colors.red('未找到小程序包,您需要电脑先访问某个小程序后产生缓存再扫描, 如果还扫描不到请反馈 ')}
90 | 当前所处目录: $ ${colors.yellow(path.resolve(manualScanPath || './'))}
91 |
92 | \u25B6 请注意扫描功能还在测试阶段,如果出现问题请到 github 反馈
93 | \u25B6 提交时请带上您电脑中小程序的 '${colors.bold('微信官方的 wxapkg 包在硬盘中的存放路径')}' 和 '${colors.bold('微信版本号')}'
94 | \u25B6 https://github.com/biggerstar/wedecode/issues
95 | `)
96 | stopCommander()
97 | }
98 | return foundPackageList
99 | }
100 |
101 | /**
102 | * 开始进行扫描小程序包流程
103 | * */
104 | export async function startSacnPackagesProcess(manualScanPath?: string): Promise {
105 | const foundPackageList: SacnPackagesPathItem[] = await sacnPackages(manualScanPath)
106 | // console.log(foundPackageList)
107 | const columns = [
108 | {
109 | name: "名字",
110 | value: "appName"
111 | },
112 | {
113 | name: "修改时间",
114 | value: "updateDate"
115 | },
116 | {
117 | name: "描述",
118 | value: "description"
119 | },
120 | ]
121 | const rowsPromiseList = foundPackageList
122 | .map(async (item: SacnPackagesPathItem) => {
123 | const statInfo = fs.statSync(item.storagePath)
124 | const date = new Date(statInfo.mtime)
125 | const dateString = `${date.getMonth() + 1}/${date.getDate()} ${date.toLocaleTimeString()}`
126 | if (!item.isAppId) return {
127 | appName: item.appId,
128 | updateDate: dateString,
129 | description: item.storagePath
130 | }
131 | const appId = item.appId
132 | const {nickname, description} = await getWxAppInfo(appId);
133 | return {
134 | appName: nickname || appId,
135 | updateDate: dateString,
136 | description: description || '',
137 | };
138 | })
139 | if (rowsPromiseList.length) {
140 | console.log(' 获取小程序信息中...')
141 | }
142 | const rows = await Promise.all(rowsPromiseList)
143 | if (rowsPromiseList.length) {
144 | clearScreen()
145 | console.log('$ 选择一个包进行编译: ')
146 | }
147 | const result = await prompts.showScanPackTable({
148 | columns,
149 | rows
150 | })
151 | const foundIndex = rows
152 | .findIndex(item => item.appName === result.packInfo?.appName)
153 | const packInfo = {...rows[foundIndex], ...foundPackageList[foundIndex]}
154 | console.log(`$ 选择了 ${packInfo.appName}( ${packInfo.appId} )`)
155 |
156 | return packInfo
157 | }
158 |
159 | /**
160 | * 获取 appid 所属主体信息
161 | * */
162 | async function getWxAppInfo(appid: string): Promise> {
163 | const options: AxiosRequestConfig = {
164 | method: 'POST',
165 | timeout: 6000,
166 | url: 'https://kainy.cn/api/weapp/info/',
167 | headers: {'content-type': 'application/json'},
168 | data: {appid: appid}
169 | };
170 | return axios
171 | .request(options)
172 | .then((res) => {
173 | return Promise.resolve(res.data?.data || {});
174 | })
175 | .catch(() => {
176 | return Promise.resolve({});
177 | });
178 | }
179 |
--------------------------------------------------------------------------------
/src/bin/wedecode/wedecode.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import {Command} from "commander";
4 | import pkg from '../../../package.json';
5 | import {clearScreen, printLog, sleep} from "@/utils/common";
6 | import {
7 | createNewVersionUpdateNotice,
8 | createSlogan,
9 | } from "@/bin/wedecode/common";
10 | import {startMainCommanderProcess} from "@/bin/wedecode/main-commander-process";
11 |
12 | const notice = createNewVersionUpdateNotice()
13 | notice.query()
14 |
15 | const program = new Command();
16 |
17 | program
18 | .name('wedecode')
19 | .usage(" [options]")
20 | .description('\u25B6 wxapkg 反编译工具')
21 | .version(pkg.version)
22 | .option("-o, --out ", '指定编译输出地目录, 正常是主包目录')
23 | .option("--open-dir", ' 结束编译后打开查看产物目录')
24 | .option("--clear", '是否清空旧的产物')
25 | .option("--px", '是否使用 px 像素单位解析 css, 默认使用的是 rpx 单位')
26 | .option("--unpack-only", '是否只进行解包,不进行反编译')
27 | .action(async (argMap: Record, options: Record) => {
28 | await sleep(200)
29 | const args = options.args || []
30 | clearScreen()
31 | await sleep(100)
32 | printLog(createSlogan(), {isStart: true});
33 | await notice.notice()
34 | /* ----------------------------------开始交互页面------------------------------------- */
35 | await startMainCommanderProcess(args, argMap)
36 | process.exit(0)
37 | })
38 |
39 | program.parse();
40 |
--------------------------------------------------------------------------------
/src/constant/index.ts:
--------------------------------------------------------------------------------
1 | export const isWindows = /^win/.test(process.platform);
2 | export const isMac = /^darwin/.test(process.platform);
3 |
4 | export const cssBodyToPageReg = /body\s*\{/g
5 |
6 | /**
7 | * 默认输出路径, 基于 inputPath 路径
8 | * */
9 | export const PUBLIC_OUTPUT_PATH = 'OUTPUT'
10 | /**
11 | * 插件目录统一重命名映射
12 | * */
13 | export const pluginDirRename = ['__plugin__', 'plugin_']
14 |
15 | /**
16 | * 清理缓存时移除文件的命中关键词,需要保证唯一特殊性
17 | * */
18 | export const removeAppFileList = [
19 | // 'app-config.json',
20 | 'page-frame.html',
21 | 'app-wxss.js',
22 | 'app-service.js',
23 | 'index.appservice.js',
24 | 'index.webview.js',
25 | 'appservice.app.js',
26 | 'page-frame.js',
27 | 'webview.app.js',
28 | 'common.app.js',
29 | // 'plugin.json',
30 | ]
31 |
32 | export const removeGameFileList = [
33 | // 'app-config.json',
34 | // 'game.js',
35 | 'subContext.js',
36 | 'worker.js',
37 | ]
38 |
39 | export const appJsonExcludeKeys = [
40 | 'navigateToMiniProgramAppIdList',
41 | ]
42 |
43 | export const GameJsonExcludeKeys = [
44 | 'openDataContext',
45 | ]
46 |
--------------------------------------------------------------------------------
/src/decompilation-controller.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 | import { getPathResolveInfo, isPluginPath, printLog, replaceExt, sleep } from "@/utils/common";
3 | import colors from "picocolors";
4 | import { glob } from "glob";
5 | import { AppMainPackageNames, isDev } from "@/bin/wedecode/enum";
6 | import { deepmerge } from "@biggerstar/deepmerge";
7 | import { deleteLocalFile, readLocalFile, readLocalJsonFile, saveLocalFile } from "@/utils/fs-process";
8 | import { removeAppFileList, removeGameFileList } from "@/constant";
9 | import path from "node:path";
10 | import { GameDecompilation } from "@/interface/game-decompilation";
11 | import { AppDecompilation } from "@/interface/app-decompilation";
12 | import { DecompilationControllerState, PackTypeMapping, PathResolveInfo } from "@/type";
13 | import { UnpackWxapkg } from "@/interface/unpack-wxapkg";
14 |
15 |
16 | export class DecompilationController {
17 | public readonly inputPath: string
18 | public readonly outputPath: string
19 | public config: DecompilationControllerState
20 | public readonly pathInfo: PathResolveInfo
21 |
22 | constructor(inputPath: string, outputPath: string) {
23 | if (!inputPath) {
24 | throw new Error('inputPath 是必须的')
25 | }
26 | if (!outputPath) {
27 | throw new Error('outputPath 是必须的')
28 | }
29 | this.inputPath = path.resolve(inputPath)
30 | this.outputPath = path.resolve(outputPath)
31 | this.pathInfo = getPathResolveInfo(this.outputPath)
32 | this.config = {
33 | usePx: false,
34 | unpackOnly: false
35 | }
36 | }
37 |
38 | public setState(opt: Partial) {
39 | Object.assign(this.config, opt)
40 | }
41 |
42 | /**
43 | * 单包反编译
44 | * */
45 | private async singlePackMode(wxapkgPath: string, outputPath: string): Promise {
46 | const packInfo = await UnpackWxapkg.unpackWxapkg(wxapkgPath, outputPath)
47 | if (this.config.unpackOnly) return
48 | if (packInfo.appType === 'game') {
49 | // 小游戏
50 | const decompilationGame = new GameDecompilation(packInfo)
51 | await decompilationGame.decompileAll()
52 | } else {
53 | // 小程序
54 | const decompilationApp = new AppDecompilation(packInfo)
55 | decompilationApp.convertPlugin = true
56 | await decompilationApp.decompileAll({
57 | usePx: this.config.usePx,
58 | })
59 | }
60 | printLog(`\n ✅ ${colors.bold(colors.green(PackTypeMapping[packInfo.packType] + '反编译结束!'))}`, { isEnd: true })
61 |
62 | }
63 |
64 | /**
65 | * 启动反编译流程
66 | * ]*/
67 | public async startDecompilerProcess(): Promise {
68 | const isDirectory = fs.statSync(this.inputPath).isDirectory()
69 | printLog(`\n \u25B6 当前操作类型: ${colors.yellow(isDirectory ? '分包模式' : '单包模式')}`, { isEnd: true })
70 | // await this.startJob()
71 | if (isDirectory) {
72 | const wxapkgPathList = glob.globSync(`${this.inputPath}/*.wxapkg`)
73 | wxapkgPathList.sort((_pathA, _b) => {
74 | const foundMainPackage = AppMainPackageNames.find(fileName => _pathA.endsWith(fileName))
75 | if (foundMainPackage) return -1; // 将 'APP.wxapkg' 排到前面, 保证第一个解析的是主包
76 | return 0;
77 | });
78 | for (const packPath of wxapkgPathList) { // 目录( 多包 )
79 | await this.singlePackMode(packPath, this.outputPath)
80 | }
81 | } else { // 文件 ( 单包 )
82 | await this.singlePackMode(this.inputPath, this.outputPath)
83 | }
84 | await this.endingAllJob()
85 | }
86 |
87 | /**
88 | * 生成小程序的项目配置
89 | * */
90 | protected async generaProjectConfigFiles() {
91 | const projectPrivateConfigJsonPath = path.join(this.outputPath, 'project.private.config.json')
92 | const DEV_defaultConfigData = {
93 | "setting": {
94 | "ignoreDevUnusedFiles": false,
95 | "ignoreUploadUnusedFiles": false,
96 | }
97 | }
98 | const defaultConfigData = {
99 | "setting": {
100 | "es6": false,
101 | "urlCheck": false,
102 | }
103 | }
104 | if (isDev) {
105 | Object.assign(defaultConfigData.setting, DEV_defaultConfigData.setting)
106 | }
107 | let finallyConfig = {}
108 | const projectPrivateConfigString = readLocalFile(projectPrivateConfigJsonPath)
109 | if (projectPrivateConfigString) {
110 | const projectPrivateConfigData = JSON.parse(projectPrivateConfigString)
111 | deepmerge(projectPrivateConfigData, defaultConfigData)
112 | finallyConfig = projectPrivateConfigData
113 | } else {
114 | finallyConfig = defaultConfigData
115 | }
116 | saveLocalFile(projectPrivateConfigJsonPath, JSON.stringify(finallyConfig, null, 2), { force: true })
117 | }
118 |
119 | private async _analyticalCompDependence(analysisList: string[], deps = []): Promise {
120 | const readFilePromises = analysisList
121 | .map((pageDir: string) => {
122 | return new Promise(async (resolve) => {
123 | const jsonPath = path.resolve(this.outputPath, path.dirname(pageDir), `${path.basename(pageDir)}.json`)
124 | let code = ''
125 | try {
126 | code = await fs.promises.readFile(jsonPath, 'utf-8')
127 | } catch (e) {
128 | // no such file or directory
129 | }
130 |
131 | resolve({
132 | pageDir,
133 | jsonPath,
134 | code
135 | })
136 | })
137 | })
138 | const allJsonCodeList = await Promise.all>(readFilePromises)
139 |
140 | const currentCompDep = []
141 | for (const info of allJsonCodeList) {
142 | deps.push(info.pageDir)
143 | try {
144 | const pageJson = JSON.parse(info.code)
145 | const usingComponents = pageJson.usingComponents
146 | if (usingComponents) {
147 | const depCompList: string[] = Object.values(usingComponents)
148 | for (const compUrl of depCompList) {
149 | let depPath = ''
150 | if (compUrl.startsWith('/')) {
151 | depPath = path.resolve(this.outputPath, compUrl.substring(1))
152 | } else {
153 | depPath = path.resolve(path.dirname(info.jsonPath), compUrl)
154 | }
155 | depPath = path.relative(this.outputPath, depPath)
156 | if (!deps.includes(depPath)) {
157 | // console.log('--------------------------------------------------')
158 | // console.log(compUrl)
159 | // console.log(info.pageDir)
160 | // console.log('dep', depPath)
161 | currentCompDep.push(depPath)
162 | deps.push(depPath)
163 | }
164 | }
165 | }
166 | } catch (e) { }
167 |
168 | // 递归处理依赖
169 | }
170 | // console.log("🚀 ~ DecompilationController ~ _analyticalCompDependence ~ currentCompDep:", currentCompDep)
171 | if (currentCompDep.length) {
172 | await this._analyticalCompDependence(currentCompDep, deps)
173 | }
174 | return deps.flat(Infinity).filter(Boolean)
175 | }
176 |
177 | /**
178 | * 生成组件构成必要素的默认 json wxs, wxml, wxss 文件
179 | * */
180 | private async generateDefaultAppFiles() {
181 | const appConfigJson = readLocalJsonFile(path.join(this.outputPath, 'app-config.json'))
182 | const appConfigPages = (appConfigJson?.pages || [])
183 | .map(cPath => cPath.endsWith('/') ? cPath.substring(0, cPath.length - 1) : cPath)
184 | // const allPageGlobPathList = glob
185 | // .globSync(`${this.outputPath}/**/*{.html,.wxml}`)
186 | // .filter((str) => {
187 | // return ![
188 | // 'page-frame.html'
189 | // ].includes(path.basename(str))
190 | // })
191 |
192 | const allPage = await this._analyticalCompDependence(appConfigPages)
193 |
194 | const allPageAndComp = allPage.filter(_path => !isPluginPath(_path))
195 |
196 | for (let pagePath of allPageAndComp) {
197 | // console.log("🚀 ~ DecompilationController ~ generateDefaultAppFiles ~ pagePath:", pagePath)
198 | // /* json */
199 | // console.log(replaceExt(pagePath, ".json"), pagePath)
200 | let jsonPath = path.join(this.outputPath, replaceExt(pagePath, ".json"))
201 | saveLocalFile(jsonPath, '{\n "component":true\n}');
202 | let jsName = replaceExt(pagePath, ".js")
203 | let jsPath = path.join(this.outputPath, jsName)
204 | saveLocalFile(jsPath, "Page({ data: {} })");
205 | /* wxml */
206 | let wxmlName = replaceExt(pagePath, ".wxml");
207 | let wxmlPath = path.join(this.outputPath, wxmlName)
208 | saveLocalFile(wxmlPath, `${wxmlName}`);
209 | }
210 | printLog(` \u25B6 生成页面和组件构成必要的默认文件成功. \n`, { isStart: true })
211 | }
212 |
213 | /**
214 | * 缓存移除
215 | * */
216 | protected async removeCache() {
217 | await sleep(500)
218 | let cont = 0
219 | const removeFileList = removeGameFileList.concat(removeAppFileList)
220 | const allFile = glob.globSync(`${this.outputPath}/**/**{.js,.html,.json}`)
221 | allFile.forEach(filepath => {
222 | const fileName = path.basename(filepath).trim()
223 | const extname = path.extname(filepath)
224 | if (!fs.existsSync(filepath)) return
225 | let _deleteLocalFile = () => {
226 | cont++
227 | deleteLocalFile(filepath, { catch: true, force: true })
228 | }
229 | if (removeFileList.includes(fileName)) {
230 | _deleteLocalFile()
231 | } else if (extname === '.html') {
232 | const feature = 'var __setCssStartTime__ = Date.now()'
233 | const data = readLocalFile(filepath)
234 | if (data.includes(feature)) _deleteLocalFile()
235 | } else if (filepath.endsWith('.appservice.js')) {
236 | _deleteLocalFile()
237 | } else if (filepath.endsWith('.webview.js')) {
238 | _deleteLocalFile()
239 | }
240 | })
241 |
242 | if (cont) {
243 | printLog(`\n \u25B6 移除中间缓存产物成功, 总计 ${colors.yellow(cont)} 个`, { isStart: true })
244 | }
245 | }
246 |
247 | /**
248 | * 收尾工作
249 | * */
250 | private async endingAllJob(): Promise {
251 | if (this.config.unpackOnly) return
252 | await this.generateDefaultAppFiles()
253 | await this.generaProjectConfigFiles()
254 | if (!isDev) {
255 | await this.removeCache()
256 | }
257 | printLog(` ✅ ${colors.bold(colors.green('编译流程结束!'))}`, { isEnd: true })
258 | }
259 | }
260 |
--------------------------------------------------------------------------------
/src/interface/app-decompilation.ts:
--------------------------------------------------------------------------------
1 | import { VM } from 'vm2'
2 | import fs from "node:fs";
3 | import colors from "picocolors";
4 | import path from "node:path";
5 | import { glob } from "glob";
6 | import process from "node:process";
7 | import cssbeautify from "cssbeautify";
8 | import { BaseDecompilation } from "./base-decompilation";
9 | import { createVM, runVmCode } from "@/utils/create-vm";
10 | import { readLocalFile, saveLocalFile } from "@/utils/fs-process";
11 | import { appJsonExcludeKeys, cssBodyToPageReg, pluginDirRename } from "@/constant";
12 | import { getZ } from "@/utils/get-z";
13 | import { tryDecompileWxml } from "@/utils/decompile-wxml";
14 | import { AppCodeInfo, ExecuteAllGwxFunction, ModuleDefine, UnPackInfo, WxmlRenderFunction, WxsRefInfo } from "@/type";
15 | import {
16 | arrayDeduplication,
17 | getParameterNames,
18 | isPluginPath, isWxAppid, jsBeautify,
19 | printLog, removeElement, resetPluginPath, resetWxsRequirePath,
20 | sleep
21 | } from "@/utils/common";
22 | import { getAppPackCodeInfo } from "@/utils/get-pack-codeInfo";
23 | import { JSDOM } from "jsdom";
24 |
25 | /**
26 | * 反编译小程序
27 | * */
28 | export class AppDecompilation extends BaseDecompilation {
29 | private codeInfo: AppCodeInfo
30 | /**
31 | * 是否将第三方的远程插件转换变成本地离线使用
32 | * */
33 | public convertPlugin: boolean = false
34 | /**
35 | * 包的配置
36 | * */
37 | public appConfig: Record = {}
38 | /**
39 | * 主包所有入口 ( 不包含分包 )
40 | * */
41 | public mainPackEntries: string[] = []
42 |
43 | /**
44 | * 所有在 page.json 中被引用的组件
45 | * */
46 |
47 | public constructor(packInfo: UnPackInfo) {
48 | super(packInfo);
49 | }
50 |
51 | /**
52 | * 初始化, 所有后续反编译且不会被动态改变的所需要的信息都在这里加载
53 | * */
54 | private async initApp() {
55 | this.codeInfo = getAppPackCodeInfo(this.pathInfo)
56 | this.appConfig = JSON.parse(readLocalFile(this.pathInfo.outputResolve(this.pathInfo.appJsonPath)) || '{}')
57 |
58 | // 用户 polyfill
59 | const loadInfo = {}
60 | for (const name in this.codeInfo) {
61 | loadInfo[name] = this.codeInfo[name].length
62 | }
63 | console.log(loadInfo)
64 | let code = this.codeInfo.appWxss || this.codeInfo.pageFrame || this.codeInfo.pageFrameHtml
65 | if (!code) {
66 | if (this.packType === 'main') {
67 | console.log(colors.red('\u274C 没有找到包特征文件'))
68 | }
69 | return
70 | }
71 | }
72 |
73 | /**
74 | * 解析出 app.json 文件, 只有主包需要处理
75 | * */
76 | private async decompileAppJSON() {
77 | if (this.packType !== 'main') return
78 | await sleep(200)
79 | const appConfig: Record = JSON.parse(this.codeInfo.appConfigJson)
80 | Object.assign(appConfig, appConfig.global)
81 | delete appConfig.global
82 | delete appConfig.page
83 | if (appConfig.entryPagePath) appConfig.entryPagePath = appConfig.entryPagePath.replace('.html', '')
84 | if (appConfig.renderer) {
85 | appConfig.renderer = appConfig.renderer.default || 'webview'
86 | }
87 |
88 | if (appConfig.extAppid) {
89 | saveLocalFile(this.pathInfo.outputResolve('ext.json'), JSON.stringify({
90 | extEnable: true,
91 | extAppid: appConfig.extAppid,
92 | ext: appConfig.ext
93 | }, null, 2))
94 | }
95 |
96 | if (this.codeInfo.appConfigJson.includes('"renderer": "skyline"') || this.codeInfo.appConfigJson.includes('"renderer":"skyline"')) {
97 | appConfig.lazyCodeLoading = "requiredComponents"
98 | delete appConfig.window['navigationStyle']
99 | delete appConfig.window['navigationBarTextStyle']
100 | delete appConfig.window['navigationBarTitleText']
101 | delete appConfig.window['navigationBarBackgroundColor']
102 | }
103 |
104 | this.mainPackEntries = arrayDeduplication([...appConfig.pages])
105 | if (appConfig.subPackages) {
106 | let subPackages = [];
107 | appConfig.subPackages.forEach((subPackage: Record) => {
108 | let root = subPackage.root;
109 | let newPages = [];
110 | root = !String(root).endsWith('/') ? root + '/' : root
111 | root = String(root).startsWith('/') ? root.substring(1) : root
112 | subPackage.root = root;
113 | if (Array.isArray(appConfig.pages)) {
114 | for (let pageString of appConfig.pages) {
115 | if (pageString.startsWith(root)) {
116 | // console.log(pageString)
117 | removeElement(this.mainPackEntries, pageString)
118 | newPages.push(pageString.replace(root, ''));
119 | }
120 | }
121 | subPackage.pages = arrayDeduplication(newPages);
122 | }
123 | if (subPackage.plugins) {
124 | subPackage.plugins = {} // 分包插件从远程替换成本地编译使用
125 | }
126 | subPackages.push(subPackage);
127 | })
128 | subPackages = subPackages.filter(sub => (sub.pages || []).length > 0)
129 | if (Object.keys(subPackages).length >= 100) {
130 | console.log(` ▶ ${colors.red('程序主动结束编译, 因为 subPackages 包个数超过限制 100, 超过微信限制')}`)
131 | process.exit(0)
132 | }
133 | delete appConfig.subPackages
134 | appConfig.subPackages = subPackages;
135 | }
136 | if (appConfig.pages) {
137 | appConfig.pages =/*必须在subPackages 之后*/ this.mainPackEntries
138 | }
139 |
140 | if (appConfig.tabBar) {
141 | if (!appConfig.tabBar.list) appConfig.tabBar.list = []
142 | const allDecompilationBeforeFileList = glob.globSync(`${this.pathInfo.outputPath}/**`)
143 | const allFileBufferInfo = allDecompilationBeforeFileList
144 | .filter(filePath => !fs.statSync(filePath).isDirectory())
145 | .map(filePath => {
146 | return {
147 | data: readLocalFile(filePath, 'base64'),
148 | path: filePath
149 | }
150 | })
151 | appConfig.tabBar.list = appConfig.tabBar.list.map((info: Record) => {
152 | const result: Record = { text: info.text }
153 | result.pagePath = info.pagePath.replace('.html', '')
154 | if (info.iconData) {
155 | const found = allFileBufferInfo.find(item => item.data === info.iconData)
156 | if (found) result.iconPath = path.relative(this.pathInfo.outputPath, found.path)
157 | }
158 | if (info.selectedIconData) {
159 | const found = allFileBufferInfo.find(item => item.data === info.selectedIconData)
160 | if (found) result.selectedIconPath = path.relative(this.pathInfo.outputPath, found.path)
161 | }
162 | return result
163 | })
164 | }
165 |
166 | if (this.convertPlugin) {
167 | appConfig.plugins = {} // 插件从远程替换成本地编译使用
168 | }
169 |
170 | // componentFramework的 旧版值为 exparser, Skyline引擎值为 glasss-easel
171 | if (appConfig.componentFramework) {
172 | appConfig.componentFramework = appConfig.componentFramework?.default ||
173 | appConfig.componentFramework.allUsed?.[0] ||
174 | appConfig.componentFramework
175 | }
176 |
177 | delete appConfig.ext
178 |
179 | appJsonExcludeKeys.forEach(key => delete appConfig[key])
180 | const outputFileName = 'app.json'
181 | const appConfigSaveString = JSON
182 | .stringify(appConfig, null, 2)
183 | .replaceAll(pluginDirRename[0], pluginDirRename[1]) // 插件换名, 因为官方禁止反编译 __ 开头 目录
184 | saveLocalFile(this.pathInfo.outputResolve(outputFileName), appConfigSaveString, { force: true })
185 | printLog(" Completed " + ` (${appConfigSaveString.length}) \t` + colors.bold(colors.gray(this.pathInfo.outputResolve(outputFileName))))
186 | printLog(` \u25B6 反编译 ${outputFileName} 文件成功. \n`, { isStart: true })
187 | }
188 |
189 | /**
190 | * 处理子包 json,只需要处理主包, 子包解压自带 json
191 | * */
192 | private async decompileAllJSON() {
193 | const plugins: Record = {}
194 | const vm = createVM({
195 | sandbox: {
196 | definePlugin: function (pluginName: string, pluginFunc: Function) {
197 | plugins[pluginName] = pluginFunc
198 | },
199 | }
200 | })
201 | runVmCode(vm, this.codeInfo.appService)
202 | // 解析代码中的各个模块 json 配置
203 | this._injectPluginAppPageJSON(vm, plugins) // 要在解析 __wxAppCode__ 之前将插件的page.json配置注入 __wxAppCode__
204 | const __wxAppCode__ = Object.assign(vm.sandbox.__wxAppCode__, vm.sandbox.global?.__wxAppCode__ || {});
205 | for (const filePath in __wxAppCode__) {
206 | if (path.extname(filePath) !== '.json') continue
207 | let tempFilePath = filePath
208 | const pageJson: Record = __wxAppCode__[filePath]
209 | const { componentPlaceholder, usingComponents } = pageJson
210 | if (componentPlaceholder) { // 处理异步分包加载占位符
211 | Object.keys(componentPlaceholder).forEach(name => componentPlaceholder[name] = 'view')
212 | }
213 |
214 | for (const key in usingComponents) {
215 | if (usingComponents[key].startsWith("/./")){
216 | // console.log("🚀 ~ decompileAllJSON ~ usingComponents[key]:", usingComponents[key])
217 | usingComponents[key] = usingComponents[key].substring(3)
218 | }
219 | usingComponents[key] = path.join(path.dirname(filePath), usingComponents[key])
220 | }
221 |
222 | let realJsonConfigString = JSON.stringify(pageJson, null, 2)
223 | let jsonOutputPath = filePath
224 | if (isPluginPath(filePath)) {
225 | tempFilePath = path.join(pluginDirRename[0], filePath.replace('plugin-private://', ''))
226 | jsonOutputPath = path.join(this.pathInfo.packRootPath, tempFilePath)
227 | }
228 | // console.log(jsonOutputPath)
229 | printLog(" Completed " + ` (${realJsonConfigString.length}) \t` + colors.bold(colors.gray(jsonOutputPath)))
230 | saveLocalFile(this.pathInfo.outputResolve(jsonOutputPath), realJsonConfigString, { force: true })
231 | }
232 | printLog(` \u25B6 反编译所有 page json 文件成功. \n`, { isStart: true })
233 | }
234 |
235 | /**
236 | * 将 json 信息注入沙箱 __wxAppCode__ 中
237 | * */
238 | private _injectPluginAppPageJSON(vm: VM, plugins: Record) {
239 | const sandBox = vm.sandbox
240 | // 反编译插件的 JS 代码
241 | for (const pluginName in plugins) {
242 | const global = {
243 | __wxAppCode__: {},
244 | publishDomainComponents() {
245 | },
246 | }
247 | const pluginFunc = plugins[pluginName]
248 | const paramNameList = getParameterNames(pluginFunc)
249 | const paramValueList = paramNameList.map((name: string) => {
250 | if (name === 'global') return global
251 | return sandBox[name] || sandBox.window[name]
252 | })
253 | pluginFunc.apply(sandBox.window, paramValueList)
254 | Object.assign(sandBox.__wxAppCode__, global.__wxAppCode__)
255 | }
256 | }
257 |
258 | private async decompileAppJS() {
259 | const _this = this
260 | const plugins: Record = {}
261 | const sandbox = {
262 | require() {
263 | },
264 | define(name: string, func: Function) {
265 | _this._parseJsDefine(name, func)
266 | },
267 | definePlugin: function (pluginName: string, pluginFunc: Function) {
268 | plugins[pluginName] = pluginFunc
269 | },
270 | }
271 | let appServiceCode = this.codeInfo.appService
272 | if (appServiceCode) {
273 | const vm = createVM()
274 | Object.assign(vm.sandbox, sandbox) // 将沙箱函数替换回来, 下方同理
275 | appServiceCode = appServiceCode
276 | .replaceAll('=__webnode__.define;', ';')
277 | .replaceAll('=__webnode__.require;', ';')
278 | runVmCode(vm, appServiceCode)
279 | Object.assign(vm.sandbox, sandbox)
280 | this._decompilePluginAppJS(vm, plugins)
281 | printLog(` \u25B6 反编译所有 js 文件成功. \n`, { isStart: true })
282 | }
283 | }
284 |
285 | /**
286 | * 反编译插件 JS 代码
287 | * */
288 | private _decompilePluginAppJS(vm: VM, plugins: Record) {
289 | const sandBox = vm.sandbox
290 | const mainEnvDefine = sandBox.define
291 | const _this = this
292 | sandBox.global = sandBox.window;
293 | let pluginDefine: Function
294 | // 反编译插件的 JS 代码
295 | for (const pluginName in plugins) {
296 | const appid = pluginName.replace('plugin://', '')
297 | pluginDefine = function (name: string, func: string) {
298 | const pluginPath = path.relative(
299 | _this.pathInfo.outputPath,
300 | _this.pathInfo.resolve(`${pluginDirRename[0]}/${appid}/${name}`)
301 | )
302 | mainEnvDefine(pluginPath, func)
303 | }
304 | const pluginFunc = plugins[pluginName]
305 | const paramNameList = getParameterNames(pluginFunc)
306 | const paramValueList = paramNameList.map((name: string) => {
307 | if (name === 'define') return pluginDefine
308 | return sandBox[name] || sandBox.window[name] || sandBox.window.document[name]
309 | })
310 | // console.log(pluginName, getParameterNames(pluginFunc));
311 | pluginFunc.apply(sandBox.window, paramValueList)
312 | }
313 | }
314 |
315 | private _setCssToHead(arr: any[], _invalid: any, opt?: { path: string, suffix?: string }) {
316 | if (typeof opt === 'object' && opt.path && Array.isArray(arr)) {
317 | let cssPath = opt.path
318 | const isPlugin = isPluginPath(cssPath)
319 | if (isPlugin) { // 解析插件,重定向到插件所在路径
320 | // 将插件路径重定向到主包 或者 分包所在路径
321 | cssPath = resetPluginPath(cssPath, path.join(this.pathInfo.packRootPath, pluginDirRename[0]))
322 | }
323 | arr = arr.map((item) => {
324 | if (Array.isArray(item)) {
325 | const type = item[0]
326 | if (type === 0) {
327 | return typeof item[1] === 'number' ? `${item[1]}rpx` : ''
328 | } else if (type === 2) {
329 | if (typeof item[1] === 'string') {
330 | const relativePath = path.relative(
331 | this.pathInfo.outputResolve(path.dirname(cssPath)),
332 | this.pathInfo.resolve(item[1]),
333 | )
334 | return `@import "${relativePath}";`
335 | }
336 | return ''
337 | } else if (type === 1) {
338 | return opt.suffix || ''
339 | } else {
340 | return item[1]
341 | }
342 | }
343 | return item
344 | })
345 | let cssText = arr.join('')
346 | cssText = cssText.replace(cssBodyToPageReg, 'page{')
347 | saveLocalFile(this.pathInfo.outputResolve(cssPath), cssbeautify(cssText))
348 | }
349 | return () => void 0
350 | }
351 |
352 | private async decompileAppWXSSWithRpx() {
353 | const globalSetMatchReg = /setCssToHead\(.+?}\)\(\)/g
354 | let code = this.codeInfo.appWxss || this.codeInfo.pageFrame || this.codeInfo.pageFrameHtml
355 | code = code.replaceAll('return rewritor;', 'return ()=> ({file, info});')
356 | code = code.replaceAll('__COMMON_STYLESHEETS__[', '__COMMON_STYLESHEETS_HOOK__[')
357 | // code = code.replaceAll('var setCssToHead', 'var __setCssToHead__')
358 | code = code.replaceAll(';__wxAppCode__', ';\n__wxAppCode__')
359 | // code = code.replaceAll(
360 | // 'var setCssToHead=function(file,_xcInvalid,info){',
361 | // 'var setCssToHead=function(file,_xcInvalid,info){ return {file, info} ',
362 | // )
363 | const vm = createVM({
364 | sandbox: { __COMMON_STYLESHEETS_HOOK__: {} }
365 | })
366 | runVmCode(vm, code)
367 | /* 拦截直接执行 的 全局 css */
368 | let lastMatch = null
369 | do {
370 | lastMatch = globalSetMatchReg.exec(code)
371 | if (!lastMatch) break
372 | const cssSeedCode: string = lastMatch[0]
373 | try {
374 | const func = new Function('setCssToHead', cssSeedCode)
375 | func(this._setCssToHead.bind(this))
376 | } catch (e) {
377 | console.log(e.message)
378 | }
379 | } while (lastMatch)
380 |
381 | /* 拦截组件的 css */
382 | const __wxAppCode__ = vm.sandbox['__wxAppCode__']
383 | for (let cssPath in __wxAppCode__) {
384 | if (path.extname(cssPath) !== '.wxss') continue
385 | const { file: astList, info = {} } = __wxAppCode__[cssPath]()
386 | this._setCssToHead(astList, null, { path: cssPath, suffix: info.suffix })
387 | }
388 | /* 拦截 @import 引入的的公共 css */
389 | const __COMMON_STYLESHEETS_HOOK__ = vm.sandbox.__COMMON_STYLESHEETS_HOOK__ || {}
390 | for (let cssPath in __COMMON_STYLESHEETS_HOOK__) {
391 | const astList = __COMMON_STYLESHEETS_HOOK__[cssPath]
392 | cssPath = path.join(this.pathInfo.packRootPath, cssPath)
393 | this._setCssToHead(astList, null, { path: cssPath })
394 | }
395 | }
396 |
397 | /**
398 | * 反编译包中的 wxss 文件
399 | * */
400 | private async decompileAppWXSS() {
401 | let code = this.codeInfo.appWxss || this.codeInfo.pageFrame || this.codeInfo.pageFrameHtml
402 | if (!code.trim()) return
403 | const vm = createVM()
404 | runVmCode(vm, code)
405 | const __wxAppCode__ = vm.sandbox['__wxAppCode__']
406 | if (!__wxAppCode__) return
407 | const children = vm.sandbox.window.document.head.children || [] as HTMLStyleElement[]
408 | const mainPackageRenderedNodes = Array.from(children)
409 | // 先加载所有的 css,在节点中可能已经加载了部分主页的的 css, 下方作用是模拟切换页面并加载其页面的 css
410 | for (let filepath in __wxAppCode__) {
411 | if (path.extname(filepath) !== '.wxss') continue
412 | __wxAppCode__[filepath]()
413 | const lastStyleEl = children[children.length - 1]
414 | const attr_wxss_path = lastStyleEl.getAttribute('wxss:path')
415 | if (!attr_wxss_path) continue
416 | if (isPluginPath(filepath)) { // 解析插件,重定向到插件所在路径
417 | // 将插件路径重定向到主包 或者 分包所在路径
418 | filepath = resetPluginPath(filepath, path.join(this.pathInfo.packRootPath, pluginDirRename[0]))
419 | lastStyleEl.setAttribute('wxss:path', filepath)
420 | }
421 | }
422 | // 提取 css 及其所在路径
423 | Array.from(children).forEach((styleEl: Element) => {
424 | if (this.packType !== 'main') {
425 | if (mainPackageRenderedNodes.includes(styleEl)) return
426 | }
427 | const wxss_path = styleEl.getAttribute('wxss:path')
428 | if (['', 'null', 'undefined', undefined, null].includes(wxss_path)) return
429 | let cssText = styleEl.innerHTML
430 | cssText = cssText.replace(cssBodyToPageReg, 'page{') // 不太严谨, 后面使用 StyleSheet 进行处理
431 | saveLocalFile(this.pathInfo.outputResolve(wxss_path), cssbeautify(cssText))
432 | printLog(" Completed " + ` (${cssText.length}) \t` + colors.bold(colors.gray(wxss_path)))
433 | })
434 | if (children.length) {
435 | printLog(` \u25B6 反编译所有 wxss 文件成功. \n`, { isStart: true })
436 | }
437 | }
438 |
439 | public functionToWXS(wxsFunc: Function, basePath: string) {
440 | if (!basePath) {
441 | throw new Error('basePath is required')
442 | }
443 | const funcHeader = 'nv_module={nv_exports:{}};';
444 | const funcEnd = 'return nv_module.nv_exports;}';
445 | const matchReturnReg = /return\s*\(\{(.|\r|\t|\n)*?}\)/
446 | const wxsCodeRequireReg = /require\(.+?\(\);/g
447 |
448 | let code = wxsFunc.toString()
449 | code = code.slice(code.indexOf(funcHeader) + funcHeader.length, code.lastIndexOf(funcEnd)).replaceAll('nv_', '')
450 | code = code.replace(wxsCodeRequireReg, (matchString: string) => {
451 | const newRequireString = resetWxsRequirePath(matchString, './')
452 | .replace(`require("`, '')
453 | .replace(`")();`, '')
454 | // console.log(newRequireString)
455 | let relativePath = path.relative(
456 | this.pathInfo.resolve(path.dirname(basePath)),
457 | this.pathInfo.resolve(newRequireString),
458 | );
459 | // console.log("🚀 ~ code=code.replace ~ relativePath:", relativePath)
460 | return `require('${relativePath}');`
461 | })
462 | const matchInfo = matchReturnReg.exec(code)
463 | const matchList = []
464 | if (matchInfo) {
465 | matchInfo.forEach(str => str.startsWith('return') && str.endsWith('})') && matchList.push(str))
466 | }
467 | matchList.forEach(returnStr => {
468 | let newReturnString: string = ''
469 | let temp = returnStr.replace('return', '').trim()
470 | if (temp.startsWith('({') && temp.endsWith('})')) {
471 | newReturnString = `return {${temp.substring(2, temp.length - 2)}}`
472 | code = code.replace(returnStr, newReturnString)
473 | }
474 | })
475 | return jsBeautify(code)
476 | }
477 |
478 | /**
479 | * 执行所有的 $gwx_ 函数, 包含 主环境 和 插件函数
480 | * */
481 | private executeAllGwxFunction(code: string): ExecuteAllGwxFunction {
482 | code = code
483 | .replaceAll(
484 | 'var e_={}',
485 | `var e_ = {}; window.COMPONENTS = global;`
486 | )
487 | .replaceAll(
488 | 'function(){if(!nnm[n])',
489 | `function(){ return {n, func: nnm[n]};`
490 | )
491 | const vm = createVM({
492 | sandbox: {
493 | setTimeout
494 | }
495 | })
496 | runVmCode(vm, code)
497 |
498 | // 主包 或 分包 自身插件的 模块 定义信息
499 | const PLUGINS: Record = {}
500 | // 主环境( 主包, 分包 )的 模块 定义信息
501 | const COMPONENTS: ModuleDefine = {
502 | entrys: {},
503 | modules: {},
504 | defines: {}
505 | }
506 | const ALL_ENTRYS: ModuleDefine["entrys"] = {}
507 | const ALL_MODULES: ModuleDefine["modules"] = {}
508 | const ALL_DEFINES: ModuleDefine["defines"] = {}
509 | const pluginNames = glob.globSync(`${this.pathInfo.resolve(pluginDirRename[1])}/*`)
510 | .map(pluginPath => path.basename(pluginPath))
511 | .filter(isWxAppid)
512 |
513 | for (const name in vm.sandbox) {
514 | const func = vm.sandbox[name]
515 | if (typeof func !== 'function') continue
516 | vm.sandbox.__wxAppCode__ = {}
517 | const isPlugin = name.startsWith('$gwx_wx') && pluginNames.find(pluginName => name.includes(pluginName))
518 | const global: Partial = {}
519 | if (isPlugin) { // 插件处理
520 | const appId = name.replace('$gwx_', '') // 插件APPID
521 | try {
522 | // 将所有的 $gwx_ 加载到 global 对象中, window.COMPONENTS 是 global 的引用
523 | func(void 0, global)
524 | PLUGINS[appId] = global as any // 区分每个插件环境
525 | } catch (e) {
526 | }
527 | } else if (name.startsWith('$gwx')) { // 主环境模块组件处理
528 | try {
529 | func('', COMPONENTS)() // 注入主环境
530 | } catch (e) {
531 | }
532 | }
533 | }
534 | //--------------------------------------------------------------------------
535 | const getWxsInfo = (data: Record, appid?: string): Record => {
536 | if (typeof data !== 'function') return data
537 | data = data()
538 | data.isInline = data.n.startsWith('m_')
539 | const wxsPath = `${resetWxsRequirePath(data.n).split(':')[0]}`
540 | if (appid) {
541 | data.appid = appid
542 | data.n = path.join(this.pathInfo.packRootPath, pluginDirRename[0], appid, wxsPath)
543 | } else {
544 | data.n = path.join(this.pathInfo.packRootPath, wxsPath)
545 | }
546 | return data
547 | }
548 | //--------------------------------------------------------------------------
549 | const wxsModuleProcess = (receive: Record, _path: string, data: any, appid?: string) => {
550 | const ext = path.extname(_path)
551 | if (ext === '.wxs' && typeof data === 'function') {
552 | data = getWxsInfo(data, appid)
553 | }
554 | if (ext === '.wxml' && typeof data === 'object') {
555 | for (const moduleName in data) {
556 | data[moduleName] = getWxsInfo(data[moduleName], appid)
557 | }
558 | }
559 | receive[_path] = data
560 | }
561 | //--------------------------------------------------------------------------
562 | const merge = (receive: Record, type: keyof ModuleDefine) => {
563 | for (let _path in COMPONENTS[type]) {
564 | let data: any = COMPONENTS[type][_path]
565 | if (type === 'modules') {
566 | wxsModuleProcess(receive, _path, data)
567 | } else {
568 | receive[path.join(_path)] = COMPONENTS[type][_path]
569 | }
570 | }
571 | for (const appid in PLUGINS) {
572 | const plugin = PLUGINS[appid]
573 | for (let _path in plugin[type]) {
574 | let data: any = plugin[type][_path]
575 | _path = path.join(this.pathInfo.packRootPath, pluginDirRename[0], appid, _path)
576 | if (type === 'modules') {
577 | wxsModuleProcess(receive, _path, data, appid)
578 | } else {
579 | receive[path.join(_path)] = data
580 | }
581 | }
582 | }
583 | }
584 | //--------------------------------------------------------------------------
585 | // 合并主环境 和 插件环境
586 | merge(ALL_ENTRYS, 'entrys')
587 | merge(ALL_MODULES, 'modules')
588 | merge(ALL_DEFINES, 'defines')
589 |
590 | return {
591 | COMPONENTS,
592 | PLUGINS,
593 | ALL_ENTRYS,
594 | ALL_MODULES,
595 | ALL_DEFINES
596 | }
597 | }
598 |
599 | private async decompileAppWXS() {
600 | let code = this.codeInfo.appWxss || this.codeInfo.pageFrame || this.codeInfo.pageFrameHtml
601 | const { ALL_MODULES, PLUGINS } = this.executeAllGwxFunction(code)
602 | const wxsRefInfo = []
603 | for (const wxmlPath in ALL_MODULES) {
604 | if (path.extname(wxmlPath) !== '.wxml') continue
605 | const wxmlRefWxsInfo = ALL_MODULES[wxmlPath]
606 | for (const moduleName in wxmlRefWxsInfo) {
607 | const { n, func, isInline } = wxmlRefWxsInfo[moduleName]
608 | // console.log(n)
609 | if (n && func) {
610 | wxsRefInfo.push({
611 | wxmlPath: wxmlPath,
612 | wxsPath: n,
613 | isInline: isInline,
614 | moduleName,
615 | wxsRender: func,
616 | templateList: []
617 | })
618 | }
619 | }
620 | }
621 | // 保存被 wxml 引用的 wxs 文件
622 | wxsRefInfo.forEach(item => {
623 | if (item.isInline) return
624 | if (!item.wxsPath.endsWith('.wxs')) return
625 | const wxsString = this.functionToWXS(item.wxsRender, item.wxsPath)
626 | saveLocalFile(this.pathInfo.outputResolve(item.wxsPath), wxsString)
627 | printLog(" Completed " + ` (${wxsString.length}) \t` + colors.bold(colors.gray(item.wxsPath)))
628 | })
629 | // 保存游离的被 JS 引用的 wxs 文件
630 | for (const wxsPath in ALL_MODULES) {
631 | if (!wxsPath.endsWith('.wxs')) continue
632 | const result: Record = ALL_MODULES[wxsPath]
633 | const wxsString = this.functionToWXS(result.func, wxsPath)
634 | saveLocalFile(this.pathInfo.outputResolve(wxsPath), wxsString)
635 | printLog(" Completed " + ` (${wxsString.length}) \t` + colors.bold(colors.gray(wxsPath)))
636 | }
637 | // 解析模板归属
638 | wxsRefInfo.forEach(item => {
639 | let relativePath = path.relative(
640 | this.pathInfo.resolve(path.dirname(item.wxmlPath)),
641 | this.pathInfo.resolve(item.wxsPath)
642 | )
643 | if (item.isInline) {
644 | item.templateList.push(`\n${this.functionToWXS(item.wxsRender, item.wxsPath)}\n`);
645 | } else {
646 | item.templateList.push(``);
647 | }
648 | })
649 | // 修改 wxml 文件
650 | wxsRefInfo.forEach(item => {
651 | if (item.templateList && !item.templateList.length) return
652 | const wxmlAbsolutePath = this.pathInfo.outputResolve(item.wxmlPath)
653 | const templateString = item.templateList.join('\n')
654 | const wxmlCode = readLocalFile(wxmlAbsolutePath)
655 | saveLocalFile(wxmlAbsolutePath, `${wxmlCode}\n${templateString}`, { force: true })
656 | })
657 |
658 | if (Object.keys(wxsRefInfo).length) {
659 | printLog(` \u25B6 反编译所有 wxs 文件成功. \n`, { isStart: true })
660 | }
661 | }
662 |
663 | /**
664 | * 获取定义的 X 路径池, 池中的内容顺序不能变
665 | * */
666 | private _getXPool(code: string) {
667 | let xPool = []
668 | const xPoolReg = /var\s+x=\s*\[(.+)];\$?/g
669 | const regResList = (code.match(xPoolReg) || [])
670 | .sort((a, b) => b.length - a.length) // 排序, 优先匹配内容最多的, 可能会遇到特殊情况,后面再看看
671 | if (regResList.length && regResList[0].includes('var x=[') && regResList[0].includes('.wxml')) {
672 | xPool = regResList[0]
673 | .replaceAll('var x=[', '')
674 | .replaceAll('];', '')
675 | .split(',')
676 | .map(str => str.replaceAll("'", ''))
677 | }
678 | return xPool
679 | }
680 |
681 | private async decompileAppWXML() {
682 | let code = this.codeInfo.appWxss || this.codeInfo.pageFrame || this.codeInfo.pageFrameHtml
683 | if (!code) return
684 | const { ALL_DEFINES, ALL_ENTRYS } = this.executeAllGwxFunction(code)
685 | let xPool = this._getXPool(code)
686 | const vm = createVM()
687 | runVmCode(vm, code)
688 | getZ(code, (z: Record) => {
689 | const entrys = ALL_ENTRYS
690 | for (let wxmlPath in entrys) {
691 | let result = tryDecompileWxml(entrys[wxmlPath].f.toString(), z, ALL_DEFINES[wxmlPath], xPool)
692 | if (result) {
693 | /* 重定向图片相对链接 */
694 | const jsdom = new JSDOM(result)
695 | const document = jsdom.window.document
696 | const allImageEls = document.querySelectorAll('[src]')
697 | const matchAppidInfo = /__plugin__\/(\w+)\//.exec(wxmlPath)
698 | if (matchAppidInfo && wxmlPath.includes(pluginDirRename[0])) {
699 | const appid = matchAppidInfo[1]
700 | const pluginRoot = path.join(wxmlPath.split(appid)[0], appid)
701 | const srcList = Array.from(allImageEls).map(node => {
702 | let src = node.getAttribute('src')
703 | const originSrc = src
704 | if (src.includes('{{') && src.includes('}}')) return null
705 | if (!src.trim()) return null;
706 | if (src.startsWith('/')) {
707 | src = path.join(pluginRoot, src)
708 | src = path.relative(path.dirname(wxmlPath), src)
709 | }
710 | return [originSrc, src]
711 | }).filter(Boolean)
712 | srcList.forEach(([originSrc, src]) => {
713 | result = result.replaceAll(originSrc, src)
714 | })
715 | }
716 | const wxmlFullPath = this.pathInfo.outputResolve(wxmlPath)
717 | saveLocalFile(wxmlFullPath, result, { force: true }) // 不管文件存在或者存在默认模板, 此时通过 z 反编译出来的文件便是 wxml, 直接保存覆盖
718 | printLog(` Completed (${result.length}) \t${colors.bold(colors.gray(wxmlPath))}`)
719 | }
720 | }
721 | })
722 | await sleep(200)
723 | printLog(` \u25B6 反编译所有 wxml 文件成功. \n`, { isStart: true })
724 | }
725 |
726 | public async decompileAll(options: { usePx?: boolean } = {}) {
727 | super.decompileAll()
728 | /* 开始编译 */
729 | await this.initApp()
730 | await this.decompileAllJSON()
731 | await this.decompileAppJSON() // 在 pageJson 解析后, 之后使用经过处理的 app.json 如果存在 app.json 则覆盖原来的 json
732 | await this.decompileAppJS()
733 | if (options.usePx) {
734 | await this.decompileAppWXSS()
735 | } else {
736 | await this.decompileAppWXSSWithRpx() // 优先 rpx 单位解析
737 | }
738 | await this.decompileAppWXML()
739 | await this.decompileAppWXS() // 解析 WXS 应该在解析完所有 WXML 之后运行
740 | await this.decompileAppWorkers()
741 | }
742 | }
743 |
744 |
--------------------------------------------------------------------------------
/src/interface/base-decompilation.ts:
--------------------------------------------------------------------------------
1 | import colors from "picocolors";
2 | import path from "node:path";
3 | import fs from "node:fs";
4 | import {PloyFillCover} from "./ployfill-cover";
5 | import {createVM, runVmCode} from "@/utils/create-vm";
6 | import {readLocalFile, saveLocalFile} from "@/utils/fs-process";
7 | import {
8 | AppTypeMapping,
9 | MiniAppType,
10 | MiniPackType,
11 | PackTypeMapping,
12 | PathResolveInfo,
13 | UnPackInfo
14 | } from "@/type";
15 | import {commonDir, jsBeautify, printLog, removeVM2ExceptionLine, sleep} from "@/utils/common";
16 |
17 | export class BaseDecompilation {
18 | public readonly pathInfo: PathResolveInfo
19 | public readonly outputPathInfo: PathResolveInfo
20 | public readonly packPath: string
21 | public readonly packType: MiniPackType
22 | public readonly appType: MiniAppType
23 | public readonly ployFill: PloyFillCover
24 |
25 | constructor(packInfo: UnPackInfo) {
26 | this.pathInfo = packInfo.pathInfo
27 | this.outputPathInfo = packInfo.outputPathInfo
28 | this.packPath = packInfo.inputPath
29 | this.packType = packInfo.packType
30 | this.appType = packInfo.appType
31 | this.ployFill = new PloyFillCover(this.packPath)
32 | }
33 |
34 | protected async decompileAppWorker(): Promise {
35 | await sleep(200)
36 | if (!fs.existsSync(this.pathInfo.workersPath)) {
37 | return
38 | }
39 | const appConfigString = readLocalFile(this.pathInfo.appJsonPath)
40 | if (!appConfigString) return
41 | const appConfig: Record = JSON.parse(appConfigString)
42 | let code = readLocalFile(this.pathInfo.workersPath)
43 | let commPath: string = '';
44 | let vm = createVM({
45 | sandbox: {
46 | define(name: string) {
47 | name = path.dirname(name) + '/';
48 | if (!commPath) commPath = name;
49 | commPath = commonDir(commPath, name);
50 | }
51 | }
52 | })
53 | runVmCode(vm, code.slice(code.indexOf("define(")))
54 | if (commPath.length > 0) commPath = commPath.slice(0, -1);
55 | printLog(`Worker path: ${commPath}`);
56 | appConfig.workers = commPath
57 | saveLocalFile(this.pathInfo.appJsonPath, JSON.stringify(appConfig, null, 2))
58 | printLog(` \u25B6 反编译 Worker 文件成功. \n`, {isStart: true})
59 | }
60 |
61 | /**
62 | * 反编译 Worker 文件
63 | * */
64 | protected async decompileAppWorkers(): Promise {
65 | await sleep(200)
66 | if (!fs.existsSync(this.pathInfo.workersPath)) {
67 | return
68 | }
69 | const _this = this
70 | let commPath: string = '';
71 | let code = readLocalFile(this.pathInfo.workersPath)
72 | let vm = createVM({
73 | sandbox: {
74 | define(name: string, func: Function) {
75 | _this._parseJsDefine(name, func)
76 | const workerPath = path.dirname(name) + '/';
77 | if (!commPath) commPath = workerPath;
78 | commPath = commonDir(commPath, workerPath);
79 | }
80 | }
81 | })
82 | runVmCode(vm, code)
83 | printLog(`Worker path: ${commPath}`);
84 |
85 | if (commPath) {
86 | const configFileName = this.appType === 'game' ? this.pathInfo.gameJsonPath : this.pathInfo.appJsonPath
87 | const appConfig: Record = JSON.parse(readLocalFile(configFileName))
88 | appConfig.workers = commPath
89 | saveLocalFile(configFileName, JSON.stringify(appConfig, null, 2), {force: true})
90 | }
91 | printLog(` \u25B6 反编译 Worker 文件成功. \n`, {isStart: true})
92 | }
93 |
94 |
95 | protected decompileAll() {
96 | printLog(` \u25B6 当前反编译目标[ ${AppTypeMapping[this.appType]} ] (${colors.yellow(PackTypeMapping[this.packType])}) : ` + colors.blue(this.packPath));
97 | printLog(` \u25B6 当前输出目录: ${colors.blue(this.pathInfo.outputPath)}\n`, {
98 | isEnd: true,
99 | });
100 | }
101 |
102 | protected _parseJsDefine(name: string, func: Function) {
103 | if (path.extname(name) !== '.js') return
104 | // console.log(name, func);
105 | /* 看看是否有 polyfill, 有的话直接使用注入 polyfill */
106 | const foundPloyfill = this.ployFill.findPloyfill(name)
107 | let resultCode: string = ''
108 | if (foundPloyfill) {
109 | resultCode = readLocalFile(foundPloyfill.fullPath)
110 | } else {
111 | let code = func.toString();
112 | code = code.slice(code.indexOf("{") + 1, code.lastIndexOf("}") - 1).trim();
113 | if (code.startsWith('"use strict";')) {
114 | code = code.replaceAll('"use strict";', '')
115 | } else if (code.startsWith("'use strict';")) {
116 | code = code.replaceAll(`'use strict';`, '')
117 | } else if ((code.startsWith('(function(){"use strict";') || code.startsWith("(function(){'use strict';")) && code.endsWith("})();")) {
118 | code = code.slice(25, -5);
119 | }
120 | code = code.replaceAll('require("@babel', 'require("./@babel')
121 | resultCode = jsBeautify(code.trim());
122 | }
123 | saveLocalFile(
124 | this.pathInfo.outputResolve(name),
125 | removeVM2ExceptionLine(resultCode.trim()),
126 | {force: true}
127 | )
128 | printLog(" Completed " + ` (${resultCode.length}) \t` + colors.bold(colors.gray(name)))
129 | }
130 |
131 | }
132 |
--------------------------------------------------------------------------------
/src/interface/game-decompilation.ts:
--------------------------------------------------------------------------------
1 | import colors from "picocolors";
2 | import {saveLocalFile} from "@/utils/fs-process";
3 | import {createVM, runVmCode} from "@/utils/create-vm";
4 | import {printLog, sleep} from "@/utils/common";
5 | import {GameCodeInfo, UnPackInfo} from "@/type";
6 | import {BaseDecompilation} from "@/interface/base-decompilation";
7 | import {getGamePackCodeInfo} from "@/utils/get-pack-codeInfo";
8 | import {GameJsonExcludeKeys} from "@/constant";
9 |
10 | /**
11 | * 反编译工具类入口
12 | * */
13 | export class GameDecompilation extends BaseDecompilation {
14 | private codeInfo: GameCodeInfo
15 | public wxsList: any[]
16 | public readonly allRefComponentList: string[] = []
17 | public readonly allSubPackagePages: string[] = []
18 | public readonly allPloyFill: { fullPath: string, ployfillPath: string }[] = []
19 |
20 | public constructor(packInfo: UnPackInfo) {
21 | super(packInfo);
22 | }
23 |
24 | /**
25 | * 初始化小游戏所需环境和变量
26 | * */
27 | private async initGame() {
28 | this.codeInfo = getGamePackCodeInfo(this.pathInfo)
29 | const loadInfo = {}
30 | for (const name in this.codeInfo) {
31 | loadInfo[name] = this.codeInfo[name].length
32 | }
33 | console.log(loadInfo)
34 | }
35 |
36 | /**
37 | * 反编译 game.json 文件, 只有主包需要处理
38 | * */
39 | private async decompileGameJSON() {
40 | if (this.packType !== 'main') return
41 | await sleep(200)
42 | const gameConfigString = this.codeInfo.appConfigJson
43 | const gameConfig: Record = JSON.parse(gameConfigString)
44 | Object.assign(gameConfig, gameConfig.global)
45 | GameJsonExcludeKeys.forEach(key => delete gameConfig[key])
46 |
47 | const outputFileName = 'game.json'
48 | const gameConfigSaveString = JSON.stringify(gameConfig, null, 2)
49 | saveLocalFile(this.pathInfo.outputResolve(outputFileName), gameConfigSaveString, {force: true})
50 | printLog(" Completed " + ` (${gameConfigSaveString.length}) \t` + colors.bold(colors.gray(this.pathInfo.outputResolve(outputFileName))))
51 | printLog(` \u25B6 反编译 ${outputFileName} 文件成功. \n`, {isStart: true})
52 | }
53 |
54 | /**
55 | * 反编译小游戏的js文件
56 | * */
57 | private async decompileGameJS() {
58 | const _this = this
59 | let cont = 0
60 | const evalCodeList = [
61 | this.codeInfo.subContextJs,
62 | this.codeInfo.gameJs
63 | ].filter(Boolean)
64 | const allJsList = []
65 | const sandbox = {
66 | define(name: string, func: Function) {
67 | allJsList.push(name)
68 | _this._parseJsDefine(name, func)
69 | cont++
70 | },
71 | require() {
72 | },
73 | }
74 | evalCodeList.forEach(code => {
75 | const vm = createVM({sandbox})
76 | if (!code.includes('define(') || !code.includes('function(require, module, exports)')) return
77 | try {
78 | runVmCode(vm, code)
79 | } catch (e) {
80 | console.log(e.message)
81 | }
82 | })
83 | // console.log(allJsList)
84 | if (cont) {
85 | printLog(` \u25B6 反编译所有 js 文件成功. \n`)
86 | }
87 | }
88 |
89 | public async decompileAll() {
90 | super.decompileAll()
91 | /* 开始编译 */
92 | await this.initGame()
93 | await this.decompileGameJSON()
94 | await this.decompileGameJS()
95 | await this.decompileAppWorkers()
96 | }
97 | }
98 |
99 |
--------------------------------------------------------------------------------
/src/interface/ployfill-cover.ts:
--------------------------------------------------------------------------------
1 | import path from "node:path";
2 | import {glob} from "glob";
3 | import {PloyfillItem} from "@/type";
4 |
5 | export class PloyFillCover {
6 | public readonly allPloyFills: PloyfillItem[] = []
7 |
8 | constructor(packPath: string) {
9 | const customHeaderPathPart = path.resolve(path.dirname(packPath), 'polyfill')
10 | const customPloyfillGlobMatch = path.resolve(customHeaderPathPart, './**/*.js')
11 | const customPloyfill: string[] = glob.globSync(customPloyfillGlobMatch)
12 | const customPloyfillInfo = customPloyfill.map(str => {
13 | return {fullPath: str, ployfillPath: path.relative(customHeaderPathPart, str)}
14 | })
15 | // 内置 polyfill
16 | const urls = new URL(import.meta.url)
17 | const headerPathPart = path.resolve(path.dirname(urls.pathname), 'polyfill')
18 | const ployfillGlobMatch = path.resolve(headerPathPart, './**/*.js')
19 | let builtinPloyfill: string[] = glob.globSync(ployfillGlobMatch)
20 | const builtinPloyfillInfo = builtinPloyfill.map(str => {
21 | return {fullPath: str, ployfillPath: path.relative(headerPathPart, str)}
22 | })
23 | this.allPloyFills = [...customPloyfillInfo, ...builtinPloyfillInfo]
24 | }
25 |
26 | public findPloyfill(targetPath: string): PloyfillItem {
27 | return this.allPloyFills.find(item => {
28 | return targetPath.endsWith(item.ployfillPath)
29 | })
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/interface/unpack-wxapkg.ts:
--------------------------------------------------------------------------------
1 | import colors from "picocolors";
2 | import process from "node:process";
3 | import fs from "node:fs";
4 | import {findCommonRoot, getPathResolveInfo, printLog} from "@/utils/common";
5 | import {readLocalFile, saveLocalFile} from "@/utils/fs-process";
6 | import {MiniAppType, MiniPackType, UnPackInfo} from "@/type";
7 | import path from "node:path";
8 |
9 | /**
10 | * 用于解包
11 | * */
12 | export class UnpackWxapkg {
13 | /**
14 | * 获取包中的文件列表, 包含开始和结束的字节信息
15 | * */
16 | private static genFileList(__APP_BUF__: Buffer) {
17 | /* 将 header 的 buffer 数据单拎出来 */
18 | const headerBuffer = __APP_BUF__.subarray(0, 18)
19 | /* 获取头字节块起始标志, 固定值为 190 */
20 | let firstMark = headerBuffer.readUInt8(0);
21 | /* 获取文件开始的索引数据块位置,后面可通过该长度把源文件切出来 */
22 | let indexInfoLength = headerBuffer.readUInt32BE(5) + 18;
23 | /* 获取头字节块结束标志, 固定值为 237 */
24 | let lastMark = headerBuffer.readUInt8(13);
25 | /* 从 header 中读出当前包的文件数量 */
26 | let fileCount = headerBuffer.readUInt32BE(14);
27 | if (firstMark !== 0xbe || lastMark !== 0xed) {
28 | console.log(` \n\u274C ${colors.red(
29 | '这不是一个正确的小程序包,在微信3.8版本以下的 PC, MAC 包需要解密\n' +
30 | '所以你需要尝试先使用项目中的解密工具 decryption-tool/UnpackMiniApp.exe 解密')}\n地址: https://github.com/biggerstar/wedecode'`
31 | )
32 | process.exit(0)
33 | }
34 | /* 将保存文件索引位置的数据 buffer 切出来 */
35 | const indexBuf = __APP_BUF__.subarray(14, indexInfoLength)
36 | let fileList = [], offset = 4;
37 | /* 遍历文件列表, 取出每个文件的路径和占用大小,并写入到文件系统中, header 中每 12 个字节保存一个文件信息 */
38 | for (let i = 0; i < fileCount; i++) {
39 | const info: Record = {};
40 | const nameLen = indexBuf.readUInt32BE(offset);
41 | offset += 4;
42 | info.path = indexBuf.toString('utf8', offset, offset + nameLen);
43 | offset += nameLen;
44 | info.off = indexBuf.readUInt32BE(offset);
45 | offset += 4;
46 | info.size = indexBuf.readUInt32BE(offset);
47 | offset += 4;
48 | fileList.push(info);
49 | }
50 | if (!fileList.length) {
51 | printLog(colors.red('\u274C 未成功解压小程序包.'))
52 | process.exit(0)
53 | }
54 | return fileList
55 | }
56 |
57 | /**
58 | * 解析并保存该包中的所有文件
59 | * 返回获取该包各种信息 和 路径的操作对象
60 | * */
61 | public static async unpackWxapkg(inputPath: string, outputPath: string): Promise {
62 | const __APP_BUF__ = fs.readFileSync(inputPath)
63 | const fileList = []
64 | fileList.splice(0, fileList.length, ...UnpackWxapkg.genFileList(__APP_BUF__))
65 | const pathInfo = getPathResolveInfo(outputPath) // 这个后面在解压完包的时候会进行分包路径重置,并永远指向分包
66 | const outputPathInfo = getPathResolveInfo(outputPath) // 这个永远指向主包
67 | let packType: MiniPackType = 'sub'
68 | let appType: MiniAppType = 'app'
69 | let subPackRootPath = findCommonRoot(fileList.map(item => item.path))
70 | if (subPackRootPath) { // 重定向到子包目录
71 | pathInfo.setPackRootPath(subPackRootPath)
72 | }
73 | for (let info of fileList) {
74 | const fileName = info.path.startsWith("/") ? info.path.slice(1) : info.path
75 | const data = __APP_BUF__.subarray(info.off, info.off + info.size)
76 | /*------------------------------------------------*/
77 | const subRootPath = pathInfo.outputResolve(fileName)
78 | saveLocalFile(subRootPath, data)
79 | /*------------------------------------------------*/
80 | }
81 | const appConfigJsonString = readLocalFile(pathInfo.appConfigJsonPath)
82 | if (appConfigJsonString) { // 独立分包也拥有自己的 app-config
83 | const appConfig: Record = JSON.parse(appConfigJsonString)
84 | const foundThatSubPackages = (appConfig.subPackages || []).find((sub: any) => sub.root === `${subPackRootPath}/`)
85 | if (!foundThatSubPackages) {
86 | packType = 'main'
87 | } else if (typeof foundThatSubPackages === 'object' && foundThatSubPackages['independent']) {
88 | packType = 'independent'
89 | }
90 | }
91 | if (fs.existsSync(outputPathInfo.gameJsPath)) {
92 | appType = 'game'
93 | }
94 | printLog(`\n \u25B6 解小程序压缩包 ${colors.blue(path.basename(inputPath))} 成功! 文件总数: ${colors.green(fileList.length)}`, {isStart: true})
95 | return {
96 | appType,
97 | packType,
98 | subPackRootPath,
99 | pathInfo,
100 | outputPathInfo,
101 | inputPath,
102 | outputPath,
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/polyfill/@babel/runtime/helpers/typeof.js:
--------------------------------------------------------------------------------
1 | /*
2 | * 为了解决 TypeError: _typeofX is not a function 问题, 使用了注入该段代码, 这样只能解决部分问题
3 | * 但是默认有一劳永逸解决的方法,如果你遇到这该类型报错
4 | * 请按操作执行: 右上角点击“详情”=>“本地设置”=>“将JS编译成ES5”=>取消勾选
5 | * */
6 |
7 | function _typeof2(o) {
8 | return (_typeof2 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
9 | return typeof o;
10 | } : function (o) {
11 | return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
12 | })(o);
13 | }
14 |
15 | function _typeof(o) {
16 | return "function" == typeof Symbol && "symbol" === _typeof2(Symbol.iterator) ? module.exports = _typeof = function (o) {
17 | return _typeof2(o);
18 | } : module.exports = _typeof = function (o) {
19 | return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : _typeof2(o);
20 | }, _typeof(o);
21 | }
22 |
23 | module.exports = _typeof;
24 |
25 |
--------------------------------------------------------------------------------
/src/type/index.ts:
--------------------------------------------------------------------------------
1 | import {getPathResolveInfo} from "@/utils/common";
2 |
3 | export type AppCodeInfo = {
4 | appConfigJson: string;
5 | appWxss: string;
6 | workers: string;
7 | pageFrame: string;
8 | pageFrameHtml: string;
9 | appService: string;
10 | appServiceApp: string;
11 | }
12 |
13 | export type GameCodeInfo = {
14 | workers: string;
15 | gameJs: string;
16 | appConfigJson: string;
17 | subContextJs: string;
18 | }
19 | export type WxmlRenderFunction = {
20 | f: Function,
21 | j: any[],
22 | i: any[],
23 | ti: any[],
24 | ic: any[]
25 | }
26 | export type ModuleDefine = {
27 | /**
28 | * 包含所有的 wxml 组件渲染函数
29 | * */
30 | entrys: Record
31 | /**
32 | * 包含当前已经载入的模块和 wxs 映射关系, 不一定是完整的, 跟随页面加载会变化
33 | * */
34 | modules: Record | Function>
35 | /**
36 | * 包含所有的 wxml 组件定义
37 | * */
38 | defines: Record>
39 | }
40 | export type UnPackInfo = {
41 | /**
42 | * wxapkg 包的类型,主包 或者 分包 或者 独立分包
43 | * */
44 | packType: MiniPackType;
45 | /**
46 | * 小程序的类型, 小程序或者小游戏
47 | * */
48 | appType: MiniAppType;
49 | /**
50 | * 当前分包相对于主包根的路径
51 | * */
52 | subPackRootPath: string;
53 | /**
54 | * 永远指向分包的路径解析
55 | * */
56 | pathInfo: PathResolveInfo;
57 | /**
58 | * 永远指向主包的路径解析
59 | * */
60 | outputPathInfo: PathResolveInfo;
61 | /**
62 | * 后缀为 .wxapkg 的包路径
63 | * */
64 | inputPath: string;
65 | /**
66 | * 输出的文件夹路径
67 | * */
68 | outputPath: string;
69 | }
70 |
71 | export type ExecuteAllGwxFunction = {
72 | COMPONENTS: ModuleDefine;
73 | PLUGINS: Record;
74 | ALL_ENTRYS: ModuleDefine["entrys"];
75 | ALL_MODULES: ModuleDefine["modules"];
76 | ALL_DEFINES: ModuleDefine["defines"];
77 | }
78 |
79 | export type PloyfillItem = {
80 | fullPath: string,
81 | ployfillPath: string
82 | }
83 |
84 | export type PathResolveInfo = ReturnType
85 |
86 | export type MiniPackType = 'main' | 'sub' | 'independent' // 主包 | 分包 | 独立分包
87 | export type MiniAppType = 'app' | 'game'
88 |
89 | export enum PackTypeMapping {
90 | main = '主包',
91 | sub = '分包',
92 | independent = '独立分包', // 还是分包, 只是不依赖主包模块
93 | }
94 |
95 | export enum AppTypeMapping {
96 | app = '小程序',
97 | game = '小游戏',
98 | }
99 |
100 | export type SacnPackagesPathItem = {
101 | isAppId: boolean;
102 | appId: string;
103 | path: string;
104 | storagePath: string;
105 | }
106 |
107 | export type PackageInfoResult = {
108 | nickname: string,
109 | username: string,
110 | description: string,
111 | avatar: string,
112 | uses_count: string
113 | principal_name: string
114 | appid: string
115 | }
116 |
117 | export type ScanPackagesResultInfo = {
118 | /**
119 | * 小程序名称
120 | * */
121 | appName: string,
122 | /**
123 | * 小程序描述
124 | * */
125 | description: string,
126 | /**
127 | * 小程序的 APPID
128 | * */
129 | appId: string,
130 | /**
131 | * 小程序的名称路径根
132 | * */
133 | path: string
134 | /**
135 | * 真实的小程序存放路径
136 | * */
137 | storagePath: string
138 | }
139 |
140 | export type WxsRefInfo = Array<{
141 | wxsRender: Function, // wxs 渲染函数
142 | moduleName: boolean,
143 | inlineModuleName?: string,
144 | isInline: boolean
145 | wxsPath: string
146 | wxmlPath: string,
147 | templateList: string[]
148 | }>
149 |
150 | export type DecompilationControllerState = {
151 | /** 使用 px 单位解析 wxss */
152 | usePx: boolean,
153 | /** 仅解包 */
154 | unpackOnly: boolean,
155 | }
156 | export type ScanTableOptions = { columns: any[]; rows: any[] }
157 |
--------------------------------------------------------------------------------
/src/utils/common.ts:
--------------------------------------------------------------------------------
1 | import path from "node:path";
2 | import process from "node:process";
3 | import {stdout as slog} from 'single-line-log'
4 | import JS from 'js-beautify'
5 | import {isDev} from "@/bin/wedecode/enum";
6 |
7 | export function getPathResolveInfo(outputDir: string) {
8 | let _packRootPath = outputDir
9 | const resolve = (_new_resolve_path: string = './', ...args: string[]): string => {
10 | return path.resolve(outputDir, _packRootPath, _new_resolve_path, ...args)
11 | }
12 | const outputResolve = (_new_resolve_path: string = './', ...args: string[]): string => {
13 | return path.resolve(outputDir, _new_resolve_path, ...args)
14 | }
15 | return {
16 | /** 相对当前包( 子包, 主包, 插件包都算当前路径 )作为根路径路径进行解析 */
17 | resolve,
18 | /** 相对当前主包路径进行解析 */
19 | outputResolve,
20 | outputPath: outputDir,
21 | join(_path: string) {
22 | return path.join(_packRootPath, _path)
23 | },
24 | setPackRootPath(rootPath: string) {
25 | _packRootPath = rootPath
26 | },
27 | /**
28 | * 当前的包根路径, 主包为 ./ , 分包为相对主包根的相对路径
29 | * */
30 | get packRootPath() {
31 | return _packRootPath === outputDir ? './' : _packRootPath
32 | },
33 | get appJsonPath() {
34 | return resolve('app.json')
35 | },
36 | get appConfigJsonPath() {
37 | return resolve('app-config.json')
38 | },
39 | get projectPrivateConfigJsonPath() {
40 | return resolve('project.private.config.json')
41 | },
42 | get appWxssPath() {
43 | return resolve('app-wxss.js')
44 | },
45 | get workersPath() {
46 | return resolve('workers.js')
47 | },
48 | get pageFramePath() {
49 | return resolve('page-frame.js')
50 | },
51 | get pageFrameHtmlPath() {
52 | return resolve('page-frame.html')
53 | },
54 | get appJsPath() {
55 | return resolve('app.js')
56 | },
57 | get appServicePath() {
58 | return resolve('app-service.js')
59 | },
60 | get appServiceAppPath() {
61 | return resolve('appservice.app.js')
62 | },
63 | get gameJsonPath() {
64 | return resolve('game.json')
65 | },
66 | get gameJsPath() {
67 | return resolve('game.js')
68 | },
69 | get subContextJsPath() {
70 | return resolve('subContext.js')
71 | },
72 | }
73 | }
74 |
75 | export function jsBeautify(code: string) {
76 | return JS.js_beautify(code, {indent_size: 2})
77 | }
78 |
79 | /** 深度遍历 */
80 | export function traverseDOMTree(
81 | parentElement: HTMLElement | DocumentFragment,
82 | astVNode: Record,
83 | callback: (parentElement: HTMLElement | DocumentFragment, astVNode: Record) => any
84 | ) {
85 | if (!astVNode) return
86 | const newElement = callback(parentElement, astVNode);
87 | if (!newElement) return;
88 | const VNodeChildren = Array.from(astVNode.children).filter(Boolean)
89 | if (!VNodeChildren.length) return
90 | for (let i = 0; i < VNodeChildren.length; i++) {
91 | traverseDOMTree(newElement, VNodeChildren[i], callback);
92 | }
93 | }
94 |
95 | export function clearScreen() {
96 | process.stdout.write(process.platform === 'win32' ? '\x1Bc' : '\x1B[2J\x1B[3J\x1B[H');
97 | }
98 |
99 | export function limitPush(arr: any[], data: any, limit = 10) {
100 | if (arr.length - 1 > limit) arr.shift()
101 | arr.push(data)
102 | }
103 |
104 | const openStreamLog = false
105 | const excludesLogMatch = isDev
106 | ? [
107 | 'Completed'
108 | ]
109 | : []
110 |
111 | export function printLog(log: string, opt: {
112 | isStart?: boolean,
113 | isEnd?: boolean,
114 | endLimit?: number,
115 | starLimit?: number,
116 | middleLimit?: number
117 | space1?: string
118 | space2?: string
119 | nativeOnly?: boolean,
120 | interceptor?: (log: string) => any
121 | } = {}) {
122 | if (excludesLogMatch.some(item => log.includes(item))) return;
123 | if (!openStreamLog) {
124 | console.log(log)
125 | return;
126 | }
127 | if (!log || !log.trim()) return
128 | if (opt.interceptor) printLog['interceptor'] = opt.interceptor
129 | if (opt.space1) printLog['space1'] = opt.space1
130 | if (opt.space2) printLog['space2'] = opt.space2
131 | if (opt.nativeOnly) printLog['nativeOnly'] = opt.nativeOnly
132 | if (!printLog['middleLogList']) printLog['middleLogList'] = []
133 | if (!printLog['startLogList']) printLog['startLogList'] = []
134 | if (!printLog['endLogList']) printLog['endLogList'] = []
135 | if (typeof printLog['interceptor'] === "function" && (printLog['interceptor'](log) === false)) {
136 | return
137 | }
138 | if (printLog['nativeOnly']) {
139 | console.log.call(console, log)
140 | return;
141 | }
142 | if (opt.isStart) {
143 | limitPush(printLog['startLogList'], log, opt.starLimit || 20)
144 | } else if (opt.isEnd) {
145 | limitPush(printLog['endLogList'], log, opt.middleLimit || 6)
146 | } else {
147 | limitPush(printLog['middleLogList'], log, opt.endLimit || 20)
148 | }
149 | log = printLog['startLogList'].join('\n')
150 | + (printLog['space1'] || '')
151 | + printLog['middleLogList'].join('\n')
152 | + (printLog['space2'] || '')
153 | + printLog['endLogList'].join('\n')
154 | clearScreen()
155 | slog(log)
156 | }
157 |
158 | /**
159 | * 从数组中移除某个值
160 | * */
161 | export function removeElement(arr: T[], elementToRemove: T): void {
162 | const index = arr.indexOf(elementToRemove);
163 | if (index > -1) {
164 | arr.splice(index, 1);
165 | }
166 | }
167 |
168 | /**
169 | * 获取公共的最长目录
170 | * */
171 | export function commonDir(pathA: string, pathB: string) {
172 | if (pathA[0] === ".") pathA = pathA.slice(1);
173 | if (pathB[0] === ".") pathB = pathB.slice(1);
174 | pathA = pathA.replace(/\\/g, '/');
175 | pathB = pathB.replace(/\\/g, '/');
176 | let a = Math.min(pathA.length, pathB.length);
177 | for (let i = 1, m = Math.min(pathA.length, pathB.length); i <= m; i++) if (!pathA.startsWith(pathB.slice(0, i))) {
178 | a = i - 1;
179 | break;
180 | }
181 | let pub = pathB.slice(0, a);
182 | let len = pub.lastIndexOf("/") + 1;
183 | return pathA.slice(0, len);
184 | }
185 |
186 | /** 获取共同的最短根路径 */
187 | export function findCommonRoot(paths: string[]) {
188 | const splitPaths = paths.map(path => path.split('/').filter(Boolean));
189 | const commonRoot = [];
190 | for (let i = 0; i < splitPaths[0].length; i++) {
191 | const partsMatch = splitPaths.every(path => path[i] === splitPaths[0][i]);
192 | if (partsMatch) {
193 | commonRoot.push(splitPaths[0][i]);
194 | } else {
195 | break;
196 | }
197 | }
198 | return commonRoot.join('/')
199 | }
200 |
201 | export function replaceExt(name: string, ext = "") {
202 | const hasSuffix = name.lastIndexOf(".") > 2 // x.x
203 | return hasSuffix ? name.slice(0, name.lastIndexOf(".")) + ext : `${name}${ext}`
204 | }
205 |
206 | export function sleep(time: number) {
207 | return new Promise(resolve1 => setTimeout(resolve1, time))
208 | }
209 |
210 | /**
211 | * 数组去重, 回调函数返回布尔值,代表本次的成员是否添加到数组中, 返回 true 允许加入, 反之
212 | * 如果未传入回调函数, 将默认去重
213 | * */
214 | export function arrayDeduplication(arr: T[], cb?: (pre: T[], cur: T) => boolean): T[] {
215 | return arr.reduce((pre: T[], cur: T) => {
216 | const res = cb ? cb(pre, cur) : void 0
217 | const isRes = typeof res === 'boolean'
218 | isRes ? res && pre.push(cur) : (!pre.includes(cur) && pre.push(cur))
219 | return pre
220 | }, [])
221 | }
222 |
223 | export function removeVM2ExceptionLine(code: string) {
224 | const reg = /\s*[a-z]\x20?=\x20?VM2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL\.handleException\([a-z]\);?/g
225 | return code.replace(reg, '')
226 | }
227 |
228 | export function resetWxsRequirePath(p: string, resetString: string = '') {
229 | return p.replaceAll('p_./', resetString).replaceAll('m_./', resetString)
230 | }
231 |
232 | export function isPluginPath(path: string) {
233 | return path.startsWith('plugin-private://') || path.startsWith('plugin://')
234 | }
235 |
236 | export function resetPluginPath(_path: string, prefixDir: string | null | void) {
237 | return path.join(
238 | prefixDir || './',
239 | _path.replaceAll('plugin-private://', '').replaceAll('plugin://', ''),
240 | )
241 | }
242 |
243 | /**
244 | * 获取某个函数的入参定义的名称
245 | * */
246 | export function getParameterNames(fn: Function) {
247 | if (typeof fn !== 'function') return [];
248 | const COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
249 | const code = fn.toString().replace(COMMENTS, '');
250 | const result = code.slice(code.indexOf('(') + 1, code.indexOf(')'))
251 | .match(/([^\s,]+)/g);
252 | return result === null
253 | ? []
254 | : result;
255 | }
256 |
257 | /**
258 | * 判断是否是wx 的 appid
259 | * */
260 | export function isWxAppid(str: string) {
261 | const reg = /^wx[0-9a-f]{16}$/i
262 | str = str.trim()
263 | return str.length === 18 && reg.test(str)
264 | }
265 |
--------------------------------------------------------------------------------
/src/utils/create-vm.ts:
--------------------------------------------------------------------------------
1 | import { VM, VMOptions } from "vm2";
2 | import { JSDOM } from "jsdom";
3 | import { deepmerge } from "@biggerstar/deepmerge";
4 | import { createWxFakeDom } from "./wx-dom";
5 |
6 | export function createVM(vmOptions: VMOptions = {}) {
7 | const domBaseHtml = `''`
8 | const dom = new JSDOM(domBaseHtml);
9 | const vm_window = dom.window
10 | const vm_navigator = dom.window.navigator
11 | const vm_document = dom.window.document
12 | const __wxAppCode__ = {}
13 | const fakeGlobal = {
14 | __wxAppCode__,
15 | publishDomainComponents: () => void 0,
16 | }
17 | Object.assign(vm_window, fakeGlobal)
18 | return new VM(deepmerge({
19 | sandbox: {
20 | ...createWxFakeDom(),
21 | setInterval: () => null,
22 | setTimeout: () => null,
23 | window: vm_window,
24 | location: dom.window.location,
25 | navigator: vm_navigator,
26 | document: vm_document,
27 | define: () => void 0,
28 | require: () => void 0,
29 | requirePlugin: () => void 0,
30 | global: {
31 | __wcc_version__: 'v0.5vv_20211229_syb_scopedata',
32 | },
33 | System: {
34 | register: () => void 0,
35 | },
36 | __vd_version_info__: {},
37 | __wxAppCode__,
38 | __wxCodeSpace__: {
39 | setRuntimeGlobals: () => void 0,
40 | addComponentStaticConfig: () => void 0,
41 | setStyleScope: () => void 0,
42 | enableCodeChunk: () => void 0,
43 | addTemplateDependencies: () => void 0,
44 | batchAddCompiledScripts: () => void 0,
45 | batchAddCompiledTemplate: () => void 0,
46 | },
47 | }
48 | }, vmOptions));
49 | }
50 |
51 | export function runVmCode(vm: VM, code: string) {
52 | try {
53 | vm.run(code)
54 | } catch (e) {
55 | console.log(e.message)
56 | }
57 | }
--------------------------------------------------------------------------------
/src/utils/decompile-wxml.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 本项目遵循 GPL-3.0 开源协议
3 | * 本段代码引用自 https://github.com/qwerty472123/wxappUnpacker ,并做了一定修改优化
4 | * */
5 |
6 | import esprima from "esprima";
7 | import escodegen from "escodegen";
8 |
9 | function analyze(core: any, z: any, namePool: Record, xPool: Record, fakePool = {}, zMulName = "0") {
10 | function anaRecursion(core: any, fakePool = {}) {
11 | return analyze(core, z, namePool, xPool, fakePool, zMulName);
12 | }
13 |
14 | function push(name: string, elem: any) {
15 | namePool[name] = elem;
16 | }
17 |
18 | function pushSon(pname: string, son: any) {
19 | // console.log(pname, son)
20 | if (fakePool[pname]) fakePool[pname].son.push(son);
21 | else namePool[pname].son.push(son);
22 | }
23 |
24 | for (let ei = 0; ei < core.length; ei++) {
25 | let e = core[ei];
26 | switch (e.type) {
27 | case "ExpressionStatement": {
28 | let f = e.expression;
29 | if (f.callee) {
30 | if (f.callee.type == "Identifier") {
31 | switch (f.callee.name) {
32 | case "_r":
33 | namePool[f.arguments[0].name].v[f.arguments[1].value] = z[f.arguments[2].value];
34 | break;
35 | case "_rz":
36 | namePool[f.arguments[1].name].v[f.arguments[2].value] = z[zMulName][f.arguments[3].value];
37 | break;
38 | case "_": // 标签属性
39 | // 放入子层级
40 | pushSon(f.arguments[0].name, namePool[f.arguments[1].name]);
41 | break;
42 | case "_2": {
43 | let item = f.arguments[6].value;//def:item
44 | let index = f.arguments[7].value;//def:index
45 | let data = z[f.arguments[0].value];
46 | let key = escodegen.generate(f.arguments[8]).slice(1, -1);//f.arguments[8].value;//def:""
47 | let obj = namePool[f.arguments[5].name];
48 | let gen = namePool[f.arguments[1].name];
49 | if (gen.tag == "gen") {
50 | let ret = gen.func.body.body.pop().argument.name;
51 | anaRecursion(gen.func.body.body, {[ret]: obj});
52 | }
53 | obj.v["wx:for"] = data;
54 | if (index != "index") obj.v["wx:for-index"] = index;
55 | if (item != "item") obj.v["wx:for-item"] = item;
56 | if (key != "") obj.v["wx:key"] = key;
57 | }
58 | break;
59 | case "_2z": {
60 | let item = f.arguments[7].value;//def:item
61 | let index = f.arguments[8].value;//def:index
62 | let data = z[zMulName][f.arguments[1].value];
63 | let key = escodegen.generate(f.arguments[9]).slice(1, -1);//f.arguments[9].value;//def:""
64 | let obj = namePool[f.arguments[6].name];
65 | let gen = namePool[f.arguments[2].name];
66 | if (gen.tag == "gen") {
67 | let ret = gen.func.body.body.pop().argument.name;
68 | anaRecursion(gen.func.body.body, {[ret]: obj});
69 | }
70 | obj.v["wx:for"] = data;
71 | if (index != "index") obj.v["wx:for-index"] = index;
72 | if (item != "item") obj.v["wx:for-item"] = item;
73 | if (key != "") obj.v["wx:key"] = key;
74 | }
75 | break;
76 | case "_ic":
77 | pushSon(f.arguments[5].name, {
78 | tag: "include",
79 | son: [],
80 | v: {src: xPool[f.arguments[0].property.value]}
81 | });
82 | break;
83 | case "_ai": {//template import
84 | let to = Object.keys(fakePool)[0];
85 | if (to) pushSon(to, {
86 | tag: "import",
87 | son: [],
88 | v: {src: xPool[f.arguments[1].property.value]}
89 | });
90 | else throw Error("Unexpected fake pool");
91 | }
92 | break;
93 | case "_af":
94 | //ignore _af
95 | break;
96 | default:
97 | throw Error("Unknown expression callee name " + f.callee.name);
98 | }
99 | } else if (f.callee.type == "MemberExpression") {
100 | if (f.callee.object.name == "cs" || f.callee.property.name == "pop") break;
101 | throw Error("Unknown member expression");
102 | } else throw Error("Unknown callee type " + f.callee.type);
103 | } else if (f.type == "AssignmentExpression" && f.operator == "=") {
104 | //no special use
105 | } else throw Error("Unknown expression statement.");
106 | break;
107 | }
108 | case "VariableDeclaration":
109 | for (let dec of e.declarations) {
110 | if (dec.init.type == "CallExpression") {
111 | switch (dec.init.callee.name) {
112 | case "_n":
113 | let tagName = dec.init.arguments[0].value
114 | if (['wx-scope'].includes(tagName)) {
115 | tagName = 'view'
116 | }
117 | push(dec.id.name, {tag: tagName, son: [], v: {}});
118 | break;
119 | case "_v":
120 | push(dec.id.name, {tag: "block", son: [], v: {}});
121 | break;
122 | case "_o":
123 | push(dec.id.name, {
124 | tag: "__textNode__",
125 | textNode: true,
126 | content: z[dec.init.arguments[0].value]
127 | });
128 | break;
129 | case "_oz":
130 | push(dec.id.name, {
131 | tag: "__textNode__",
132 | textNode: true,
133 | content: z[zMulName][dec.init.arguments[1].value]
134 | });
135 | break;
136 | case "_m": {
137 | if (dec.init.arguments[2].elements.length > 0) {
138 | throw Error("Noticable generics content: " + dec.init.arguments[2].toString());
139 | }
140 | let mv = {};
141 | let name = null, base = 0;
142 | for (let x of dec.init.arguments[1].elements) {
143 | let v = x.value;
144 | if (!v && typeof v != "number") {
145 | if (x.type == "UnaryExpression" && x.operator == "-") v = -x.argument.value;
146 | else throw Error("Unknown type of object in _m attrs array: " + x.type);
147 | }
148 | if (name === null) {
149 | name = v;
150 | } else {
151 | if (base + v < 0) mv[name] = null; else {
152 | mv[name] = z[base + v];
153 | if (base == 0) base = v;
154 | }
155 | name = null;
156 | }
157 | }
158 | push(dec.id.name, {tag: dec.init.arguments[0].value, son: [], v: mv});
159 | }
160 | break;
161 | case "_mz": {
162 | if (dec.init.arguments[3].elements.length > 0) {
163 | throw Error("Noticable generics content: " + dec.init.arguments[3].toString());
164 | }
165 | let mv = {};
166 | let name = null, base = 0;
167 | for (let x of dec.init.arguments[2].elements) {
168 | let v = x.value;
169 | if (!v && typeof v != "number") {
170 | if (x.type == "UnaryExpression" && x.operator == "-") v = -x.argument.value;
171 | else throw Error("Unknown type of object in _mz attrs array: " + x.type);
172 | }
173 | if (name === null) {
174 | name = v;
175 | } else {
176 | if (base + v < 0) mv[name] = null; else {
177 | mv[name] = z[zMulName][base + v];
178 | if (base == 0) base = v;
179 | }
180 | name = null;
181 | }
182 | }
183 | push(dec.id.name, {tag: dec.init.arguments[1].value, son: [], v: mv});
184 | }
185 | break;
186 | case "_gd"://template use/is
187 | {
188 | let is = namePool[dec.init.arguments[1].name].content;
189 | let data = null, obj = null;
190 | ei++;
191 | for (let e of core[ei].consequent.body) {
192 | if (e.type == "VariableDeclaration") {
193 | for (let f of e.declarations) {
194 | if (f.init.type == "LogicalExpression" && f.init.left.type == "CallExpression") {
195 | if (f.init.left.callee.name == "_1") data = z[f.init.left.arguments[0].value];
196 | else if (f.init.left.callee.name == "_1z") data = z[zMulName][f.init.left.arguments[1].value];
197 | if (data.startsWith('{{({') && data.endsWith('})}}')) {
198 | // 将在 getZ 的 scope 函数中为普通对象定义的表达式解析恢复为双括号,因为 template标签 中可以直接在{{}} 中放置对象, 无需再使用() 包裹
199 | data = `{{${data.substring(4, data.length - 4)}}}`
200 | }
201 | }
202 | }
203 | } else if (e.type == "ExpressionStatement") {
204 | let f = e.expression;
205 | if (f.type == "AssignmentExpression" && f.operator == "=" && f.left.property && f.left.property.name == "wxXCkey") {
206 | obj = f.left.object.name;
207 | }
208 | }
209 | }
210 | namePool[obj].tag = "template";
211 | Object.assign(namePool[obj].v, {is: is, data: data});
212 | }
213 | break;
214 | default: {
215 | let funName = dec.init.callee.name;
216 | if (funName.startsWith("gz$gwx")) {
217 | zMulName = funName.slice(6);
218 | } else throw Error("Unknown init callee " + funName);
219 | }
220 | }
221 | } else if (dec.init.type == "FunctionExpression") {
222 | push(dec.id.name, {tag: "gen", func: dec.init});
223 | } else if (dec.init.type == "MemberExpression") {
224 | if (dec.init.object.type == "MemberExpression" && dec.init.object.object.name == "e_" && dec.init.object.property.type == "MemberExpression" && dec.init.object.property.object.name == "x") {
225 | if (dec.init.property.name == "j") {//include
226 | //do nothing
227 | } else if (dec.init.property.name == "i") {//import
228 | //do nothing
229 | } else throw Error("Unknown member expression declaration.");
230 | } else throw Error("Unknown member expression declaration.");
231 | } else throw Error("Unknown declaration init type " + dec.init.type);
232 | }
233 | break;
234 | case "IfStatement":
235 | if (e.test.callee.name.startsWith("_o")) {
236 | function parse_OFun(e) {
237 | if (e.test.callee.name == "_o") return z[e.test.arguments[0].value];
238 | else if (e.test.callee.name == "_oz") return z[zMulName][e.test.arguments[1].value];
239 | else throw Error("Unknown if statement test callee name:" + e.test.callee.name);
240 | }
241 |
242 | let vname = e.consequent.body[0].expression.left.object.name;
243 | let nif = {tag: "block", v: {"wx:if": parse_OFun(e)}, son: []};
244 | anaRecursion(e.consequent.body, {[vname]: nif});
245 | pushSon(vname, nif);
246 | if (e.alternate) {
247 | while (e.alternate && e.alternate.type == "IfStatement") {
248 | e = e.alternate;
249 | //@ts-ignore
250 | nif = {tag: "block", v: {"wx:elif": parse_OFun(e)}, son: []};
251 | anaRecursion(e.consequent.body, {[vname]: nif});
252 | pushSon(vname, nif);
253 | }
254 | if (e.alternate && e.alternate.type == "BlockStatement") {
255 | e = e.alternate;
256 | //@ts-ignore
257 | nif = {tag: "block", v: {"wx:else": null}, son: []};
258 | anaRecursion(e.body, {[vname]: nif});
259 | pushSon(vname, nif);
260 | }
261 | }
262 | } else {
263 | throw Error("Unknown if statement.");
264 | }
265 | break;
266 | default:
267 | throw Error("Unknown type " + e.type);
268 | }
269 | }
270 | }
271 |
272 | function wxmlify(str: string | String, isText?: boolean) {
273 | if (typeof str == "undefined" || str === null) return "Empty";//throw Error("Empty str in "+(isText?"text":"prop"));
274 | if (isText) return str;//may have some bugs in some specific case(undocumented by tx)
275 | else return str.replace(/"/g, '\\"');
276 | }
277 |
278 | function elemToString(elem: Record, dep: any) {
279 | const longerList = [];//put tag name which can't be style.
280 | const indent = ' '.repeat(4);
281 |
282 | function isTextTag(elem: Record) {
283 | return elem.tag == "__textNode__" && elem.textNode;
284 | }
285 |
286 | function elemRecursion(elem: Record, dep: any) {
287 | return elemToString(elem, dep);
288 | }
289 |
290 | function trimMerge(rets: any) {
291 | let needTrimLeft = false, ans = "";
292 | for (let ret of rets) {
293 | if (ret.textNode == 1) {
294 | if (!needTrimLeft) {
295 | needTrimLeft = true;
296 | ans = ans.trimEnd();
297 | }
298 | } else if (needTrimLeft) {
299 | needTrimLeft = false;
300 | ret = ret.trimStart();
301 | }
302 | ans += ret;
303 | }
304 | return ans;
305 | }
306 |
307 | if (isTextTag(elem)) {
308 | //In comment, you can use typify text node, which beautify its code, but may destroy ui.
309 | //So, we use a "hack" way to solve this problem by letting typify program stop when face textNode\
310 | const StringC = String;
311 | String()
312 | let str = new StringC(wxmlify(elem.content, true));
313 | str['textNode'] = 1;
314 | return wxmlify(str, true);//indent.repeat(dep)+wxmlify(elem.content.trim(),true)+"\n";
315 | }
316 | if (elem.tag == "block") {
317 | if (elem.son.length == 1 && !isTextTag(elem.son[0])) {
318 | let ok = true, s = elem.son[0];
319 | for (let x in elem.v) if (x in s.v) {
320 | ok = false;
321 | break;
322 | }
323 | if (ok && !(("wx:for" in s.v || "wx:if" in s.v) && ("wx:if" in elem.v || "wx:else" in elem.v || "wx:elif" in elem.v))) {//if for and if in one tag, the default result is an if in for. And we should block if nested in elif/else been combined.
324 | Object.assign(s.v, elem.v);
325 | return elemRecursion(s, dep);
326 | }
327 | } else if (Object.keys(elem.v).length == 0) {
328 | let ret = [];
329 | for (let s of elem.son) ret.push(elemRecursion(s, dep));
330 | return trimMerge(ret);
331 | }
332 | }
333 | let ret = indent.repeat(dep) + "<" + elem.tag;
334 | for (let attr in elem.v) {
335 | if (attr.toString().trim().startsWith("wx:") && typeof elem.v[attr] == "string") {
336 | if (elem.v[attr].startsWith("{{({") && elem.v[attr].endsWith("})}}")) {
337 | const data = elem.v[attr].slice(4, elem.v[attr].length - 4)
338 | if (!data.includes(",") && data.split(":").length == 2) {
339 | // example {{uuid:uuid}}
340 | elem.v[attr] = `{{${data}}}`;
341 | }
342 | }
343 | }
344 | ret += " " + attr + (elem.v[attr] !== null ? "=\"" + wxmlify(elem.v[attr]) + "\"" : "");
345 | }
346 | if (elem.son.length == 0) {
347 | if (longerList.includes(elem.tag)) return ret + " />\n";
348 | else return ret + ">" + elem.tag + ">\n";
349 | }
350 | ret += ">\n";
351 | let rets = [ret];
352 | for (let s of elem.son) rets.push(elemRecursion(s, dep + 1));
353 | rets.push(indent.repeat(dep) + "" + elem.tag + ">\n");
354 | return trimMerge(rets);
355 | }
356 |
357 | function genReferenceTemplate(z: Record, defineRef: Record) {
358 | const state = []
359 | const result = [];
360 | for (let v in defineRef) {
361 | // template 引用定义
362 | state[0] = v;
363 | let oriCode = defineRef[v].toString();
364 | let rName = oriCode.slice(oriCode.lastIndexOf("return") + 6).replace(/[;}]/g, "").trim();
365 | let tryPtr = oriCode.indexOf("\ntry{");
366 | let zPtr = oriCode.indexOf("var z=gz$gwx");
367 | let code = oriCode.slice(tryPtr + 5, oriCode.lastIndexOf("\n}catch(")).trim();
368 | if (zPtr != -1 && tryPtr > zPtr) {
369 | let attach = oriCode.slice(zPtr);
370 | attach = attach.slice(0, attach.indexOf("()")) + "()\n";
371 | code = attach + code;
372 | }
373 | let r = {tag: "template", v: {name: v}, son: []};
374 | analyze(esprima.parseScript(code).body, z, {[rName]: r}, {[rName]: r});
375 | result.push(elemToString(r, 0));
376 | }
377 | return result.join("");
378 | }
379 |
380 | function getDecompiledWxml(code: string, z: Record, xPool: string[]) {
381 | let rName = code.slice(code.lastIndexOf("return") + 6).replace(/[;}]/g, "").trim();
382 | code = code.slice(code.indexOf("\n"), code.lastIndexOf("return")).trim();
383 | let r = {son: []};
384 | const namePool = {[rName]: r}
385 | const fakePool = {[rName]: r}
386 | analyze(esprima.parseScript(code).body, z, namePool, xPool, fakePool);
387 | let ans = [];
388 | for (let elem of r.son) ans.push(elemToString(elem, 0));
389 | return ans.join("")
390 | }
391 |
392 | export function tryDecompileWxml(f_func_code: string, z: Record, define: any, xPool: string[]): string {
393 | try {
394 | return getDecompiledWxml(f_func_code, z, xPool) + genReferenceTemplate(z, define)
395 | } catch (e) {
396 | console.log('[tryDecompileWxml]', e.message)
397 | return ''
398 | }
399 | }
400 |
401 |
--------------------------------------------------------------------------------
/src/utils/fs-process.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 | import path from "node:path";
3 | import type { RmOptions } from "fs";
4 | import { pluginDirRename } from "@/constant";
5 |
6 | /**
7 | * 读取文件,没有文件或者文件为空返回空字符串
8 | * */
9 | export function readLocalFile(path: string, encoding: BufferEncoding = 'utf-8'): string {
10 | return fs.existsSync(path) ? fs.readFileSync(path, encoding) : ''
11 | }
12 |
13 | /**
14 | * 读取文件,没有文件或者文件为空返回 null
15 | * */
16 | export function readLocalJsonFile>(path: string, encoding: BufferEncoding = 'utf-8'): T | null {
17 | try {
18 | return JSON.parse(readLocalFile(path, encoding))
19 | } catch (e) {
20 | return null
21 | }
22 | }
23 |
24 | /**
25 | * 顺序读取列表中的文件, 直到读取的文件包含内容
26 | * */
27 | export function readFileUntilContainContent(pathList: string[], encoding: BufferEncoding = 'utf-8'): {
28 | data: string,
29 | found: boolean,
30 | path: string
31 | } {
32 | for (const filePath of pathList) {
33 | if (fs.existsSync(filePath)) {
34 | const data = fs.readFileSync(filePath, encoding)
35 | if (data.length) {
36 | return {
37 | found: true,
38 | data,
39 | path: filePath
40 | }
41 | }
42 | }
43 | }
44 | return {
45 | found: false,
46 | data: '',
47 | path: ''
48 | }
49 | }
50 |
51 | /**
52 | * @param {string} filepath
53 | * @param {any} data
54 | * @param {Object} opt
55 | * @param {boolean} opt.force 是否强制覆盖, 默认为 false
56 | * @param {boolean} opt.emptyInstead 如果文原始件为空则允许覆盖
57 | * */
58 | export function saveLocalFile(
59 | filepath: string,
60 | data: string | NodeJS.ArrayBufferView | Buffer,
61 | opt: { force?: boolean, emptyInstead?: boolean } = {}
62 | ): boolean {
63 | filepath = filepath.replace(pluginDirRename[0], pluginDirRename[1]) // 重定向插件路径
64 | const targetData = fs.existsSync(filepath) ? fs.readFileSync(filepath, { encoding: 'utf-8' }).trim() : ''
65 | let force = typeof opt.force === 'boolean' ? opt.force : opt.emptyInstead || !targetData.length
66 | const outputDirPath = path.dirname(filepath)
67 | const isExistsFile = fs.existsSync(filepath)
68 | const isExistsPath = fs.existsSync(outputDirPath)
69 | if (isExistsFile && !force) return false
70 | if (!isExistsPath) {
71 | fs.mkdirSync(outputDirPath, { recursive: true })
72 | }
73 | fs.writeFileSync(filepath, data as any)
74 | return true
75 | }
76 |
77 | export function deleteLocalFile(path: string, opt: RmOptions & { catch?: boolean } = {}): void {
78 | try {
79 | fs.rmSync(path, opt)
80 | } catch (e) {
81 | if (!opt.catch) throw e
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/utils/get-pack-codeInfo.ts:
--------------------------------------------------------------------------------
1 | import * as cheerio from "cheerio";
2 | import {AppCodeInfo, GameCodeInfo, PathResolveInfo} from "@/type";
3 | import {readLocalFile} from "@/utils/fs-process";
4 |
5 | /**
6 | * 获取 APP 包中主要的一些代码文件
7 | * @param pathInfo
8 | * @param opt
9 | * @param opt.adaptLen 小于该长度的内容认为空
10 | * */
11 | export function getAppPackCodeInfo(pathInfo: PathResolveInfo, opt: { adaptLen?: number } = {}): AppCodeInfo {
12 | const {adaptLen = 100} = opt || {}
13 |
14 | function __readFile(path: string) {
15 | if (!path) return ''
16 | const content = readLocalFile(path)
17 | return content.length > adaptLen ? content : ''
18 | }
19 |
20 | let pageFrameHtmlCode = __readFile(pathInfo.pageFrameHtmlPath)
21 | if (pageFrameHtmlCode) {
22 | const $ = cheerio.load(pageFrameHtmlCode);
23 | pageFrameHtmlCode = $('script').text()
24 | }
25 | const appServiceCode = __readFile(pathInfo.appServicePath)
26 | const appServiceAppCode = __readFile(pathInfo.appServiceAppPath)
27 | return {
28 | appConfigJson: __readFile(pathInfo.appConfigJsonPath),
29 | appWxss: __readFile(pathInfo.appWxssPath),
30 | appService: appServiceCode,
31 | appServiceApp: appServiceAppCode,
32 | pageFrame: __readFile(pathInfo.pageFramePath),
33 | workers: __readFile(pathInfo.workersPath),
34 | pageFrameHtml: pageFrameHtmlCode,
35 | }
36 | }
37 |
38 | /**
39 | * 获取 GAME 包中主要的一些代码文件
40 | * */
41 | export function getGamePackCodeInfo(pathInfo: PathResolveInfo, opt: { adaptLen?: number } = {}): GameCodeInfo {
42 | const {adaptLen = 100} = opt || {}
43 |
44 | function __readFile(path: string) {
45 | if (!path) return ''
46 | const content = readLocalFile(path)
47 | return content.length > adaptLen ? content : ''
48 | }
49 |
50 | return {
51 | workers: __readFile(pathInfo.workersPath),
52 | gameJs: __readFile(pathInfo.gameJsPath),
53 | appConfigJson: __readFile(pathInfo.appConfigJsonPath),
54 | subContextJs: __readFile(pathInfo.subContextJsPath),
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/utils/get-z.ts:
--------------------------------------------------------------------------------
1 | import { createVM } from "./create-vm";
2 |
3 | function parseParenthesesTyping(str: string): 'single' | 'double' | 'multiple' {
4 | str = str.trim()
5 | const sameObject = str.startsWith('{') && str.endsWith('}')
6 | const sameArray = str.startsWith('[') && str.endsWith(']')
7 | const hasSpreading = (sameArray || sameObject) && str.includes('...')
8 | if (sameObject &&
9 | !hasSpreading &&
10 | str.split(':').length === 2
11 | ) {
12 | // {uuid:uuid}
13 | return 'single'
14 | } else if (sameObject || sameArray) {
15 | // {uuid:uuid, ...val}
16 | return 'multiple'
17 | } else {
18 | return 'double'
19 | }
20 | }
21 |
22 | function restoreSingle(ops: any, withScope = false) {
23 | if (typeof ops == "undefined") return "";
24 |
25 | function scope(value: string) {
26 | if (withScope) return value;
27 | // const typing = parseParenthesesTyping(value);
28 | // if (typing === 'single') return "{" + value + "}";
29 | // else if (typing === 'multiple') return "{{(" + value + ")}}";
30 | // else return "{{" + value + "}}";
31 | // if (value.includes('cont:cont,mkAppear:mkAppear')) {
32 | // console.log("🚀 ~ scope ~ value:", value)
33 | // }
34 | if (value.startsWith('{') && value.endsWith('}')) return "{{(" + value + ")}}";
35 | if (value.startsWith('...')) return "{" + value.substring(3) + "}";
36 | return "{{" + value + "}}";
37 | }
38 |
39 | function enBrace(value: string, type = '{') {
40 | if (value.startsWith('{') ||
41 | value.startsWith('[') ||
42 | value.startsWith('(') ||
43 | value.endsWith('}') ||
44 | value.endsWith(']') ||
45 | value.endsWith(')')
46 | ) {
47 | value = ' ' + value + ' '
48 | }
49 | // console.log(type, value)
50 | switch (type) {
51 | case '{':
52 | return '{' + value + '}';
53 | case '[':
54 | return '[' + value + ']';
55 | case '(':
56 | return '(' + value + ')';
57 | default:
58 | throw Error("Unknown brace type " + type);
59 | }
60 | }
61 |
62 | function restoreNext(ops: any, w = withScope) {
63 | return restoreSingle(ops, w);
64 | }
65 |
66 | function jsoToWxOn(obj: any) {//convert JS Object to WeChat Object Notation(No quotes@key+str)
67 | let ans = "";
68 | if (typeof obj === "undefined") {
69 | return 'undefined';
70 | } else if (obj === null) {
71 | return 'null';
72 | } else if (obj instanceof RegExp) {
73 | return obj.toString();
74 | } else if (obj instanceof Array) {
75 | for (let i = 0; i < obj.length; i++) ans += ',' + jsoToWxOn(obj[i]);
76 | return enBrace(ans.slice(1), '[');
77 | } else if (typeof obj == "object") {
78 | for (let k in obj) ans += "," + k + ":" + jsoToWxOn(obj[k]);
79 | return enBrace(ans.slice(1), '{');
80 | } else if (typeof obj == "string") {
81 | let parts = obj.split('"'), ret = [];
82 | for (let part of parts) {
83 | let atoms = part.split("'"), ans = [];
84 | for (let atom of atoms) ans.push(JSON.stringify(atom).slice(1, -1));
85 | ret.push(ans.join("\\'"));
86 | }
87 | return "'" + ret.join('"') + "'";
88 | } else return JSON.stringify(obj);
89 | }
90 |
91 | let op = ops[0];
92 | if (!Array.isArray(op)) {
93 | switch (op) {
94 | case 3://string
95 | return ops[1];//may cause problems if wx use it to be string
96 | case 1://direct value
97 | const val = jsoToWxOn(ops[1])
98 | return scope(val);
99 | case 11://values list, According to var a = 11;
100 | let ans = "";
101 | ops.shift();
102 | for (let perOp of ops) ans += restoreNext(perOp);
103 | return ans;
104 | }
105 | } else {
106 | let ans: string = "";
107 | switch (op[0]) {//vop
108 | case 2://arithmetic operator
109 | {
110 | function getPrior(op: number, len: number) {
111 | const priorList = {
112 | "?:": 4,
113 | "&&": 6,
114 | "||": 5,
115 | "+": 13,
116 | "*": 14,
117 | "/": 14,
118 | "%": 14,
119 | "|": 7,
120 | "^": 8,
121 | "&": 9,
122 | "!": 16,
123 | "~": 16,
124 | "===": 10,
125 | "==": 10,
126 | "!=": 10,
127 | "!==": 10,
128 | ">=": 11,
129 | "<=": 11,
130 | ">": 11,
131 | "<": 11,
132 | "<<": 12,
133 | ">>": 12,
134 | "-": len === 3 ? 13 : 16
135 | };
136 | return priorList[op] ? priorList[op] : 0;
137 | }
138 |
139 | function getOp(i: number) {
140 | let ret = restoreNext(ops[i], true);
141 | if (ops[i] instanceof Object && typeof ops[i][0] == "object" && ops[i][0][0] === 2) {
142 | //Add brackets if we need
143 | if (getPrior(op[1], ops.length) > getPrior(ops[i][0][1], ops[i].length)) ret = enBrace(ret, '(');
144 | }
145 | return ret;
146 | }
147 |
148 | switch (op[1]) {
149 | case "?:":
150 | ans = getOp(1) + "?" + getOp(2) + ":" + getOp(3);
151 | break;
152 | case "!":
153 | case "~":
154 | ans = op[1] + getOp(1);
155 | break;
156 | // @ts-ignore
157 | case "-":
158 | if (ops.length !== 3) {
159 | ans = op[1] + getOp(1);
160 | break;
161 | }//should not add more in there![fall through]
162 | default:
163 | ans = getOp(1) + op[1] + getOp(2);
164 | }
165 | break;
166 | }
167 | case 4: // unkown-arrayStart? 将操作符下 数组 拼接数组成字符串形式
168 | ans = restoreNext(ops[1], true);
169 | break;
170 | case 5: // merge-array
171 | {
172 | switch (ops.length) {
173 | case 2:
174 | ans = enBrace(restoreNext(ops[1], true), '[');
175 | break;
176 | case 1:
177 | ans = '[]';
178 | break;
179 | default: {
180 | let a = restoreNext(ops[1], true);
181 | //console.log(a,a.startsWith('[')&&a.endsWith(']'));
182 | if (a.startsWith('[') && a.endsWith(']')) {
183 | if (a !== '[]') {
184 | ans = enBrace(a.slice(1, -1).trim() + ',' + restoreNext(ops[2], true), '[');
185 | //console.log('-',a);
186 | } else {
187 | ans = enBrace(restoreNext(ops[2], true), '[');
188 | }
189 | } else {
190 | ans = enBrace('...' + a + ',' + restoreNext(ops[2], true), '[');//may/must not support in fact
191 | }
192 | }
193 | }
194 | break;
195 | }
196 | case 6://get value of an object
197 | {
198 | let sonName = restoreNext(ops[2], true);
199 | if (sonName._type === "var") {
200 | ans = restoreNext(ops[1], true) + enBrace(sonName, '[');
201 | } else {
202 | let attach = "";
203 | if (/^[A-Za-z\_][A-Za-z\d\_]*$/.test(sonName)/*is a qualified id*/)
204 | attach = '.' + sonName;
205 | else attach = enBrace(sonName, '[');
206 | ans = restoreNext(ops[1], true) + attach;
207 | }
208 | break;
209 | }
210 | case 7://get value of str
211 | {
212 | switch (ops[1][0]) {
213 | case 11:
214 | ans = enBrace("__unTestedGetValue:" + enBrace(jsoToWxOn(ops), '['), '{');
215 | break;
216 | case 3:
217 | //@ts-ignore
218 | ans = new String(ops[1][1]);
219 | ans['_type'] = "var";
220 | break;
221 | default:
222 | throw Error("Unknown type to get value");
223 | }
224 | break;
225 | }
226 | case 8://first object
227 | ans = enBrace(ops[1] + ':' + restoreNext(ops[2], true), '{');//ops[1] have only this way to define
228 | break;
229 | case 9://object
230 | {
231 | function type(x) {
232 | if (x.startsWith('...')) return 1;
233 | if (x.startsWith('{') && x.endsWith('}')) return 0;
234 | return 2;
235 | }
236 |
237 | let a = restoreNext(ops[1], true);
238 | let b = restoreNext(ops[2], true);
239 | let xa = type(a), xb = type(b);
240 | if (xa == 2 || xb == 2) ans = enBrace("__unkownMerge:" + enBrace(a + "," + b, '['), '{');
241 | else {
242 | if (!xa) a = a.slice(1, -1).trim();
243 | if (!xb) b = b.slice(1, -1).trim();
244 | //console.log(l,r);
245 | ans = enBrace(a + ',' + b, '{');
246 | }
247 | break;
248 | }
249 | case 10://...object
250 | ans = '...' + restoreNext(ops[1], true);
251 | break;
252 | case 12: {
253 | let arr = restoreNext(ops[2], true);
254 | if (arr.startsWith('[') && arr.endsWith(']'))
255 | ans = restoreNext(ops[1], true) + enBrace(arr.slice(1, -1).trim(), '(');
256 | else ans = restoreNext(ops[1], true) + '.apply' + enBrace('null,' + arr, '(');
257 | break;
258 | }
259 | default:
260 | ans = enBrace("__unkownSpecific:" + jsoToWxOn(ops), '{');
261 | }
262 | // console.log(ans)
263 | return scope(ans);
264 | }
265 | }
266 |
267 | function catchZ(code: string, cb: Function) {
268 | const reg = /function\s+gz\$gwx(\w+)\(\)\{(?:.|\n)*?;return\s+__WXML_GLOBAL__\.ops_cached\.\$gwx[\w\n]+}/g
269 | const allGwxFunctionMatch = code.match(reg)
270 | if (!allGwxFunctionMatch) return
271 | const allFunctionMap = {}
272 | const zObject = {}
273 | const vm = createVM({
274 | sandbox: { __WXML_GLOBAL__: { ops_cached: {} } }
275 | })
276 | allGwxFunctionMatch.forEach(funcString => { // 提取出所有的Z生成函数及其对应gwx函数名称
277 | const funcReg = /function\s+gz\$gwx(\w*)\(\)/g
278 | const funcName = funcReg.exec(funcString)?.[1]
279 | if (!funcName) return
280 | vm.run(funcString)
281 | const hookZFunc = vm.sandbox[`gz$gwx${funcName}`]
282 | if (hookZFunc) {
283 | allFunctionMap[funcName] = hookZFunc
284 | zObject[funcName] = hookZFunc()
285 | zObject[funcName] = zObject[funcName]
286 | .map((data: any) => {
287 | if (Array.isArray(data) && data[0] === '11182016' && Array.isArray(data[1])) return data[1]
288 | return data;
289 | })
290 | }
291 | })
292 | cb(zObject);
293 | }
294 |
295 | export function getZ(code: string, cb: Function) {
296 | catchZ(code, (z: Record) => {
297 | let ans = {}
298 | for (let gwxFuncName in z) {
299 | ans[gwxFuncName] = z[gwxFuncName].map(gwxData => restoreSingle(gwxData, false))
300 | }
301 | cb(ans)
302 | });
303 | }
304 |
--------------------------------------------------------------------------------
/src/utils/wx-dom.ts:
--------------------------------------------------------------------------------
1 | const systemInfo = {
2 | windowWidth: 375,
3 | windowHeight: 600,
4 | pixelRatio: 2,
5 | language: "en",
6 | version: "1.9.90",
7 | platform: "ios"
8 | };
9 |
10 | export function createWxFakeDom() {
11 | return {
12 | console,
13 | setTimeout,
14 | setInterval,
15 | clearTimeout,
16 | clearInterval,
17 | __wxConfig: {},
18 | App() {
19 | },
20 | Component() {
21 | },
22 | Page() {
23 | },
24 | getApp: () => ({}),
25 | require: () => void 0,
26 | module: {},
27 | exports: {},
28 | global: {},
29 | Behavior: function () {
30 | },
31 | getCurrentPages: () => [],
32 | requireMiniProgram: function () {
33 | },
34 | $gwx: () => void 0,
35 | WXWebAssembly: {},
36 | __wxCodeSpace__: {},
37 | wx: {
38 | request() { },
39 | getExtConfig() { },
40 | getExtConfigSync() { },
41 | postMessageToReferrerPage: function () { },
42 | postMessageToReferrerMiniProgram: function () { },
43 | onUnhandledRejection: function () { },
44 | onThemeChange: function () { },
45 | onPageNotFound: function () { },
46 | onLazyLoadError: function () { },
47 | onError: function () { },
48 | onAudioInterruptionEnd: function () { },
49 | onAudioInterruptionBegin: function () { },
50 | onAppShow: function () { },
51 | onAppHide: function () { },
52 | offUnhandledRejection: function () { },
53 | offThemeChange: function () { },
54 | offPageNotFound: function () { },
55 | offLazyLoadError: function () { },
56 | offError: function () { },
57 | offAudioInterruptionEnd: function () { },
58 | offAudioInterruptionBegin: function () { },
59 | offAppShow: function () { },
60 | offAppHide: function () { },
61 | getStorageSync: function () { },
62 | setStorageSync: function () { },
63 | getStorage: function () { },
64 | setStorage: function () { },
65 | getSystemInfo() {
66 | return systemInfo
67 | },
68 | getSystemInfoSync() {
69 | return systemInfo
70 | },
71 | getRealtimeLogManager() {
72 | return {
73 | log: (msg: string) => console.log(msg),
74 | err: (msg: string) => console.error(msg)
75 | }
76 | },
77 | getMenuButtonBoundingClientRect() {
78 | return {
79 | top: 0,
80 | right: 0,
81 | bottom: 0,
82 | left: 0,
83 | width: 0,
84 | height: 0
85 | }
86 | },
87 | },
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/test/command.mjs:
--------------------------------------------------------------------------------
1 | import inquirer from "inquirer";
2 | import TableInput from "@biggerstar/inquirer-selectable-table";
3 |
4 | inquirer.registerPrompt("table", TableInput);
5 |
6 | const prompts = {
7 | scanPack() {
8 | return inquirer['prompt'](
9 | [
10 | {
11 | type: "table",
12 | name: "packInfo",
13 | message: "",
14 | pageSize: 100,
15 | columns: [
16 | {
17 | name: "firstName",
18 | value: "firstName"
19 | },
20 | {
21 | name: "lastName",
22 | value: "lastName"
23 | },
24 | {
25 | name: "location",
26 | value: "location"
27 | }
28 | ],
29 | rows: [
30 | {
31 | firstName: "Abel1111111111111111111111111111111",
32 | lastName: "Nazeh0000000000000000000000000",
33 | location: "Nigeria9999999999999999999999999999"
34 | },
35 | {
36 | firstName: "Daniel",
37 | lastName: "Ruiz",
38 | location: "Spain"
39 | },
40 | {
41 | firstName: "John",
42 | lastName: "Doe",
43 | location: "Leaf Village"
44 | },
45 | {
46 | firstName: "Kakashi",
47 | lastName: "Hatake",
48 | location: "Leaf Village"
49 | },
50 | ]
51 | }
52 | ]
53 | )
54 | }
55 | }
56 | prompts.scanPack()
57 |
--------------------------------------------------------------------------------
/test/command1.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect} from 'react';
2 | import {render, Text} from 'ink';
3 |
4 | const Counter = () => {
5 | const [counter, setCounter] = useState(0);
6 |
7 | useEffect(() => {
8 | const timer = setInterval(() => {
9 | setCounter(previousCounter => previousCounter + 1);
10 | }, 100);
11 |
12 | return () => {
13 | clearInterval(timer);
14 | };
15 | }, []);
16 |
17 | return {counter} tests passed;
18 | };
19 |
20 | render();
21 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 测试脚本
7 |
8 |
9 |
10 | 开始调试吧 =>
11 |
12 |
18 |
19 |
20 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "lib": [
6 | "esnext",
7 | "dom"
8 | ],
9 | "baseUrl": ".",
10 | "outDir": "dist",
11 | "declaration": false,
12 | "downlevelIteration": true,
13 | "strict": false,
14 | "allowJs": true,
15 | "noImplicitThis": true,
16 | "noUnusedParameters": false,
17 | "noImplicitReturns": true,
18 | "resolveJsonModule": true,
19 | "noUnusedLocals": false,
20 | "noFallthroughCasesInSwitch": true,
21 | "allowSyntheticDefaultImports": true,
22 | "esModuleInterop": true,
23 | "experimentalDecorators": true,
24 | "noImplicitAny": false,
25 | "strictNullChecks": false,
26 | "moduleResolution": "node",
27 | "importHelpers": true,
28 | "paths": {
29 | "@/*": [
30 | "./src/*"
31 | ]
32 | }
33 | },
34 | "compileOnSave": false,
35 | "include": [
36 | "src",
37 | "test",
38 | "typings"
39 | ],
40 | "exclude": [
41 | "dist",
42 | "pkg",
43 | "node_modules"
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import {resolve} from 'node:path';
2 | import {cwd} from 'node:process'
3 | import copy from "rollup-plugin-copy";
4 | import {defineConfig} from "vite";
5 | import { builtinModules } from 'node:module'
6 | import pkg from './package.json'
7 |
8 | const external = [
9 | ...builtinModules,
10 | ...builtinModules.map(name=> `node:${name}`),
11 | ...Object.keys(pkg.dependencies),
12 | ]
13 |
14 | export default defineConfig( {
15 | resolve: {
16 | extensions: [".ts", ".js", '.tsx', '.mjs'],
17 | alias: {
18 | "@": resolve(cwd(), 'src'),
19 | types: resolve(cwd(), 'src/types')
20 | }
21 | },
22 | build: {
23 | emptyOutDir: false,
24 | minify: false,
25 | outDir: resolve(cwd(), 'dist'),
26 | rollupOptions: {
27 | external,
28 | output: {
29 | sourcemap: false,
30 | globals: {}
31 | },
32 | treeshake: true
33 | },
34 | lib: {
35 | entry: resolve(cwd(), './src/bin/wedecode/wedecode.ts'),
36 | formats: ['es'],
37 | name: 'wedecode',
38 | fileName: () => 'wedecode.js',
39 | },
40 | },
41 | plugins: [
42 | copy({
43 | targets: [
44 | // 复制内置 polyfill
45 | { src: 'src/polyfill', dest: 'dist' },
46 | ]
47 | }),
48 | ]
49 | })
50 |
51 |
--------------------------------------------------------------------------------