├── .changeset ├── README.md └── config.json ├── .commitlintrc.yml ├── .eslintrc.yml ├── .github └── workflows │ └── npm-publish.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierrc.yml ├── .vscode ├── launch.json └── settings.json ├── docs └── assets │ └── images │ └── example.gif ├── package.json ├── packages ├── cli │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── clime │ │ │ └── command-common-options.ts │ │ ├── commands │ │ │ ├── default.ts │ │ │ ├── scan.ts │ │ │ └── unpack.ts │ │ ├── decorators │ │ │ └── register-service.decorator.ts │ │ ├── index.ts │ │ └── utils │ │ │ ├── enum.utils.ts │ │ │ └── exception.utils.ts │ └── tsconfig.json ├── shared │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── container.ts │ │ ├── enums │ │ │ ├── miniprogram-type.enum.ts │ │ │ └── platform.enum.ts │ │ ├── index.ts │ │ ├── interfaces │ │ │ ├── command-common-options.interface.ts │ │ │ ├── pack.interface.ts │ │ │ ├── scan.interface.ts │ │ │ └── unpack.interface.ts │ │ └── types │ │ │ ├── miniprogram-info.type.ts │ │ │ └── result.type.ts │ ├── tsconfig.base.json │ └── tsconfig.json └── wechat │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ ├── classes │ │ ├── code-reverser │ │ │ ├── _type.ts │ │ │ ├── index.ts │ │ │ └── pipelines │ │ │ │ ├── reverse-app-json.ts │ │ │ │ ├── reverse-game-json.ts │ │ │ │ ├── reverse-js.ts │ │ │ │ ├── reverse-others.ts │ │ │ │ ├── reverse-wxml.ts │ │ │ │ └── reverse-wxss.ts │ │ ├── file-bundle.ts │ │ ├── offset.ts │ │ ├── wxapkg-unpack.ts │ │ ├── wxml-code-parser.ts │ │ ├── wxml-node.ts │ │ └── wxml-reverse-emulator │ │ │ ├── common.utils.ts │ │ │ ├── gwh.utils.ts │ │ │ ├── gwt.utils.ts │ │ │ ├── index.ts │ │ │ └── rev.utils.ts │ ├── enum │ │ └── bundle-file-name.enum.ts │ ├── index.ts │ ├── services │ │ ├── pack-windows.service.ts │ │ ├── scan-windows.service.ts │ │ └── unpack-windows.service.ts │ └── utils │ │ └── weapp-info.utils.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── readme.md └── wechat-render-op-code.md /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "master", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.commitlintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - "@commitlint/config-conventional" 3 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | commonjs: true 4 | es6: true 5 | parser: "@typescript-eslint/parser" 6 | plugins: 7 | - "@typescript-eslint" 8 | - "prettier" 9 | extends: 10 | - "plugin:@typescript-eslint/recommended" 11 | - "plugin:prettier/recommended" 12 | globals: 13 | Atomics: readonly 14 | SharedArrayBuffer: readonly 15 | parserOptions: 16 | ecmaVersion: 2018 17 | rules: 18 | "@typescript-eslint/explicit-function-return-type": 19 | - warn 20 | - allowExpressions: true 21 | "@typescript-eslint/explicit-member-accessibility": off 22 | "@typescript-eslint/ban-types": off 23 | "@typescript-eslint/no-angle-bracket-type-assertion": off 24 | "@typescript-eslint/no-explicit-any": off 25 | "@typescript-eslint/no-non-null-assertion": off 26 | "@typescript-eslint/no-parameter-properties": off 27 | "@typescript-eslint/prefer-interface": off 28 | "@typescript-eslint/indent": off 29 | overrides: 30 | - files: 31 | - "*.test.ts" 32 | - "*.spec.ts" 33 | rules: 34 | "@typescript-eslint/no-empty-function": off 35 | "@typescript-eslint/interface-name-prefix": off 36 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: publish 5 | 6 | on: 7 | workflow_dispatch: 8 | 9 | release: 10 | types: [created] 11 | # push: 12 | # branches: 13 | # - master 14 | 15 | jobs: 16 | build-and-publish: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version: 21 23 | registry-url: https://registry.npmjs.org/ 24 | - run: npm install pnpm -g 25 | - run: pnpm install 26 | - run: pnpm lint 27 | - run: pnpm build 28 | - run: pnpm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}" 29 | env: 30 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 31 | - uses: changesets/action@v1 32 | with: 33 | version: pnpm changeset version 34 | commit: "chore: update versions" 35 | title: "chore: update versions" 36 | publish: pnpm publish -r --access public 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | **/dist/ -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no -- commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | trailingComma: none 2 | tabWidth: 2 3 | bracketSpacing: true 4 | endOfLine: auto 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Debug CLI", 11 | "skipFiles": ["/**"], 12 | "program": "${workspaceFolder}\\packages\\cli\\dist\\index.js" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "entrys", 4 | "HKCU", 5 | "Miniprogram", 6 | "regedit", 7 | "Weapp", 8 | "wechat", 9 | "wxapkg", 10 | "wxml", 11 | "wxss" 12 | ], 13 | "editor.formatOnSave": true 14 | } 15 | -------------------------------------------------------------------------------- /docs/assets/images/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AEPKILL/miniprogram-track/9c4bcca73601ffb3e9dd5a368be13c738392f83d/docs/assets/images/example.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "miniprogram-track", 3 | "version": "1.0.0", 4 | "private": true, 5 | "packageManager": "pnpm@8.15.6+sha256.01c01eeb990e379b31ef19c03e9d06a14afa5250b82e81303f88721c99ff2e6f", 6 | "description": "Miniprogram track 是用于小程序安全审计的工具包", 7 | "scripts": { 8 | "build": "pnpm -F @miniprogram-track/cli run build", 9 | "dev": "pnpm -F @miniprogram-track/cli run dev", 10 | "cli": "node ./packages/cli/dist/index.js", 11 | "lint": " npx eslint packages/**/src/**/*.ts --fix", 12 | "prepare": "husky" 13 | }, 14 | "keywords": [], 15 | "author": "AEPKILL", 16 | "license": "MIT", 17 | "lint-staged": { 18 | "*.{js,ts}": [ 19 | "eslint --fix" 20 | ] 21 | }, 22 | "devDependencies": { 23 | "@changesets/cli": "^2.27.1", 24 | "@commitlint/cli": "^19.2.1", 25 | "@commitlint/config-conventional": "^19.1.0", 26 | "@typescript-eslint/eslint-plugin": "^7.6.0", 27 | "@typescript-eslint/parser": "^7.6.0", 28 | "eslint": "^8.57.0", 29 | "eslint-config-prettier": "^9.1.0", 30 | "eslint-plugin-prettier": "^5.1.3", 31 | "husky": "^9.0.11", 32 | "lint-staged": "^15.2.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @miniprogram-track/cli 2 | 3 | ## 1.0.2 4 | 5 | ### Patch Changes 6 | 7 | - 249ad92: update 8 | - Updated dependencies [249ad92] 9 | - @miniprogram-track/shared@1.0.2 10 | - @miniprogram-track/wechat@1.0.2 11 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@miniprogram-track/cli", 3 | "version": "1.0.2", 4 | "description": "", 5 | "main": "index.js", 6 | "bin": { 7 | "miniprogram-track": "dist/index.js" 8 | }, 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "build": "pnpm clear && tsc -b ./tsconfig.json", 14 | "dev": "pnpm clear && tsc -b ./tsconfig.json -w", 15 | "clear": "rimraf dist&&pnpm -F @miniprogram-track/shared clear&&pnpm -F @miniprogram-track/wechat clear", 16 | "prepare": "ts-patch install" 17 | }, 18 | "keywords": [], 19 | "author": "AEPKILL", 20 | "license": "MIT", 21 | "dependencies": { 22 | "@miniprogram-track/shared": "workspace:^", 23 | "@miniprogram-track/wechat": "workspace:^", 24 | "@types/node": "^20.12.7", 25 | "chalk": "^4.1.2", 26 | "clime": "^0.5.16", 27 | "husky-di": "^1.0.10", 28 | "ora": "^5.4.1", 29 | "reflect-metadata": "^0.2.2" 30 | }, 31 | "devDependencies": { 32 | "rimraf": "^5.0.5", 33 | "ts-patch": "^3.1.2", 34 | "typescript": "^5.4.5", 35 | "typescript-transform-paths": "^3.4.7" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/cli/src/clime/command-common-options.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-12 14:49:37 5 | */ 6 | 7 | import { ExpectedError, option, Options } from "clime"; 8 | 9 | import { displayEnum, valueInEnum } from "@/utils/enum.utils"; 10 | import { 11 | ICommandCommonOptions, 12 | MiniprogramTypeEnum, 13 | PlatformEnum 14 | } from "@miniprogram-track/shared"; 15 | 16 | export class CommandCommonOptions 17 | extends Options 18 | implements ICommandCommonOptions 19 | { 20 | @option({ 21 | name: "type", 22 | description: "小程序类型", 23 | default: MiniprogramTypeEnum.wechat, 24 | validator(value: string) { 25 | if (!valueInEnum(MiniprogramTypeEnum, value)) { 26 | throw new ExpectedError( 27 | `不支持小程序类型 ${value}, 可选的类型: ${displayEnum( 28 | MiniprogramTypeEnum 29 | )}` 30 | ); 31 | } 32 | } 33 | }) 34 | type!: string; 35 | 36 | @option({ 37 | name: "platform", 38 | description: "平台", 39 | default: PlatformEnum.windows, 40 | validator(value) { 41 | if (!valueInEnum(PlatformEnum, value)) { 42 | throw new ExpectedError( 43 | `不支持平台 ${value}, 可选的平台: ${displayEnum(PlatformEnum)}` 44 | ); 45 | } 46 | } 47 | }) 48 | platform!: string; 49 | } 50 | -------------------------------------------------------------------------------- /packages/cli/src/commands/default.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-12 14:26:58 5 | */ 6 | 7 | export const description = `用于小程序安全审计的工具包`; 8 | -------------------------------------------------------------------------------- /packages/cli/src/commands/scan.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-12 15:16:37 5 | */ 6 | 7 | import { command, Command, metadata } from "clime"; 8 | import chalk from "chalk"; 9 | 10 | import { CommandCommonOptions } from "@/clime/command-common-options.js"; 11 | import { RegisterService } from "@/decorators/register-service.decorator"; 12 | import { container, IScan } from "@miniprogram-track/shared"; 13 | import ora from "ora"; 14 | 15 | @command({ 16 | description: "扫描小程序" 17 | }) 18 | export default class ScanCommand extends Command { 19 | @RegisterService 20 | @metadata 21 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 22 | async execute(_options: CommandCommonOptions): Promise { 23 | const spinner = ora({ 24 | discardStdin: false 25 | }); 26 | spinner.text = "扫描中..."; 27 | spinner.start(); 28 | try { 29 | const miniprogramInfoArray = await container.resolve(IScan).scan(); 30 | spinner.succeed(); 31 | 32 | for (const item of miniprogramInfoArray) { 33 | if (item.wxapkgPaths.length === 0) { 34 | continue; 35 | } 36 | 37 | console.log(`${chalk.green("appid")}: ${item.appid}`); 38 | console.log(` 名称: ${item.name || "未知"}`); 39 | console.log(` 开发者: ${item.author || "未知"}`); 40 | console.log(` miniprogramDir: ${item.miniprogramDir}`); 41 | console.log(` wxapkgPaths:`); 42 | for (const path of item.wxapkgPaths) { 43 | console.log(` ${path}`); 44 | } 45 | 46 | const isLastItem = 47 | item.appid == 48 | miniprogramInfoArray[miniprogramInfoArray.length - 1].appid; 49 | if (!isLastItem) { 50 | console.log(chalk.yellow("-".repeat(100))); 51 | } 52 | } 53 | } catch (error: any) { 54 | spinner.text = `扫描失败,失败原因: ${error.message || "未知原因"}`; 55 | spinner.fail(); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/cli/src/commands/unpack.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-12 15:16:37 5 | */ 6 | 7 | import { command, Command, ExpectedError, metadata, option } from "clime"; 8 | import ora from "ora"; 9 | 10 | import { CommandCommonOptions } from "@/clime/command-common-options.js"; 11 | import { RegisterService } from "@/decorators/register-service.decorator"; 12 | import { container, IUnPack, UnpackOptions } from "@miniprogram-track/shared"; 13 | import chalk from "chalk"; 14 | 15 | class UnpackCommandOptions 16 | extends CommandCommonOptions 17 | implements UnpackOptions 18 | { 19 | @option({ 20 | name: "appid", 21 | description: "appid", 22 | required: true 23 | }) 24 | appid!: string; 25 | 26 | @option({ 27 | name: "pkgPath", 28 | description: "小程序包的路径" 29 | }) 30 | pkgPath?: string; 31 | 32 | @option({ 33 | name: "miniprogramDir", 34 | description: "小程序目录" 35 | }) 36 | miniprogramDir?: string; 37 | 38 | @option({ 39 | name: "targetDir", 40 | description: "解包目标目录" 41 | }) 42 | targetDir?: string; 43 | 44 | @option({ 45 | name: "restoreCode", 46 | description: "是否尝试还原原始代码结构", 47 | default: true, 48 | toggle: true 49 | }) 50 | restoreCode?: boolean; 51 | } 52 | 53 | @command({ 54 | description: "解包小程序" 55 | }) 56 | export default class UnpackCommand extends Command { 57 | @RegisterService 58 | @metadata 59 | async execute(options: UnpackCommandOptions): Promise { 60 | const { pkgPath, miniprogramDir, restoreCode } = options; 61 | 62 | if (!pkgPath && !miniprogramDir) { 63 | throw new ExpectedError(`请输入 --pkgPath 或 --miniprogramDir 参数`); 64 | } 65 | 66 | if (pkgPath && miniprogramDir) { 67 | console.warn( 68 | chalk.yellow( 69 | `--pkgPath 和 --miniprogramDir 参数同时传入,优先使用 --miniprogramDir 参数` 70 | ) 71 | ); 72 | } 73 | 74 | if (restoreCode) { 75 | console.warn( 76 | chalk.yellow( 77 | `--restoreCode 选项尚处于实验性阶段,若有问题请及时反馈 https://github.com/AEPKILL/miniprogram-track/issues/new ` 78 | ) 79 | ); 80 | } 81 | 82 | const spinner = ora({ 83 | discardStdin: false 84 | }); 85 | 86 | spinner.text = "解包中..."; 87 | spinner.start(); 88 | try { 89 | const unpackInfo = await container.resolve(IUnPack).unpack(options); 90 | spinner.text = `解包完成,已解包文件到: ${unpackInfo.path}`; 91 | spinner.succeed(); 92 | } catch (e: any) { 93 | spinner.text = `解包失败: ${e.message || "未知原因"}`; 94 | spinner.fail(); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/cli/src/decorators/register-service.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-12 18:04:40 5 | */ 6 | 7 | import { Command, ExpectedError } from "clime"; 8 | import { ValueProvider } from "husky-di"; 9 | 10 | import { throwExpectedError } from "@/utils/exception.utils"; 11 | import { 12 | container, 13 | ICommandCommonOptions, 14 | MiniprogramTypeEnum 15 | } from "@miniprogram-track/shared"; 16 | import { registerService as registerWechatService } from "@miniprogram-track/wechat"; 17 | 18 | export const RegisterService = ( 19 | _target: Command, 20 | _key: string | symbol, 21 | descriptor: TypedPropertyDescriptor<(options: T) => any> 22 | ) => { 23 | const { value } = descriptor; 24 | return { 25 | ...descriptor, 26 | value(options: T) { 27 | const { type } = options; 28 | 29 | container.register( 30 | ICommandCommonOptions, 31 | new ValueProvider({ 32 | useValue: options 33 | }) 34 | ); 35 | 36 | switch (type) { 37 | case MiniprogramTypeEnum.wechat: { 38 | throwExpectedError(() => registerWechatService(container, options)); 39 | break; 40 | } 41 | default: { 42 | throw new ExpectedError(`不支持的小程序类型: ${type}`); 43 | } 44 | } 45 | 46 | return value!.apply(this, [options]); 47 | } 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /packages/cli/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * @overview 5 | * @author AEPKILL 6 | * @created 2024-04-12 17:22:03 7 | */ 8 | 9 | import "reflect-metadata"; 10 | 11 | import { CLI, Shim } from "clime"; 12 | import { resolve } from "path"; 13 | import os from "os"; 14 | import chalk from "chalk"; 15 | 16 | if (os.platform() != "win32") { 17 | console.log(chalk.red("目前 miniprogram-track 仅支持 windows 系统")); 18 | process.exit(0); 19 | } 20 | 21 | const cli = new CLI("miniprogram-track", resolve(__dirname, "commands")); 22 | const shim = new Shim(cli); 23 | 24 | shim.execute(process.argv); 25 | -------------------------------------------------------------------------------- /packages/cli/src/utils/enum.utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-12 20:13:14 5 | */ 6 | 7 | export function getEnumKeys(enumObject: T): Array { 8 | return Object.keys(enumObject).filter((key) => isNaN(Number(key))) as Array< 9 | keyof T 10 | >; 11 | } 12 | 13 | export function valueInEnum( 14 | enumObject: T, 15 | value: number | string 16 | ): boolean { 17 | return getEnumKeys(enumObject).some((key) => enumObject[key] === value); 18 | } 19 | 20 | export function getEnumValues( 21 | enumObject: T 22 | ): Array { 23 | return getEnumKeys(enumObject).map((key) => enumObject[key]); 24 | } 25 | 26 | export function displayEnum( 27 | enumObject: T, 28 | separate = ", " 29 | ): string { 30 | return getEnumValues(enumObject).join(separate); 31 | } 32 | -------------------------------------------------------------------------------- /packages/cli/src/utils/exception.utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-12 19:29:09 5 | */ 6 | 7 | import { ExpectedError } from "clime"; 8 | 9 | export function throwExpectedError(run: (...args: any[]) => any): void { 10 | try { 11 | run(); 12 | } catch (error) { 13 | if (error instanceof Error) { 14 | throw new ExpectedError(error.message); 15 | } 16 | throw new ExpectedError("unknown error."); 17 | } 18 | } 19 | 20 | export async function throwExpectedErrorAsync( 21 | run: (...args: any[]) => Promise 22 | ): Promise { 23 | try { 24 | return await run(); 25 | } catch (error) { 26 | if (error instanceof Error) { 27 | throw new ExpectedError(error.message); 28 | } 29 | throw new ExpectedError("unknown error."); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@miniprogram-track/shared/tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src", 6 | "declaration": true, 7 | "baseUrl": "src", 8 | "paths": { 9 | "@/*": ["*"] 10 | } 11 | }, 12 | "include": ["src"], 13 | "references": [ 14 | { 15 | "path": "../shared" 16 | }, 17 | { 18 | "path": "../wechat" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /packages/shared/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @miniprogram-track/shared 2 | 3 | ## 1.0.2 4 | 5 | ### Patch Changes 6 | 7 | - 249ad92: update 8 | -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@miniprogram-track/shared", 3 | "version": "1.0.2", 4 | "description": "", 5 | "author": "AEPKILL", 6 | "main": "dist/index.js", 7 | "exports": { 8 | ".": { 9 | "default": "./dist/index.js", 10 | "types": "./dist/index.d.ts" 11 | }, 12 | "./tsconfig.base.json": { 13 | "default": "./tsconfig.base.json" 14 | } 15 | }, 16 | "files": [ 17 | "dist" 18 | ], 19 | "scripts": { 20 | "build": "tsc -p tsconfig.json", 21 | "clear": "rimraf dist", 22 | "prepare": "ts-patch install" 23 | }, 24 | "keywords": [], 25 | "license": "MIT", 26 | "dependencies": { 27 | "husky-di": "^1.0.10" 28 | }, 29 | "devDependencies": { 30 | "rimraf": "^5.0.5", 31 | "ts-patch": "^3.1.2", 32 | "typescript": "^5.4.5", 33 | "typescript-transform-paths": "^3.4.7" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/shared/src/container.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-12 13:16:33 5 | */ 6 | 7 | import { createContainer } from "husky-di"; 8 | 9 | export const container = createContainer("miniprogram-track"); 10 | -------------------------------------------------------------------------------- /packages/shared/src/enums/miniprogram-type.enum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 小程序类型 3 | * @author AEPKILL 4 | * @created 2024-04-12 12:43:54 5 | */ 6 | 7 | export enum MiniprogramTypeEnum { 8 | /** 微信小程序 */ 9 | wechat = "wechat" 10 | } 11 | -------------------------------------------------------------------------------- /packages/shared/src/enums/platform.enum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-12 14:51:43 5 | */ 6 | 7 | export enum PlatformEnum { 8 | windows = "windows" 9 | } 10 | -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-12 14:33:00 5 | */ 6 | 7 | export { container } from "./container"; 8 | 9 | export { MiniprogramTypeEnum } from "./enums/miniprogram-type.enum"; 10 | export { PlatformEnum } from "./enums/platform.enum"; 11 | 12 | export * from "./types/result.type"; 13 | export * from "./types/miniprogram-info.type"; 14 | 15 | export * from "./interfaces/command-common-options.interface"; 16 | export * from "./interfaces/pack.interface"; 17 | export * from "./interfaces/unpack.interface"; 18 | export * from "./interfaces/scan.interface"; 19 | -------------------------------------------------------------------------------- /packages/shared/src/interfaces/command-common-options.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-12 15:01:52 5 | */ 6 | 7 | import { createServiceIdentifier } from "husky-di"; 8 | 9 | export interface ICommandCommonOptions { 10 | type: string; 11 | platform: string; 12 | } 13 | 14 | export const ICommandCommonOptions = 15 | createServiceIdentifier("ICommandCommonOptions"); 16 | -------------------------------------------------------------------------------- /packages/shared/src/interfaces/pack.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-12 12:42:53 5 | */ 6 | 7 | import { createServiceIdentifier } from "husky-di"; 8 | 9 | export interface IPack {} 10 | 11 | export const IPack = createServiceIdentifier("IPack"); 12 | -------------------------------------------------------------------------------- /packages/shared/src/interfaces/scan.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-12 14:32:36 5 | */ 6 | 7 | import { MiniprogramInfo } from "@/types/miniprogram-info.type"; 8 | import { createServiceIdentifier } from "husky-di"; 9 | 10 | export interface IScan { 11 | scan(): Promise; 12 | } 13 | 14 | export const IScan = createServiceIdentifier("IScan"); 15 | -------------------------------------------------------------------------------- /packages/shared/src/interfaces/unpack.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-14 14:47:16 5 | */ 6 | 7 | import { createServiceIdentifier } from "husky-di"; 8 | 9 | export interface UnpackInfo { 10 | path: string; 11 | } 12 | 13 | export interface UnpackOptions { 14 | appid: string; 15 | pkgPath?: string; 16 | miniprogramDir?: string; 17 | targetDir?: string; 18 | 19 | restoreCode?: boolean; 20 | } 21 | 22 | export interface IUnPack { 23 | unpack(options: UnpackOptions): Promise; 24 | } 25 | 26 | export const IUnPack = createServiceIdentifier("IUnPack"); 27 | -------------------------------------------------------------------------------- /packages/shared/src/types/miniprogram-info.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-12 14:29:23 5 | */ 6 | 7 | export type MiniprogramInfo = { 8 | /** 小程序名称*/ 9 | name: string; 10 | 11 | /** 小程序描述 */ 12 | description: string; 13 | 14 | /** 小程序开发者 */ 15 | author: string; 16 | 17 | /** 小程序开发者 id */ 18 | authorId: string; 19 | 20 | /** 小程序路径 */ 21 | miniprogramDir: string; 22 | 23 | /** 小程序头像 */ 24 | avatar: string; 25 | 26 | /** 小程序 appid */ 27 | appid: string; 28 | 29 | /** 小程序包路径 */ 30 | wxapkgPaths: string[]; 31 | }; 32 | -------------------------------------------------------------------------------- /packages/shared/src/types/result.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-12 14:31:29 5 | */ 6 | export type SuccessResult = { 7 | success: true; 8 | data: T; 9 | code?: number; 10 | message?: string; 11 | }; 12 | 13 | export type ErrorResult = { 14 | success: false; 15 | code?: number; 16 | message?: string; 17 | error?: any; 18 | }; 19 | 20 | export type Result = SuccessResult | ErrorResult; 21 | -------------------------------------------------------------------------------- /packages/shared/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "CommonJS", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true, 7 | "esModuleInterop": true, 8 | "noImplicitAny": true, 9 | "strict": true, 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "plugins": [ 13 | { "transform": "typescript-transform-paths" }, 14 | { "transform": "typescript-transform-paths", "afterDeclarations": true } 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src", 6 | "tsBuildInfoFile": "./dist/.tsbuildinfo", 7 | "declaration": true, 8 | "composite": true, 9 | "baseUrl": "src", 10 | "paths": { 11 | "@/*": ["*"] 12 | } 13 | }, 14 | "include": ["src"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/wechat/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @miniprogram-track/wechat 2 | 3 | ## 1.0.2 4 | 5 | ### Patch Changes 6 | 7 | - 249ad92: update 8 | - Updated dependencies [249ad92] 9 | - @miniprogram-track/shared@1.0.2 10 | -------------------------------------------------------------------------------- /packages/wechat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@miniprogram-track/wechat", 3 | "version": "1.0.2", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "exports": { 7 | ".": { 8 | "default": "./dist/index.js", 9 | "types": "./dist/index.d.ts" 10 | } 11 | }, 12 | "files": [ 13 | "dist" 14 | ], 15 | "scripts": { 16 | "build": "echo 'build wechat'", 17 | "prepare": "ts-patch install", 18 | "clear": "rimraf dist" 19 | }, 20 | "keywords": [], 21 | "author": "AEPKILL", 22 | "license": "MIT", 23 | "dependencies": { 24 | "@babel/core": "^7.24.4", 25 | "@babel/generator": "^7.24.4", 26 | "@babel/traverse": "^7.24.1", 27 | "@babel/types": "^7.24.0", 28 | "@miniprogram-track/shared": "workspace:^", 29 | "@types/babel__core": "^7.20.5", 30 | "@types/babel__generator": "^7.6.8", 31 | "@types/babel__traverse": "^7.20.5", 32 | "@types/fs-extra": "^11.0.4", 33 | "@types/node": "^20.12.7", 34 | "fs-extra": "^11.2.0", 35 | "glob": "^10.3.12", 36 | "husky-di": "^1.0.10", 37 | "regedit": "^5.1.3" 38 | }, 39 | "devDependencies": { 40 | "rimraf": "^5.0.5", 41 | "ts-patch": "^3.1.2", 42 | "typescript": "^5.4.5", 43 | "typescript-transform-paths": "^3.4.7" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/wechat/src/classes/code-reverser/_type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-15 21:52:58 5 | */ 6 | 7 | import { FileBundle } from "../file-bundle"; 8 | 9 | export type ComponentSource = { 10 | config: Record; 11 | js: string; 12 | wxml: string; 13 | wxss: string; 14 | }; 15 | 16 | export interface ICodeReverser { 17 | readonly originalBundle: FileBundle; 18 | readonly restoreBundle: FileBundle; 19 | readonly appConfig: Record | null; 20 | readonly pages: Record; 21 | 22 | readonly appServiceParsed: babel.ParseResult | null; 23 | readonly workersParsed: babel.ParseResult | null; 24 | readonly appWxssParsed: babel.ParseResult | null; 25 | 26 | readonly gameParsed: babel.ParseResult | null; 27 | readonly gameConfig: Record | null; 28 | } 29 | 30 | export type ReversePipeline = (codeReverser: ICodeReverser) => void; 31 | -------------------------------------------------------------------------------- /packages/wechat/src/classes/code-reverser/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-15 11:34:57 5 | */ 6 | 7 | import { FileBundle } from "@/classes/file-bundle"; 8 | import { BundleFileNameEnum } from "@/enum/bundle-file-name.enum"; 9 | import * as babel from "@babel/core"; 10 | 11 | import { ComponentSource, ICodeReverser } from "./_type"; 12 | import { reverseAppJson } from "./pipelines/reverse-app-json"; 13 | import { reverseGameJson } from "./pipelines/reverse-game-json"; 14 | import { reverseJs } from "./pipelines/reverse-js"; 15 | import { reverseOthers } from "./pipelines/reverse-others"; 16 | import { reverseWxml } from "./pipelines/reverse-wxml"; 17 | import { reverseWxss } from "./pipelines/reverse-wxss"; 18 | 19 | export class CodeReverser implements ICodeReverser { 20 | readonly originalBundle: FileBundle; 21 | readonly restoreBundle: FileBundle; 22 | readonly appConfig: Record | null = null; 23 | readonly pages: Record; 24 | 25 | readonly appServiceParsed: babel.ParseResult | null = null; 26 | readonly workersParsed: babel.ParseResult | null = null; 27 | readonly appWxssParsed: babel.ParseResult | null = null; 28 | 29 | readonly gameParsed: babel.ParseResult | null = null; 30 | readonly gameConfig: Record | null = null; 31 | 32 | constructor(bundle: FileBundle) { 33 | this.originalBundle = bundle; 34 | this.restoreBundle = new FileBundle(`${this.originalBundle}-restore`); 35 | 36 | const appConfigContent = this.originalBundle 37 | .get(BundleFileNameEnum.appConfig) 38 | ?.buffer.toString(); 39 | 40 | if (appConfigContent) { 41 | this.appConfig = JSON.parse(appConfigContent); 42 | } 43 | 44 | const appServiceContent = this.originalBundle 45 | .get(BundleFileNameEnum.appService) 46 | ?.buffer.toString(); 47 | if (appServiceContent) { 48 | this.appServiceParsed = babel.parse(appServiceContent); 49 | } 50 | 51 | const appWxssContent = this.originalBundle 52 | .get(BundleFileNameEnum.appWxss) 53 | ?.buffer.toString(); 54 | if (appWxssContent) { 55 | this.appWxssParsed = babel.parse(appWxssContent); 56 | } 57 | 58 | const workersContent = this.originalBundle 59 | .get(BundleFileNameEnum.workers) 60 | ?.buffer.toString(); 61 | if (workersContent) { 62 | this.workersParsed = babel.parse(workersContent); 63 | } 64 | 65 | const gameConfigContent = this.originalBundle 66 | .get(BundleFileNameEnum.gameConfig) 67 | ?.buffer.toString(); 68 | if (gameConfigContent) { 69 | this.gameConfig = JSON.parse(gameConfigContent); 70 | } 71 | 72 | const gameContent = this.originalBundle 73 | .get(BundleFileNameEnum.game) 74 | ?.buffer.toString(); 75 | if (gameContent) { 76 | this.gameParsed = babel.parse(gameContent); 77 | } 78 | 79 | this.pages = {}; 80 | } 81 | 82 | async reverse(): Promise { 83 | reverseAppJson(this); 84 | reverseGameJson(this); 85 | 86 | reverseJs(this); 87 | reverseWxss(this); 88 | reverseWxml(this); 89 | reverseOthers(this); 90 | 91 | return this.restoreBundle; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /packages/wechat/src/classes/code-reverser/pipelines/reverse-app-json.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-15 22:00:38 5 | */ 6 | 7 | import { ReversePipeline } from "../_type"; 8 | 9 | export const reverseAppJson: ReversePipeline = ({ 10 | appConfig, 11 | restoreBundle 12 | }) => { 13 | if (!appConfig) { 14 | return; 15 | } 16 | 17 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 18 | const { subPackages, pages, page, entryPagePath, ...rest } = appConfig; 19 | 20 | const finallyPages = pages.filter((it: string) => 21 | subPackages.every((it2: any) => !it.startsWith(it2.root.substring(1))) 22 | ); 23 | 24 | restoreBundle.append({ 25 | path: "app.json", 26 | buffer: Buffer.from( 27 | JSON.stringify( 28 | { 29 | pages: finallyPages, 30 | subPackages, 31 | entryPagePath: (entryPagePath || "").replace(/\.html$/, ""), 32 | ...rest 33 | }, 34 | null, 35 | 2 36 | ) 37 | ) 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /packages/wechat/src/classes/code-reverser/pipelines/reverse-game-json.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-15 22:00:38 5 | */ 6 | 7 | import { ReversePipeline } from "../_type"; 8 | 9 | export const reverseGameJson: ReversePipeline = ({ 10 | gameConfig, 11 | restoreBundle 12 | }) => { 13 | if (!gameConfig) return; 14 | 15 | restoreBundle.append({ 16 | path: "app.json", 17 | buffer: Buffer.from(JSON.stringify(gameConfig, null, 2)) 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/wechat/src/classes/code-reverser/pipelines/reverse-js.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-15 22:00:38 5 | */ 6 | 7 | import { FileBundle } from "@/classes/file-bundle"; 8 | import * as babel from "@babel/core"; 9 | import { join } from "path"; 10 | 11 | import generator from "@babel/generator"; 12 | import traverse from "@babel/traverse"; 13 | 14 | import { ReversePipeline } from "../_type"; 15 | 16 | function reverseJsInBundleFromParsedResult( 17 | ast: babel.ParseResult, 18 | restoreBundle: FileBundle 19 | ): void { 20 | traverse(ast, { 21 | CallExpression(path) { 22 | const isDefineFunctionCall = 23 | (path.node.callee as babel.types.V8IntrinsicIdentifier).name === 24 | "define" && path.parentPath?.parentPath?.type === "Program"; 25 | if (isDefineFunctionCall) { 26 | const args = path.node.arguments; 27 | const functionContent = (args[1] as babel.types.FunctionExpression).body 28 | .body; 29 | const codes = functionContent.map((it) => generator(it).code); 30 | 31 | let fileName = (args[0] as babel.types.StringLiteral).value; 32 | if (fileName.startsWith("/__plugin__") && !fileName.endsWith(".js")) { 33 | fileName = join(fileName, "index.js"); 34 | } 35 | 36 | restoreBundle.append({ 37 | path: fileName, 38 | buffer: Buffer.from(codes.join("\n\n")) 39 | }); 40 | } 41 | } 42 | }); 43 | } 44 | 45 | export const reverseJs: ReversePipeline = ({ 46 | appServiceParsed, 47 | gameParsed, 48 | workersParsed, 49 | restoreBundle 50 | }) => { 51 | if (appServiceParsed) { 52 | reverseJsInBundleFromParsedResult(appServiceParsed, restoreBundle); 53 | } 54 | if (gameParsed) { 55 | reverseJsInBundleFromParsedResult(gameParsed, restoreBundle); 56 | } 57 | if (workersParsed) { 58 | reverseJsInBundleFromParsedResult(workersParsed, restoreBundle); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /packages/wechat/src/classes/code-reverser/pipelines/reverse-others.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-17 22:00:38 5 | */ 6 | 7 | import { BundleFileNameEnum } from "@/enum/bundle-file-name.enum"; 8 | import { ReversePipeline } from "../_type"; 9 | import { FileBundle } from "@/classes/file-bundle"; 10 | 11 | export const reverseOthers: ReversePipeline = ({ 12 | originalBundle, 13 | restoreBundle 14 | }) => { 15 | const specialBundleFiles = Object.values(BundleFileNameEnum).map((it) => 16 | FileBundle.addRootPrefix(it) 17 | ); 18 | 19 | for (const it of originalBundle.filesList) { 20 | if (true || !specialBundleFiles.includes(it.path)) { 21 | restoreBundle.append(it); 22 | } 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /packages/wechat/src/classes/code-reverser/pipelines/reverse-wxml.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-16 10:58:42 5 | */ 6 | 7 | import { WxmlReverseEmulator } from "@/classes/wxml-reverse-emulator"; 8 | import { ReversePipeline } from "../_type"; 9 | import { WxmlCodeParser } from "@/classes/wxml-code-parser"; 10 | 11 | export const reverseWxml: ReversePipeline = ({ 12 | appWxssParsed, 13 | restoreBundle 14 | }) => { 15 | if (!appWxssParsed) { 16 | return; 17 | } 18 | 19 | const wxmlCodeParser = new WxmlCodeParser(); 20 | const renderInfoRecord = wxmlCodeParser.parse(appWxssParsed); 21 | 22 | for (const it in renderInfoRecord) { 23 | const renderInfo = renderInfoRecord[it]; 24 | const wxml = new WxmlReverseEmulator(renderInfo).getWxml(); 25 | console.log(`renderFunctionName:`, it, renderInfo.renderFunctionName); 26 | console.log(wxml); 27 | 28 | restoreBundle.append({ 29 | path: renderInfo.fileName, 30 | buffer: Buffer.from(wxml) 31 | }); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /packages/wechat/src/classes/code-reverser/pipelines/reverse-wxss.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-16 10:58:42 5 | */ 6 | 7 | import { ReversePipeline } from "../_type"; 8 | import path from "path"; 9 | import vm from "vm"; 10 | 11 | type MarkupOptions = { 12 | suffix: string; 13 | path: string; 14 | }; 15 | 16 | function makeup(file: Array, opt: MarkupOptions): string { 17 | let res = ""; 18 | const ex = file; 19 | for (let i = ex.length - 1; i >= 0; i--) { 20 | const content = ex[i]; 21 | if (Array.isArray(content)) { 22 | const op = content[0]; 23 | if (op == 0) { 24 | res = content[1] + "rpx" + res; 25 | } else if (op == 1) { 26 | res = opt.suffix + res; 27 | } else if (op == 2) { 28 | res = `@import "${path.posix.relative(opt.path, content[1])}"; ${res}`; 29 | } 30 | } else { 31 | res = content + res; 32 | } 33 | } 34 | return res.replace(/body\s*{/g, "page{"); 35 | } 36 | 37 | export const reverseWxss: ReversePipeline = ({ 38 | originalBundle, 39 | restoreBundle 40 | }) => { 41 | const appWxss = originalBundle.get("app-wxss.js"); 42 | if (!appWxss) { 43 | return; 44 | } 45 | 46 | const wxssContent = appWxss.buffer 47 | .toString() 48 | .replace(/var\s*setCssToHead\s*=/, "var setCssToHead2 =") 49 | .replace( 50 | /var\s*__COMMON_STYLESHEETS__\s*=/, 51 | "var __COMMON_STYLESHEETS__2 =" 52 | ); 53 | 54 | const context = vm.createContext({ 55 | __COMMON_STYLESHEETS__: new Proxy( 56 | {}, 57 | { 58 | set(target, p, value, receiver) { 59 | if (typeof p === "string") { 60 | restoreBundle.append({ 61 | path: p, 62 | buffer: Buffer.from( 63 | makeup(value, { 64 | suffix: "", 65 | path: path.parse(p).dir 66 | }) 67 | ) 68 | }); 69 | } 70 | return Reflect.set(target, p, value, receiver); 71 | } 72 | } 73 | ), 74 | setCssToHead: function ( 75 | file: Array, 76 | _xcInvalid: any, 77 | info: { path: string } 78 | ) { 79 | if (info && info.path) { 80 | restoreBundle.append({ 81 | path: info.path, 82 | buffer: Buffer.from( 83 | makeup(file, { 84 | suffix: "", 85 | path: path.parse(info.path).dir 86 | }) 87 | ) 88 | }); 89 | } 90 | 91 | return function () {}; 92 | }, 93 | navigator, 94 | window: { 95 | screen: {} 96 | } 97 | }); 98 | 99 | vm.runInContext(wxssContent, context); 100 | }; 101 | -------------------------------------------------------------------------------- /packages/wechat/src/classes/file-bundle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-15 12:32:28 5 | */ 6 | import fs from "fs-extra"; 7 | import path from "path"; 8 | 9 | export interface BundleFile { 10 | path: string; 11 | buffer: Buffer; 12 | size?: number; 13 | } 14 | 15 | function addRootPrefix(path: string): string { 16 | if (path[0] === "/") return path; 17 | return `/${path}`; 18 | } 19 | 20 | export class FileBundle { 21 | #files: Record = {}; 22 | 23 | constructor(readonly name: string) {} 24 | 25 | get files(): Record { 26 | return this.#files; 27 | } 28 | 29 | get filesList(): BundleFile[] { 30 | return Object.values(this.#files); 31 | } 32 | 33 | isExist(path: string): boolean { 34 | return addRootPrefix(path) in this.#files; 35 | } 36 | 37 | append(file: BundleFile): void { 38 | this.#files[addRootPrefix(file.path)] = file; 39 | } 40 | 41 | get(path: string): BundleFile | undefined { 42 | return this.#files[addRootPrefix(path)]; 43 | } 44 | 45 | remove(path: string): void { 46 | delete this.#files[addRootPrefix(path)]; 47 | } 48 | 49 | clone(): FileBundle { 50 | const Bundle = new FileBundle(this.name); 51 | for (const file of this.filesList) { 52 | Bundle.append(file); 53 | } 54 | return Bundle; 55 | } 56 | 57 | async saveTo(targetDir: string): Promise { 58 | for (const file of this.filesList) { 59 | const filePath = path.join(targetDir, file.path); 60 | const fileDir = path.parse(filePath).dir; 61 | const exists = await fs.exists(filePath); 62 | 63 | if (exists) { 64 | console.log(`WARN: file ${filePath} already exists, overwrite it`); 65 | } 66 | 67 | await fs.ensureDir(fileDir); 68 | await fs.writeFile(filePath, file.buffer); 69 | } 70 | } 71 | 72 | static readonly addRootPrefix = addRootPrefix; 73 | } 74 | -------------------------------------------------------------------------------- /packages/wechat/src/classes/offset.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-14 21:25:51 5 | */ 6 | 7 | export class Offset { 8 | #index: number; 9 | 10 | constructor(index: number) { 11 | this.#index = index; 12 | } 13 | 14 | get index(): number { 15 | return this.#index; 16 | } 17 | 18 | forward(offset: number): number { 19 | const current = this.#index; 20 | this.#index += offset; 21 | return current; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/wechat/src/classes/wxapkg-unpack.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-14 17:52:50 5 | */ 6 | import fs from "fs-extra"; 7 | import { pbkdf2Sync, createDecipheriv } from "crypto"; 8 | import { Offset } from "./offset"; 9 | import { FileBundle } from "./file-bundle"; 10 | import path from "path"; 11 | 12 | export interface WxapkgFileHeader { 13 | firstMask: number; 14 | info1: number; 15 | indexInfoLength: number; 16 | bodyInfoLength: number; 17 | lastMark: number; 18 | fileCount: number; 19 | } 20 | 21 | export interface WxapkgChunk { 22 | nameLength: number; 23 | name: string; 24 | offset: number; 25 | size: number; 26 | } 27 | 28 | export interface WxapkgMetadata { 29 | header: WxapkgFileHeader; 30 | chunks: WxapkgChunk[]; 31 | } 32 | 33 | export interface WxapkgUnpackOptions { 34 | appid: string; 35 | pkgPath: string; 36 | } 37 | 38 | export class WxapkgUnpack { 39 | readonly pkgPath: string; 40 | readonly buffer: Buffer; 41 | readonly appid: string; 42 | 43 | constructor(options: WxapkgUnpackOptions) { 44 | const { pkgPath, appid } = options; 45 | 46 | this.pkgPath = pkgPath; 47 | this.appid = appid; 48 | 49 | const buffer = fs.readFileSync(pkgPath); 50 | if (WxapkgUnpack.isNeedDecrypt(buffer)) { 51 | this.buffer = WxapkgUnpack.decrypt(buffer, appid); 52 | } else { 53 | this.buffer = buffer; 54 | } 55 | } 56 | 57 | unpack(): FileBundle { 58 | const metadata = WxapkgUnpack.getFileMetadata(this.buffer); 59 | const { header } = metadata; 60 | const isValid = 61 | header.firstMask == WxapkgUnpack.pkgFirstMaskConst && 62 | header.lastMark == WxapkgUnpack.pkgLastMaskConst; 63 | const bundle = new FileBundle(path.parse(this.pkgPath).name); 64 | 65 | if (!isValid) { 66 | throw new Error(`${this.pkgPath} 不是一个有效的 wxapkg 文件`); 67 | } 68 | 69 | for (const chunk of metadata.chunks) { 70 | bundle.append({ 71 | path: chunk.name, 72 | buffer: this.buffer.subarray(chunk.offset, chunk.offset + chunk.size), 73 | size: chunk.size 74 | }); 75 | } 76 | 77 | return bundle; 78 | } 79 | 80 | static readonly pkgFirstMaskConst = 0xbe; 81 | static readonly pkgLastMaskConst = 0xed; 82 | static readonly cryptPkgHeaderFlagConst = [ 83 | 0x56, 0x31, 0x4d, 0x4d, 0x57, 0x58 84 | ]; 85 | 86 | static isNeedDecrypt(buffer: Buffer): boolean { 87 | return this.cryptPkgHeaderFlagConst.every((v, i) => buffer[i] === v); 88 | } 89 | 90 | static getFileMetadata(buffer: Buffer): WxapkgMetadata { 91 | const view = new DataView(buffer.buffer); 92 | const offset = new Offset(0); 93 | 94 | const metadata: WxapkgMetadata = { 95 | header: { 96 | firstMask: view.getUint8(offset.forward(1)), 97 | info1: view.getUint32(offset.forward(4)), 98 | indexInfoLength: view.getUint32(offset.forward(4)), 99 | bodyInfoLength: view.getUint32(offset.forward(4)), 100 | lastMark: view.getUint8(offset.forward(1)), 101 | fileCount: view.getUint32(offset.forward(4)) 102 | }, 103 | chunks: [] 104 | }; 105 | const fileCount = metadata.header.fileCount; 106 | 107 | for (let i = 0; i < fileCount; i++) { 108 | const nameLength = view.getUint32(offset.forward(4)); 109 | const name = buffer 110 | .subarray(offset.index, (offset.forward(nameLength), offset.index)) 111 | .toString(); 112 | const chunk: WxapkgChunk = { 113 | name, 114 | nameLength, 115 | offset: view.getUint32(offset.forward(4)), 116 | size: view.getUint32(offset.forward(4)) 117 | }; 118 | 119 | metadata.chunks.push(chunk); 120 | } 121 | 122 | return metadata; 123 | } 124 | 125 | static decrypt(buffer: Buffer, appid: string): Buffer { 126 | const salt = "saltiest"; 127 | const iv = "the iv: 16 bytes"; 128 | const cryptPkgHeaderLength = 1024; 129 | const dk = pbkdf2Sync( 130 | Buffer.from(appid), 131 | Buffer.from(salt), 132 | 1000, 133 | 32, 134 | "sha1" 135 | ); 136 | const head = Buffer.alloc(cryptPkgHeaderLength); 137 | const bodyKey = 138 | appid.length < 2 ? 0x66 : appid.charCodeAt(appid.length - 2); 139 | const body = buffer 140 | .subarray( 141 | WxapkgUnpack.cryptPkgHeaderFlagConst.length + cryptPkgHeaderLength 142 | ) 143 | .map((it) => bodyKey ^ it); 144 | 145 | createDecipheriv("aes-256-cbc", dk, Buffer.from(iv)) 146 | .setAutoPadding(false) 147 | .update( 148 | buffer.subarray( 149 | WxapkgUnpack.cryptPkgHeaderFlagConst.length, 150 | WxapkgUnpack.cryptPkgHeaderFlagConst.length + cryptPkgHeaderLength 151 | ) 152 | ) 153 | .copy(head); 154 | 155 | return Buffer.concat([head.subarray(0, 1023), body]); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /packages/wechat/src/classes/wxml-code-parser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-18 10:30:51 5 | */ 6 | 7 | import * as babel from "@babel/core"; 8 | import generate from "@babel/generator"; 9 | import traverse from "@babel/traverse"; 10 | 11 | export type WxmlRenderInfoRecord = Record; 12 | 13 | export type WxmlRenderInfo = { 14 | renderFunctionName: string; 15 | renderFunctionCode: string; 16 | opsFunctionName: string; 17 | opsFunctionCode: string; 18 | opsArray: OpsArray; 19 | xArray: string[]; 20 | defines: Record>; 21 | entrys: Record; 22 | fileName: string; 23 | templateName?: string; 24 | }; 25 | 26 | export type FunctionName = string; 27 | export type FunctionRecord = Record< 28 | FunctionName, 29 | { 30 | code: string; 31 | } 32 | >; 33 | 34 | export type OpsArray = Array; 35 | 36 | export class WxmlCodeParser { 37 | parse(ast: babel.ParseResult): WxmlRenderInfoRecord { 38 | let wxmlRecord: WxmlRenderInfoRecord = {}; 39 | 40 | traverse(ast, { 41 | AssignmentExpression: (path) => { 42 | if ( 43 | /\$gwx\d*$|\$gwx\d*_[A-Za-z_0-9]+/.test( 44 | (path.node.left as babel.types.Identifier).name 45 | ) 46 | ) { 47 | wxmlRecord = { 48 | ...wxmlRecord, 49 | ...this.getWxmRenderFunctionRecordInScope(path) 50 | }; 51 | } 52 | } 53 | }); 54 | 55 | return wxmlRecord; 56 | } 57 | 58 | getWxmRenderFunctionRecordInScope( 59 | scopePath: babel.NodePath 60 | ): WxmlRenderInfoRecord { 61 | const wxmlRecord: WxmlRenderInfoRecord = {}; 62 | const xArray = this.getXArrayInScope(scopePath); 63 | const mFunctionRecord = this.getMFunctionRecordInScope(scopePath); 64 | const opsFunctionRecord = this.getOpsFunctionRecord(scopePath); 65 | const defines: Record> = {}; 66 | const entrys: Record = {}; 67 | 68 | function getEntrysArrayElement( 69 | element: babel.types.Expression | babel.types.PatternLike 70 | ): unknown { 71 | if (element.type === "Identifier") { 72 | return element.name; 73 | } 74 | if (element.type === "ArrayExpression") { 75 | return element.elements.map((it2) => { 76 | if (it2?.type === "MemberExpression") { 77 | if ( 78 | it2.object.type === "Identifier" && 79 | it2.object.name === "x" && 80 | it2.property.type === "NumericLiteral" 81 | ) { 82 | return xArray[it2.property.value]; 83 | } 84 | } 85 | return "getEntrysArrayElement: unknown of elements"; 86 | }); 87 | } 88 | return "getEntrysArrayElement: unknown"; 89 | } 90 | 91 | scopePath.traverse({ 92 | AssignmentExpression: (path) => { 93 | // d_[x[0]]['xxxxxxx'] = function() {} 94 | if ( 95 | path.node.left.type === "MemberExpression" && 96 | path.node.left.object.type === "MemberExpression" && 97 | path.node.left.object.object.type === "Identifier" && 98 | path.node.left.object.object.name === "d_" && 99 | path.node.left.object.property.type === "MemberExpression" && 100 | path.node.left.object.property.object.type === "Identifier" && 101 | path.node.left.object.property.object.name === "x" && 102 | path.node.right.type === "FunctionExpression" 103 | ) { 104 | const index = ( 105 | path.node.left.object.property 106 | .property as babel.types.NumericLiteral 107 | ).value; 108 | const fileName = xArray[index]; 109 | const renderFunctionName = `_x${index}`; 110 | const templateName = ( 111 | path.node.left.property as babel.types.StringLiteral 112 | ).value; 113 | 114 | path.node.right.id = { 115 | type: "Identifier", 116 | name: renderFunctionName 117 | }; 118 | 119 | const renderFunctionCode = generate(path.node.right).code; 120 | const opsFunctionName = 121 | this.getOpsFunctionNameByRenderCode(renderFunctionCode); 122 | const opsFunctionCode = opsFunctionRecord[opsFunctionName]?.code; 123 | 124 | defines[fileName] = { 125 | ...defines[fileName] 126 | }; 127 | defines[fileName][ 128 | (path.node.left.property as babel.types.StringLiteral).value 129 | ] = renderFunctionName; 130 | 131 | if (fileName && opsFunctionName && opsFunctionCode) { 132 | wxmlRecord[fileName] = { 133 | templateName, 134 | fileName, 135 | renderFunctionName, 136 | renderFunctionCode, 137 | opsFunctionName, 138 | opsFunctionCode, 139 | xArray, 140 | defines, 141 | entrys, 142 | opsArray: this.getOpsArray(opsFunctionName, opsFunctionCode) 143 | }; 144 | } 145 | } 146 | 147 | // e_[x[0]] = { f: m0, j: [], i: [], ti: [], ic: [] }; 148 | if ( 149 | path.node.left.type === "MemberExpression" && 150 | path.node.left.object.type === "Identifier" && 151 | path.node.left.object.name === "e_" && 152 | path.node.left.property.type === "MemberExpression" && 153 | path.node.left.property.object.type === "Identifier" && 154 | path.node.left.property.object.name === "x" && 155 | path.node.right.type === "ObjectExpression" 156 | ) { 157 | const index = ( 158 | path.node.left.property.property as babel.types.NumericLiteral 159 | ).value; 160 | const fileName = xArray[index]; 161 | const properties = (path.node.right.properties || 162 | []) as babel.types.ObjectProperty[]; 163 | const renderProperty = properties.find( 164 | (it) => (it.key as babel.types.Identifier).name === "f" 165 | ) as babel.types.Property; 166 | defines[fileName] = { 167 | ...defines[fileName] 168 | }; 169 | 170 | entrys[fileName] = { 171 | ...entrys[fileName] 172 | }; 173 | 174 | for (const it of properties) { 175 | entrys[fileName][(it.key as babel.types.Identifier).name] = 176 | getEntrysArrayElement(it.value); 177 | } 178 | 179 | if ( 180 | renderProperty && 181 | fileName && 182 | !(defines[fileName] && Object.keys(defines[fileName]).length) 183 | ) { 184 | if (renderProperty.value?.type === "Identifier") { 185 | const renderFunctionName = renderProperty.value.name; 186 | const renderFunctionCode = 187 | mFunctionRecord[renderFunctionName]?.code; 188 | const opsFunctionName = 189 | this.getOpsFunctionNameByRenderCode(renderFunctionCode); 190 | const opsFunctionCode = opsFunctionRecord[opsFunctionName].code; 191 | 192 | if (renderFunctionCode && opsFunctionCode) { 193 | wxmlRecord[fileName] = { 194 | fileName, 195 | renderFunctionName, 196 | renderFunctionCode, 197 | opsFunctionName, 198 | xArray, 199 | defines, 200 | entrys, 201 | opsFunctionCode: opsFunctionCode, 202 | opsArray: this.getOpsArray(opsFunctionName, opsFunctionCode) 203 | }; 204 | } 205 | } 206 | } 207 | } 208 | } 209 | }); 210 | 211 | return wxmlRecord; 212 | } 213 | 214 | getXArrayInScope(scopePath: babel.NodePath): string[] { 215 | let xArray: string[] = []; 216 | scopePath.traverse({ 217 | FunctionDeclaration(path) { 218 | path.skip(); 219 | }, 220 | VariableDeclarator(path) { 221 | if ( 222 | (path.node.id as babel.types.Identifier).name === "x" && 223 | path.node.init?.type == "ArrayExpression" 224 | ) { 225 | xArray = path.node.init.elements.map( 226 | (it) => (it as babel.types.StringLiteral).value 227 | ); 228 | } 229 | } 230 | }); 231 | return xArray; 232 | } 233 | 234 | getOpsFunctionRecord(scopePath: babel.NodePath): FunctionRecord { 235 | const opsFunctionRecord: FunctionRecord = {}; 236 | scopePath.traverse({ 237 | FunctionDeclaration(path) { 238 | const optsFunctionNameRE = /gz\$gwx\d*_([A-Za-z_0-9]+)/; 239 | const functionName = path.node.id?.name || ""; 240 | const matched = functionName.match(optsFunctionNameRE); 241 | if (matched && matched[1] != null) { 242 | opsFunctionRecord[functionName] = { 243 | code: generate(path.node).code 244 | }; 245 | } 246 | } 247 | }); 248 | return opsFunctionRecord; 249 | } 250 | 251 | getMFunctionRecordInScope(scopePath: babel.NodePath): FunctionRecord { 252 | const renderFunctionRecord: FunctionRecord = {}; 253 | const mFunctionNameRe = /m\d+/; 254 | scopePath.traverse({ 255 | FunctionDeclaration(path) { 256 | path.skip(); 257 | }, 258 | VariableDeclarator(path) { 259 | const id = path.node.id as babel.types.Identifier; 260 | const init = path.node.init; 261 | if ( 262 | mFunctionNameRe.test(id.name) && 263 | init?.type == "FunctionExpression" 264 | ) { 265 | init.id = { 266 | type: "Identifier", 267 | name: id.name 268 | }; 269 | renderFunctionRecord[id.name] = { 270 | code: generate(init).code 271 | }; 272 | } 273 | } 274 | }); 275 | 276 | return renderFunctionRecord; 277 | } 278 | 279 | getOpsFunctionNameByRenderCode(code: string): string { 280 | let opsFunctionName = ""; 281 | 282 | traverse(babel.parse(code)!, { 283 | VariableDeclarator(path) { 284 | if ( 285 | path.parentPath?.parentPath?.parentPath?.parentPath?.type === 286 | "Program" 287 | ) { 288 | const id = path.node.id as babel.types.Identifier; 289 | const init = path.node.init as babel.types.CallExpression; 290 | if (id.name === "z" && init?.type == "CallExpression") { 291 | opsFunctionName = (init.callee as babel.types.Identifier).name; 292 | } 293 | } 294 | } 295 | }); 296 | 297 | return opsFunctionName; 298 | } 299 | 300 | getOpsArray(opsFunctionName: string, opsFunctionCode: string): OpsArray { 301 | const opsFunction = new Function( 302 | "__WXML_GLOBAL__", 303 | `${opsFunctionCode}; return ${opsFunctionName}();` 304 | ); 305 | 306 | return opsFunction({ 307 | ops_cached: {} 308 | }); 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /packages/wechat/src/classes/wxml-node.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-17 08:48:00 5 | */ 6 | 7 | export enum WxmlTagEnum { 8 | block = "block", 9 | template = "template" 10 | } 11 | 12 | export type WxmlChildren = string | WxmlNode; 13 | 14 | export class WxmlNode { 15 | parent: WxmlNode | null; 16 | children: WxmlChildren[]; 17 | attrs: Record; 18 | proxy: WxmlNode | null; 19 | 20 | constructor( 21 | readonly tag: string, 22 | parent?: WxmlNode | null, 23 | attrs?: Record 24 | ) { 25 | this.attrs = { 26 | ...attrs 27 | }; 28 | this.parent = parent || null; 29 | this.proxy = null; 30 | this.children = []; 31 | } 32 | 33 | get wxVkey(): string | undefined { 34 | return this.attrs["wx:key"]; 35 | } 36 | 37 | set wxVkey(value: string) { 38 | if (WxmlNode.currentIfBlock) { 39 | if (this.proxy) this.proxy = null; 40 | this.appendChild(WxmlNode.currentIfBlock); 41 | this.proxy = WxmlNode.currentIfBlock; 42 | WxmlNode.currentIfBlock = null; 43 | } 44 | this.attrs["wx:key"] = value; 45 | } 46 | 47 | appendChild(child: WxmlChildren): void { 48 | if (this.proxy) return this.proxy.appendChild(child); 49 | 50 | if (child instanceof WxmlNode) { 51 | child.parent = this; 52 | } 53 | this.children.push(child); 54 | } 55 | 56 | appendChildToFirst(child: WxmlChildren): void { 57 | if (this.proxy) return this.proxy.appendChildToFirst(child); 58 | 59 | if (child instanceof WxmlNode) { 60 | child.parent = this; 61 | } 62 | this.children.unshift(child); 63 | } 64 | 65 | toWxml(): string { 66 | let wxml = `<${this.tag}`; 67 | 68 | for (const key in this.attrs) { 69 | wxml = `${wxml} ${key}="${this.attrs[key]}"`; 70 | } 71 | 72 | if (this.children.length) { 73 | wxml += `>\n`; 74 | wxml += this.children 75 | .map((child) => (child instanceof WxmlNode ? child.toWxml() : child)) 76 | .join("\n"); 77 | wxml += ``; 78 | } else { 79 | wxml += `/>`; 80 | } 81 | 82 | return wxml; 83 | } 84 | 85 | optimize(): WxmlNode { 86 | return this; 87 | // // eslint-disable-next-line @typescript-eslint/no-this-alias 88 | // let node: WxmlNode = this; 89 | 90 | // if (node.tag === WxmlTagEnum.block) { 91 | // if (this.children.length === 1) { 92 | // const child = this.children[0]; 93 | 94 | // if (child instanceof WxmlNode) { 95 | // node = new WxmlNode(child.tag); 96 | // node.children = child.children; 97 | 98 | // for (const key in this.attrs) { 99 | // node.attrs[key] = this.attrs[key]; 100 | // } 101 | 102 | // for (const key in child.attrs) { 103 | // node.attrs[key] = child.attrs[key]; 104 | // } 105 | // } 106 | // } 107 | // } 108 | 109 | // if (node.children.length) { 110 | // node.children = this.children 111 | // .filter((it) => { 112 | // if (it instanceof WxmlNode) { 113 | // if (it.tag === WxmlTagEnum.block) { 114 | // if (it.children.length === 0) { 115 | // return false; 116 | // } 117 | // } 118 | // } 119 | // return true; 120 | // }) 121 | // .map((it) => (it instanceof WxmlNode ? it.optimize() : it)); 122 | // } 123 | 124 | // return node; 125 | } 126 | 127 | static currentIfBlock: WxmlNode | null = null; 128 | } 129 | -------------------------------------------------------------------------------- /packages/wechat/src/classes/wxml-reverse-emulator/common.utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-18 21:12:19 5 | */ 6 | 7 | export enum BracketTypeEnum { 8 | braces = "{", 9 | bracket = "[", 10 | parentheses = "(" 11 | } 12 | 13 | export const BracketWrapRecord: Record = { 14 | [BracketTypeEnum.braces]: ["{", "}"], 15 | [BracketTypeEnum.bracket]: ["[", "]"], 16 | [BracketTypeEnum.parentheses]: ["(", ")"] 17 | }; 18 | 19 | export function wrapBracket( 20 | value: string, 21 | type = BracketTypeEnum.braces 22 | ): string { 23 | const [start, end] = BracketWrapRecord[type]; 24 | 25 | { 26 | let index = 0; 27 | while (!value.startsWith(start)) { 28 | value = `${start.substring(index, index + 1)}${value}`; 29 | index++; 30 | } 31 | } 32 | 33 | { 34 | let index = end.length; 35 | while (!value.endsWith(end)) { 36 | value = `${value}${end.substring(index - 1, index)}`; 37 | index--; 38 | } 39 | } 40 | 41 | return value; 42 | } 43 | 44 | export function wrapScope(value: string): string { 45 | while (!value.startsWith("{{")) { 46 | value = `{${value}`; 47 | } 48 | while (!value.endsWith("}}")) { 49 | value = `${value}}`; 50 | } 51 | return value; 52 | } 53 | -------------------------------------------------------------------------------- /packages/wechat/src/classes/wxml-reverse-emulator/gwh.utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-18 22:12:19 5 | */ 6 | 7 | export function $gwh(): { 8 | hn(obj: any, all?: boolean): "h" | "n"; 9 | nh(obj: any, special?: any): { __value__: any; __wxspec__: any }; 10 | rv(obj: any): any; 11 | hm(obj: any): boolean; 12 | } { 13 | class X { 14 | hn(obj: any, all = false): "h" | "n" { 15 | if (typeof obj === "object") { 16 | let cnt = 0; 17 | let any1 = false; 18 | let any2 = false; 19 | for (const x in obj) { 20 | any1 ||= x === "__value__"; 21 | any2 ||= x === "__wxspec__"; 22 | cnt++; 23 | if (cnt > 2) break; 24 | } 25 | return cnt === 2 && 26 | any1 && 27 | any2 && 28 | (all || obj.__wxspec__ !== "m" || this.hn(obj.__value__) === "h") 29 | ? "h" 30 | : "n"; 31 | } 32 | return "n"; 33 | } 34 | 35 | nh(obj: any, special: any = true): { __value__: any; __wxspec__: any } { 36 | return { __value__: obj, __wxspec__: special }; 37 | } 38 | 39 | rv(obj: any): any { 40 | return this.hn(obj, true) === "n" ? obj : this.rv(obj.__value__); 41 | } 42 | 43 | hm(obj: any): boolean { 44 | if (typeof obj === "object") { 45 | let cnt = 0; 46 | let any1 = false; 47 | let any2 = false; 48 | for (const x in obj) { 49 | any1 ||= x === "__value__"; 50 | any2 ||= x === "__wxspec__"; 51 | cnt++; 52 | if (cnt > 2) break; 53 | } 54 | return ( 55 | cnt === 2 && 56 | any1 && 57 | any2 && 58 | (obj.__wxspec__ === "m" || this.hm(obj.__value__)) 59 | ); 60 | } 61 | return false; 62 | } 63 | } 64 | 65 | return new X(); 66 | } 67 | -------------------------------------------------------------------------------- /packages/wechat/src/classes/wxml-reverse-emulator/gwt.utils.ts: -------------------------------------------------------------------------------- 1 | // /** 2 | // * @overview 3 | // * @author AEPKILL 4 | // * @created 2024-04-18 22:12:19 5 | // */ 6 | 7 | // export function $gwrt(should_pass_type_info: boolean, $gdc: any, _ca: any) { 8 | // function ArithmeticEv(ops: any, e: any, s: any, g: any, o: any) { 9 | // const _f = false; 10 | // const rop = ops[0][1]; 11 | // let _a, _b, _c, _d; 12 | // switch (rop) { 13 | // case "?:": 14 | // _a = rev(ops[1], e, s, g, o, _f); 15 | // _d = wh.rv(_a) 16 | // ? rev(ops[2], e, s, g, o, _f) 17 | // : rev(ops[3], e, s, g, o, _f); 18 | // return _d; 19 | // break; 20 | // case "&&": 21 | // _a = rev(ops[1], e, s, g, o, _f); 22 | // _d = rev(ops[2], e, s, g, o, _f); 23 | // _d = _c && _d; 24 | // return _d; 25 | // break; 26 | // case "||": 27 | // return rev(ops[1], e, s, g, o, _f) || rev(ops[2], e, s, g, o, _f); 28 | // break; 29 | // case "+": 30 | // case "*": 31 | // case "/": 32 | // case "%": 33 | // case "|": 34 | // case "^": 35 | // case "&": 36 | // case "===": 37 | // case "==": 38 | // case "!=": 39 | // case "!==": 40 | // case ">=": 41 | // case "<=": 42 | // case ">": 43 | // case "<": 44 | // case "<<": 45 | // case ">>": 46 | // _a = rev(ops[1], e, s, g, o, _f); 47 | // _b = rev(ops[2], e, s, g, o, _f); 48 | // switch (rop) { 49 | // case "+": 50 | // _d = _a + _b; 51 | // break; 52 | // case "*": 53 | // _d = _a * _b; 54 | // break; 55 | // case "/": 56 | // _d = _a / _b; 57 | // break; 58 | // case "%": 59 | // _d = _a % _b; 60 | // break; 61 | // case "|": 62 | // _d = _a | _b; 63 | // break; 64 | // case "^": 65 | // _d = _a ^ _b; 66 | // break; 67 | // case "&": 68 | // _d = _a & _b; 69 | // break; 70 | // case "===": 71 | // _d = _a === _b; 72 | // break; 73 | // case "==": 74 | // _d = _a == _b; 75 | // break; 76 | // case "!=": 77 | // _d = _a != _b; 78 | // break; 79 | // case "!==": 80 | // _d = _a !== _b; 81 | // break; 82 | // case ">=": 83 | // _d = _a >= _b; 84 | // break; 85 | // case "<=": 86 | // _d = _a <= _b; 87 | // break; 88 | // case ">": 89 | // _d = _a > _b; 90 | // break; 91 | // case "<": 92 | // _d = _a < _b; 93 | // break; 94 | // case "<<": 95 | // _d = _a << _b; 96 | // break; 97 | // case ">>": 98 | // _d = _a >> _b; 99 | // break; 100 | // default: 101 | // break; 102 | // } 103 | // return _d; 104 | // break; 105 | // case "-": 106 | // _a = ops.length === 3 ? rev(ops[1], e, s, g, o, _f) : 0; 107 | // _b = 108 | // ops.length === 3 109 | // ? rev(ops[2], e, s, g, o, _f) 110 | // : rev(ops[1], e, s, g, o, _f); 111 | // _d = _a - _b; 112 | // return _d; 113 | // break; 114 | // case "!": 115 | // _a = rev(ops[1], e, s, g, o, _f); 116 | 117 | // return !_a; 118 | // case "~": 119 | // _a = rev(ops[1], e, s, g, o, _f); 120 | // return ~_a; 121 | // default: 122 | // console.warn("unrecognized op" + rop); 123 | // } 124 | // } 125 | // function rev(ops: any, e: any, s: any, g: any, o: any, newap?: any): any { 126 | // const op = ops[0]; 127 | // const _f = false; 128 | // if (typeof newap !== "undefined") o.ap = newap; 129 | // if (typeof op === "object") { 130 | // const vop = op[0]; 131 | // let _a: any, 132 | // _aa: any, 133 | // _b: any, 134 | // _bb: any, 135 | // _d: any, 136 | // _s: any, 137 | // _e: any, 138 | // _ta: any, 139 | // _tb: any, 140 | // _td: any; 141 | // switch (vop) { 142 | // case 2: 143 | // return ArithmeticEv(ops, e, s, g, o); 144 | // break; 145 | // case 4: 146 | // return rev(ops[1], e, s, g, o, _f); 147 | // break; 148 | // case 5: 149 | // switch (ops.length) { 150 | // case 2: 151 | // _a = rev(ops[1], e, s, g, o, _f); 152 | // return [_a]; 153 | // break; 154 | // case 1: 155 | // return []; 156 | // break; 157 | // default: 158 | // _a = rev(ops[1], e, s, g, o, _f); 159 | // _b = rev(ops[2], e, s, g, o, _f); 160 | // _a.push(_b); 161 | // return _a; 162 | // break; 163 | // } 164 | // break; 165 | // case 6: 166 | // _a = rev(ops[1], e, s, g, o); 167 | // let ap = o.ap; 168 | // _aa = _a; 169 | // if (should_pass_type_info) { 170 | // if (_aa === null || typeof _aa === "undefined") { 171 | // return undefined; 172 | // } 173 | // _b = rev(ops[2], e, s, g, o, _f); 174 | // _bb = _b; 175 | // o.ap = ap; 176 | // o.is_affected |= _tb; 177 | // if ( 178 | // _bb === null || 179 | // typeof _bb === "undefined" || 180 | // _bb === "__proto__" || 181 | // _bb === "prototype" || 182 | // _bb === "caller" 183 | // ) { 184 | // return undefined; 185 | // } 186 | // _d = _aa[_bb]; 187 | // if (typeof _d === "function" && !ap) _d = undefined; 188 | 189 | // return _d; 190 | // } else { 191 | // if (_aa === null || typeof _aa === "undefined") { 192 | // return undefined; 193 | // } 194 | // _b = rev(ops[2], e, s, g, o, _f); 195 | // _bb = _b; 196 | // o.ap = ap; 197 | // if ( 198 | // _bb === null || 199 | // typeof _bb === "undefined" || 200 | // _bb === "__proto__" || 201 | // _bb === "prototype" || 202 | // _bb === "caller" 203 | // ) { 204 | // return undefined; 205 | // } 206 | // _d = _aa[_bb]; 207 | // if (typeof _d === "function" && !ap) _d = undefined; 208 | // return _d; 209 | // } 210 | // case 7: 211 | // switch (ops[1][0]) { 212 | // case 11: 213 | // return g; 214 | // case 3: 215 | // _s = wh.rv(s); 216 | // _e = wh.rv(e); 217 | // _b = ops[1][1]; 218 | // if (g && g.f && g.f.hasOwnProperty(_b)) { 219 | // _a = g.f; 220 | // o.ap = true; 221 | // } else { 222 | // _a = 223 | // _s && _s.hasOwnProperty(_b) 224 | // ? s 225 | // : _e && _e.hasOwnProperty(_b) 226 | // ? e 227 | // : undefined; 228 | // } 229 | // if (should_pass_type_info) { 230 | // if (_a) { 231 | // _ta = wh.hn(_a) === "h"; 232 | // _aa = _ta ? wh.rv(_a) : _a; 233 | // _d = _aa[_b]; 234 | // _td = wh.hn(_d) === "h"; 235 | // o.is_affected |= _ta || _td; 236 | // _d = _ta && !_td ? wh.nh(_d, "e") : _d; 237 | // return _d; 238 | // } 239 | // } else { 240 | // if (_a) { 241 | // _ta = wh.hn(_a) === "h"; 242 | // _aa = _ta ? wh.rv(_a) : _a; 243 | // _d = _aa[_b]; 244 | // _td = wh.hn(_d) === "h"; 245 | // o.is_affected |= _ta || _td; 246 | // return wh.rv(_d); 247 | // } 248 | // } 249 | // return undefined; 250 | // } 251 | // break; 252 | // case 8: 253 | // _a = {}; 254 | // _a[ops[1]] = rev(ops[2], e, s, g, o, _f); 255 | // return _a; 256 | // break; 257 | // case 9: 258 | // _a = rev(ops[1], e, s, g, o, _f); 259 | // _b = rev(ops[2], e, s, g, o, _f); 260 | // function merge(_a: any, _b: any, _ow: any): any { 261 | // _ta = wh.hn(_a) === "h"; 262 | // _tb = wh.hn(_b) === "h"; 263 | // _aa = wh.rv(_a); 264 | // _bb = wh.rv(_b); 265 | // for (const k in _bb) { 266 | // if (_ow || !_aa.hasOwnProperty(k)) { 267 | // _aa[k] = should_pass_type_info 268 | // ? _tb 269 | // ? wh.nh(_bb[k], "e") 270 | // : _bb[k] 271 | // : wh.rv(_bb[k]); 272 | // } 273 | // } 274 | // return _a; 275 | // } 276 | // const _c = _a; 277 | // let _ow = true; 278 | // if (typeof ops[1][0] === "object" && ops[1][0][0] === 10) { 279 | // _a = _b; 280 | // _b = _c; 281 | // _ow = false; 282 | // } 283 | // if (typeof ops[1][0] === "object" && ops[1][0][0] === 10) { 284 | // const _r = {}; 285 | // return merge(merge(_r, _a, _ow), _b, _ow); 286 | // } else return merge(_a, _b, _ow); 287 | // break; 288 | // case 10: 289 | // _a = rev(ops[1], e, s, g, o, _f); 290 | // _a = should_pass_type_info ? _a : wh.rv(_a); 291 | // return _a; 292 | // break; 293 | // case 12: 294 | // let _r; 295 | // _a = rev(ops[1], e, s, g, o); 296 | // if (!o.ap) { 297 | // return should_pass_type_info && wh.hn(_a) === "h" 298 | // ? wh.nh(_r, "f") 299 | // : _r; 300 | // } 301 | // ap = o.ap; 302 | // _b = rev(ops[2], e, s, g, o, _f); 303 | // o.ap = ap; 304 | // _ta = wh.hn(_a) === "h"; 305 | // _tb = _ca(_b); 306 | // _aa = wh.rv(_a); 307 | // _bb = wh.rv(_b); 308 | // const snap_bb = $gdc(_bb, "nv_"); 309 | // try { 310 | // _r = 311 | // typeof _aa === "function" 312 | // ? // eslint-disable-next-line prefer-spread 313 | // $gdc(_aa.apply(null, snap_bb)) 314 | // : undefined; 315 | // } catch (e) { 316 | // _r = undefined; 317 | // } 318 | // return should_pass_type_info && (_tb || _ta) ? wh.nh(_r, "f") : _r; 319 | // } 320 | // } else { 321 | // if (op === 3 || op === 1) return ops[1]; 322 | // else if (op === 11) { 323 | // let _a = ""; 324 | // for (let i = 1; i < ops.length; i++) { 325 | // const xp = wh.rv(rev(ops[i], e, s, g, o, _f)); 326 | // _a += typeof xp === "undefined" ? "" : xp; 327 | // } 328 | // return _a; 329 | // } 330 | // } 331 | // } 332 | // function wrapper(ops: any, e: any, s: any, g: any, o: any, newap: any) { 333 | // return rev(ops, e, s, g, o, newap); 334 | // } 335 | // return wrapper; 336 | // } 337 | -------------------------------------------------------------------------------- /packages/wechat/src/classes/wxml-reverse-emulator/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | /** 3 | * @overview 4 | * @author AEPKILL 5 | * @created 2024-04-18 22:12:19 6 | */ 7 | 8 | import * as babel from "@babel/core"; 9 | import generate from "@babel/generator"; 10 | import traverse from "@babel/traverse"; 11 | import * as t from "@babel/types"; 12 | 13 | import { WxmlRenderInfo } from "../wxml-code-parser"; 14 | import { WxmlNode, WxmlTagEnum } from "../wxml-node"; 15 | import { $gwt } from "./rev.utils"; 16 | import { $gwh } from "./gwh.utils"; 17 | import { wrapScope } from "./common.utils"; 18 | 19 | export class WxmlReverseEmulator { 20 | renderFunctionAst: babel.ParseResult; 21 | 22 | constructor(readonly renderInfo: WxmlRenderInfo) { 23 | const { renderFunctionCode } = renderInfo; 24 | this.renderFunctionAst = babel.parse(renderFunctionCode)!; 25 | traverse(this.renderFunctionAst, { 26 | IfStatement(path) { 27 | const { test } = path.node; 28 | 29 | // 去除掉模板递归渲染的检查 30 | if ( 31 | t.isMemberExpression(test) && 32 | t.isIdentifier(test.object, { name: "p_" }) && 33 | t.isIdentifier(test.property, { name: "b" }) 34 | ) { 35 | path.remove(); 36 | return; 37 | } 38 | 39 | const statements: babel.types.CallExpression[] = []; 40 | let current: babel.types.Statement | null | undefined = path.node; 41 | 42 | while (current) { 43 | const { type } = current; 44 | 45 | switch (type) { 46 | case "IfStatement": { 47 | const { test, consequent } = current; 48 | statements.push( 49 | t.callExpression( 50 | t.callExpression( 51 | t.identifier(current == path.node ? "_$If" : "_$ElseIf"), 52 | [test] 53 | ), 54 | [ 55 | t.arrowFunctionExpression( 56 | [], 57 | consequent as babel.types.BlockStatement 58 | ) 59 | ] 60 | ) 61 | ); 62 | current = current.alternate; 63 | break; 64 | } 65 | case "BlockStatement": { 66 | statements.push( 67 | t.callExpression(t.callExpression(t.identifier("_$Else"), []), [ 68 | t.arrowFunctionExpression([], current) 69 | ]) 70 | ); 71 | } 72 | default: { 73 | current = null; 74 | } 75 | } 76 | } 77 | 78 | path.replaceWithMultiple(statements); 79 | } 80 | }); 81 | } 82 | 83 | getWxmlNode(): WxmlNode { 84 | const root = this.renderInfo.templateName 85 | ? new WxmlNode(WxmlTagEnum.template, null, { 86 | name: this.renderInfo.templateName 87 | }) 88 | : new WxmlNode(WxmlTagEnum.block); 89 | const code = generate(this.renderFunctionAst).code; 90 | const wh = $gwh(); 91 | const grb = $gwt(); 92 | const gra = $gwt(); 93 | 94 | function $gdc(o: any, p?: string, r?: number): any { 95 | const rv = o; 96 | if (rv === null || rv === undefined) return rv; 97 | if ( 98 | typeof rv === "string" || 99 | typeof rv === "boolean" || 100 | typeof rv === "number" 101 | ) { 102 | return rv; 103 | } 104 | 105 | if (rv.constructor === Object) { 106 | const copy = {} as any; 107 | for (const k in rv as any) { 108 | if (Object.prototype.hasOwnProperty.call(rv, k)) { 109 | const key = p ? `${p}${k}` : k.substring(3); 110 | copy[key] = $gdc(rv[k], p, r); 111 | } 112 | } 113 | return copy; 114 | } 115 | 116 | if (rv.constructor === Array) { 117 | const copy = [] as any; 118 | for (let i = 0; i < rv.length; i++) { 119 | copy.push($gdc(rv[i], p, r)); 120 | } 121 | return copy; 122 | } 123 | 124 | if (rv.constructor === Date) { 125 | const copy = new Date(); 126 | copy.setTime(rv.getTime()); 127 | return copy; 128 | } 129 | 130 | if (rv.constructor === RegExp) { 131 | let flags = ""; 132 | if (rv.global) flags += "g"; 133 | if (rv.ignoreCase) flags += "i"; 134 | if (rv.multiline) flags += "m"; 135 | return new RegExp(rv.source, flags); 136 | } 137 | 138 | if (r && typeof rv === "function") { 139 | if (r === 1) return $gdc(rv(), undefined, 2); 140 | if (r === 2) return rv; 141 | } 142 | 143 | return null as unknown; 144 | } 145 | 146 | const wfor = ( 147 | to_iter: any, 148 | func: Function, 149 | env: any, 150 | _s: any, 151 | global: any, 152 | father: WxmlNode, 153 | itemname: any, 154 | indexname: any, 155 | keyname: any 156 | ) => { 157 | const block = new WxmlNode(WxmlTagEnum.block, null, { 158 | "wx:for": `{{${to_iter}}}`, 159 | "wx:for-item": itemname, 160 | "wx:for-index": indexname 161 | }); 162 | block.appendChild( 163 | new WxmlReverseEmulator({ 164 | ...this.renderInfo, 165 | renderFunctionName: "wxFor", 166 | renderFunctionCode: func 167 | .toString() 168 | .replace(/function\s*\(/, "function wxFor("), 169 | templateName: void 0 170 | }).getWxmlNode() 171 | ); 172 | father.appendChild(block); 173 | }; 174 | 175 | const context = { 176 | e: this.renderInfo.entrys, 177 | e_: this.renderInfo.entrys, 178 | d_: this.renderInfo.defines, 179 | // scope 180 | s: {}, 181 | // root 182 | r: root, 183 | z: this.renderInfo.opsArray, 184 | // global 185 | gg: {}, 186 | x: this.renderInfo.xArray, 187 | [this.renderInfo.opsFunctionName]: () => { 188 | return this.renderInfo.opsArray; 189 | }, 190 | // global.modules 191 | f_: {}, 192 | p_: {}, 193 | _(parent: WxmlNode, child: WxmlNode) { 194 | parent.appendChild(child); 195 | }, 196 | _v() { 197 | return new WxmlNode(WxmlTagEnum.block); 198 | }, 199 | _n(tag: string) { 200 | return new WxmlNode(tag); 201 | }, 202 | _$If(condition: string) { 203 | const ifBlock = new WxmlNode(WxmlTagEnum.block); 204 | ifBlock.attrs["wx:if"] = wrapScope(condition.toString()); 205 | WxmlNode.currentIfBlock = ifBlock; 206 | 207 | return function (block: () => void) { 208 | block(); 209 | }; 210 | }, 211 | _$elseIf: (condition: string) => { 212 | const elseIfBlock = new WxmlNode(WxmlTagEnum.block); 213 | elseIfBlock.attrs["wx:elif"] = wrapScope(condition.toString()); 214 | WxmlNode.currentIfBlock = elseIfBlock; 215 | 216 | return function (block: () => void) { 217 | block(); 218 | }; 219 | }, 220 | _$else() { 221 | const elseBlock = new WxmlNode(WxmlTagEnum.block); 222 | elseBlock.attrs["wx:else"] = ""; 223 | WxmlNode.currentIfBlock = elseBlock; 224 | 225 | return function (block: () => void) { 226 | block(); 227 | }; 228 | }, 229 | wh, 230 | $gdc, 231 | _da( 232 | node: WxmlNode, 233 | attrname: string, 234 | _opindex: number, 235 | raw: string, 236 | _o: any 237 | ) { 238 | node.attrs[attrname] = $gdc(raw, "", 2); 239 | }, 240 | _rz( 241 | z: any[], 242 | node: WxmlNode, 243 | attrname: string, 244 | opindex: number, 245 | env: any, 246 | scope: any, 247 | global: { opindex?: number } 248 | ): void { 249 | global.opindex = opindex; 250 | const o: {} = {}; 251 | const a = grb(z[opindex], env, scope, global, o); 252 | 253 | context._da(node, attrname, opindex, `${a}`, o); 254 | }, 255 | _mz( 256 | z: any[], 257 | tag: string, 258 | attrs: Array, 259 | generics: Array, 260 | env: any, 261 | scope: any, 262 | global: any 263 | ): any { 264 | const tmp = context._n(tag); 265 | let base = 0; 266 | 267 | for (let i = 0; i < attrs.length; i += 2) { 268 | if (base + Number(attrs[i + 1]) < 0) { 269 | tmp.attrs[attrs[i]] = true; 270 | } else { 271 | context._rz( 272 | z, 273 | tmp, 274 | attrs[i], 275 | Number(base + attrs[i + 1]), 276 | env, 277 | scope, 278 | global 279 | ); 280 | if (base === 0) base = Number(attrs[i + 1]); 281 | } 282 | } 283 | 284 | for (let i = 0; i < generics.length; i += 2) { 285 | if (base + Number(generics[i + 1]) < 0) { 286 | tmp.attrs[generics[i]] = ""; 287 | } else { 288 | let $t = grb(z[base + Number(generics[i + 1])], env, scope, global); 289 | if ($t !== "") { 290 | $t = "wx-" + $t; 291 | } 292 | tmp.attrs[generics[i]] = $t; 293 | if (base === 0) base = Number(generics[i + 1]); 294 | } 295 | } 296 | return tmp; 297 | }, 298 | _ai(i: any, p: any, e: any, me: any, r: any, c: any) { 299 | const x = context._grp(p, e, me); 300 | if (x) { 301 | root.appendChild( 302 | new WxmlNode("import", root, { 303 | src: x 304 | }) 305 | ); 306 | } 307 | }, 308 | _gd(p: string, c: string, e: any, d: any) { 309 | const node = new WxmlNode("template", null, { 310 | is: c 311 | }); 312 | 313 | return function ( 314 | data: string | object, 315 | data2: string, 316 | parent: WxmlNode 317 | ) { 318 | node.attrs["data"] = 319 | typeof data === "object" ? wrapScope(JSON.stringify(data)) : data; 320 | 321 | parent.appendChild(node); 322 | }; 323 | }, 324 | 325 | _1: ( 326 | opindex: number, 327 | env: any, 328 | scope: any, 329 | global: any, 330 | o?: any 331 | ): any => { 332 | o = o || {}; 333 | global.opindex = opindex; 334 | return gra(this.renderInfo.opsArray[opindex], env, scope, global, o); 335 | }, 336 | 337 | _1z( 338 | z: any[], 339 | opindex: number, 340 | env: any, 341 | scope: any, 342 | global: any, 343 | o?: any 344 | ): any { 345 | o = o || {}; 346 | global.opindex = opindex; 347 | return gra(z[opindex], env, scope, global, o); 348 | }, 349 | 350 | _2( 351 | opindex: any, 352 | func: any, 353 | env: any, 354 | scope: any, 355 | global: any, 356 | father: any, 357 | itemname: any, 358 | indexname: any, 359 | keyname: any 360 | ) { 361 | const o: {} = {}; 362 | const to_iter = context._1(opindex, env, scope, global); 363 | wfor( 364 | to_iter, 365 | func, 366 | env, 367 | scope, 368 | global, 369 | father, 370 | itemname, 371 | indexname, 372 | keyname 373 | ); 374 | }, 375 | _2z( 376 | z: any, 377 | opindex: any, 378 | func: any, 379 | env: any, 380 | scope: any, 381 | global: any, 382 | father: any, 383 | itemname: any, 384 | indexname: any, 385 | keyname: any 386 | ) { 387 | const o: {} = {}; 388 | const to_iter = context._1z(z, opindex, env, scope, global); 389 | 390 | wfor( 391 | to_iter, 392 | func, 393 | env, 394 | scope, 395 | global, 396 | father, 397 | itemname, 398 | indexname, 399 | keyname 400 | ); 401 | }, 402 | _oz( 403 | z: any[], 404 | opindex: number, 405 | env: any, 406 | scope: any, 407 | global: { opindex?: number } 408 | ): any { 409 | global.opindex = opindex; 410 | const nothing = {}; 411 | 412 | const r: any = grb(z[opindex], env, scope, global, nothing); 413 | 414 | return r; 415 | }, 416 | _grp( 417 | p: string, 418 | e: { [key: string]: any }, 419 | me: string 420 | ): string | undefined { 421 | if (p[0] !== "/") { 422 | const mepart = me.split("/").slice(0, -1); 423 | const ppart = p.split("/"); 424 | for (let i = 0; i < ppart.length; i++) { 425 | if (ppart[i] === "..") { 426 | mepart.pop(); 427 | } else if (!ppart[i] || ppart[i] === ".") { 428 | continue; 429 | } else { 430 | mepart.push(ppart[i]); 431 | } 432 | } 433 | p = mepart.join("/"); 434 | } 435 | 436 | if (me.startsWith(".") && p.startsWith("/")) { 437 | p = "." + p; 438 | } 439 | 440 | return e[p] ? p : e[p + ".wxml"] ? p + ".wxml" : undefined; 441 | } 442 | }; 443 | 444 | console.log(this.renderInfo.renderFunctionName, `START`); 445 | 446 | new Function( 447 | ...Object.keys(context), 448 | `${code}; 449 | return ${this.renderInfo.renderFunctionName}(e, s, r, gg)` 450 | )(...Object.values(context)) as null | WxmlNode; 451 | 452 | return root; 453 | } 454 | 455 | getWxml(): string { 456 | const wxmlNode = this.getWxmlNode(); 457 | if (wxmlNode.tag === WxmlTagEnum.block) { 458 | return wxmlNode 459 | .optimize() 460 | .children.map((it) => 461 | it instanceof WxmlNode ? it.optimize().toWxml() : it 462 | ) 463 | .join("\n"); 464 | } 465 | return wxmlNode.optimize().toWxml(); 466 | } 467 | } 468 | -------------------------------------------------------------------------------- /packages/wechat/src/classes/wxml-reverse-emulator/rev.utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-18 22:12:19 5 | */ 6 | 7 | import { BracketTypeEnum, wrapBracket, wrapScope } from "./common.utils"; 8 | 9 | export function $gwt() { 10 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 11 | return function rev(ops: any, _e?: any, _s?: any, _g?: any, _o?: any): any { 12 | return rev2(ops); 13 | }; 14 | 15 | function toScopeJson(value: any): string { 16 | if (value instanceof RegExp) { 17 | return value.source; 18 | } else if (Array.isArray(value)) { 19 | return wrapBracket( 20 | value.map((it) => toScopeJson(it)).join(","), 21 | BracketTypeEnum.bracket 22 | ); 23 | } else if (value != null && typeof value === "object") { 24 | return wrapBracket( 25 | Object.entries(value) 26 | .map(([key, value]) => `${key}:${toScopeJson(value)}`) 27 | .join(","), 28 | BracketTypeEnum.braces 29 | ); 30 | } else { 31 | return JSON.stringify(value); 32 | } 33 | } 34 | 35 | // function getOperatorPrior(op: string, len: number): number { 36 | // switch (op) { 37 | // case "?:": 38 | // return 4; 39 | // case "&&": 40 | // return 6; 41 | // case "||": 42 | // return 5; 43 | // case "*": 44 | // return 14; 45 | // case "/": 46 | // return 14; 47 | // case "%": 48 | // return 14; 49 | // case "|": 50 | // return 7; 51 | // case "^": 52 | // return 8; 53 | // case "&": 54 | // return 9; 55 | // case "!": 56 | // case "~": 57 | // return 16; 58 | // case "===": 59 | // case "==": 60 | // case "!=": 61 | // case "!==": 62 | // return 10; 63 | // case ">=": 64 | // case "<=": 65 | // case ">": 66 | // case "<": 67 | // return 11; 68 | // case "<<": 69 | // case ">>": 70 | // return 12; 71 | // case "+": 72 | // case "-": 73 | // return len === 3 ? 13 : 16; 74 | // default: 75 | // return 0; 76 | // } 77 | // } 78 | 79 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 80 | function getArithmetic(_ops: any[]): string { 81 | return `getArithmetic`; 82 | } 83 | 84 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 85 | function rev2(ops: any[], scope = false): string { 86 | const op = ops[0]; 87 | let result = ""; 88 | 89 | if (typeof op === "object") { 90 | const vop = op[0]; 91 | switch (vop) { 92 | // arithmetic 93 | case 2: { 94 | result = getArithmetic(ops); 95 | break; 96 | } 97 | case 4: { 98 | result = rev2(ops[1], scope); 99 | break; 100 | } 101 | case 5: { 102 | switch (ops.length) { 103 | case 2: { 104 | result = wrapBracket(rev2(ops[1]), BracketTypeEnum.bracket); 105 | break; 106 | } 107 | case 1: { 108 | result = `[]`; 109 | break; 110 | } 111 | default: { 112 | const a = rev2(ops[1]); 113 | const b = rev2(ops[2]); 114 | result = `[${a.replace(/^\[/, "").replace(/\]$/, "")}, ${b.replace(/^\[/, "").replace(/\]$/, "")}]`; 115 | break; 116 | } 117 | } 118 | } 119 | case 6: { 120 | const a = rev2(ops[1]); 121 | 122 | if (a === null || a === undefined) { 123 | result = "undefined"; 124 | break; 125 | } 126 | 127 | const b = rev2(ops[2]); 128 | if ( 129 | b === null || 130 | b === undefined || 131 | b === "__proto__" || 132 | b === "prototype" || 133 | b === "caller" 134 | ) { 135 | result = `undefined`; 136 | break; 137 | } 138 | 139 | if (/^[A-Za-z\_][A-Za-z\d\_]*$/.test(b)) { 140 | result = wrapScope(`${a}.${b}`); 141 | break; 142 | } 143 | 144 | result = wrapScope(`${a}[${b}]`); 145 | break; 146 | } 147 | case 7: { 148 | switch (ops[1][0]) { 149 | case 11: 150 | result = `{global: true}`; 151 | break; 152 | case 3: 153 | result = rev2(ops[1]); 154 | break; 155 | case 8: { 156 | const a = rev2(ops[1]); 157 | const b = rev2(ops[2]); 158 | result = `${a} : ${b}`; 159 | break; 160 | } 161 | case 9: { 162 | const a = rev2(ops[1]); 163 | const b = rev2(ops[2]); 164 | 165 | result = wrapBracket(`...${a}, ...${b}`, BracketTypeEnum.braces); 166 | break; 167 | } 168 | case 10: { 169 | result = rev2(ops[1]); 170 | break; 171 | } 172 | case 12: { 173 | const a = rev2(ops[1]); 174 | const b = rev2(ops[2]); 175 | result = `${a}.apply(null, ${b})`; 176 | break; 177 | } 178 | } 179 | } 180 | } 181 | } else { 182 | if (op === 1) { 183 | result = wrapScope(toScopeJson(ops[1])); 184 | } else if (op === 3) { 185 | result = ops[1]; 186 | } else if (op === 11) { 187 | let _a = ""; 188 | for (let i = 1; i < ops.length; i++) { 189 | const xp = rev2(ops[i]); 190 | _a += typeof xp === "undefined" ? "" : xp; 191 | } 192 | result = _a; 193 | } else { 194 | console.warn(`Rev[0]: 未知的 op: ${op}`); 195 | } 196 | } 197 | 198 | if (result === "") return `unknown - -- ${JSON.stringify(ops)}`; 199 | 200 | return `${result}`; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /packages/wechat/src/enum/bundle-file-name.enum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-16 11:05:37 5 | */ 6 | 7 | export enum BundleFileNameEnum { 8 | appConfig = "app-config.json", 9 | appService = "app-service.js", 10 | workers = "workers.js", 11 | subContext = "subContext.js", 12 | appWxss = "app-wxss.js", 13 | 14 | gameConfig = "game.json", 15 | game = "game.js", 16 | 17 | plugin = "plugin.js", 18 | pluginConfig = "plugin.json" 19 | } 20 | -------------------------------------------------------------------------------- /packages/wechat/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-12 15:00:02 5 | */ 6 | 7 | import { ClassProvider, IContainer, LifecycleEnum } from "husky-di"; 8 | 9 | import { PackWindowsService } from "@/services/pack-windows.service"; 10 | import { ScanWindowsService } from "@/services/scan-windows.service"; 11 | import { UnpackWindowsService } from "@/services/unpack-windows.service"; 12 | import { 13 | ICommandCommonOptions, 14 | IPack, 15 | IScan, 16 | IUnPack, 17 | PlatformEnum 18 | } from "@miniprogram-track/shared"; 19 | 20 | export function registerService( 21 | container: IContainer, 22 | options: ICommandCommonOptions 23 | ): void { 24 | const { platform } = options; 25 | 26 | switch (platform) { 27 | case PlatformEnum.windows: { 28 | container.register( 29 | IScan, 30 | new ClassProvider({ 31 | lifecycle: LifecycleEnum.singleton, 32 | useClass: ScanWindowsService 33 | }) 34 | ); 35 | container.register( 36 | IPack, 37 | new ClassProvider({ 38 | lifecycle: LifecycleEnum.singleton, 39 | useClass: PackWindowsService 40 | }) 41 | ); 42 | container.register( 43 | IUnPack, 44 | new ClassProvider({ 45 | lifecycle: LifecycleEnum.singleton, 46 | useClass: UnpackWindowsService 47 | }) 48 | ); 49 | break; 50 | } 51 | 52 | default: { 53 | throw new Error(`unsupported platform: ${platform}`); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/wechat/src/services/pack-windows.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-12 17:59:49 5 | */ 6 | 7 | import { injectable } from "husky-di"; 8 | 9 | @injectable() 10 | export class PackWindowsService {} 11 | -------------------------------------------------------------------------------- /packages/wechat/src/services/scan-windows.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-12 14:35:23 5 | */ 6 | 7 | import { injectable } from "husky-di"; 8 | import os from "os"; 9 | import path from "path"; 10 | import { promisified } from "regedit"; 11 | import { globSync } from "glob"; 12 | 13 | import { IScan, MiniprogramInfo } from "@miniprogram-track/shared"; 14 | import { getPartialMiniprogramInfoByAppid } from "@/utils/weapp-info.utils"; 15 | 16 | @injectable() 17 | export class ScanWindowsService implements IScan { 18 | async scan(): Promise { 19 | const wechatFileSavePath = await this.getWechatFileSavePath(); 20 | const appletRootPath = path.join(wechatFileSavePath, "WeChat Files/Applet"); 21 | const appidList = globSync(["wx*"], { cwd: appletRootPath }); 22 | const miniprogramInfo = await Promise.all( 23 | appidList.map((it) => getPartialMiniprogramInfoByAppid(it)) 24 | ); 25 | 26 | return miniprogramInfo.map((it) => { 27 | const miniprogramPath = path.join(appletRootPath, it.appid); 28 | const wxapkgPaths = globSync(["**/*.wxapkg"], { 29 | cwd: miniprogramPath 30 | }); 31 | 32 | return { 33 | ...it, 34 | miniprogramDir: miniprogramPath, 35 | wxapkgPaths: wxapkgPaths.map((it) => path.join(miniprogramPath, it)) 36 | }; 37 | }); 38 | } 39 | 40 | async getWechatFileSavePath(): Promise { 41 | const regKey = "HKCU\\SOFTWARE\\Tencent\\WeChat" as const; 42 | const regValues = await promisified.list([regKey]); 43 | const fileSavePath = regValues[regKey].values?.FileSavePath?.value as 44 | | string 45 | | undefined; 46 | 47 | if (fileSavePath) { 48 | return fileSavePath; 49 | } 50 | 51 | return path.join(os.homedir(), "Documents"); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/wechat/src/services/unpack-windows.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-12 17:59:49 5 | */ 6 | 7 | import fs from "fs"; 8 | import { globSync } from "glob"; 9 | import { injectable } from "husky-di"; 10 | import path from "path"; 11 | 12 | import { CodeReverser } from "@/classes/code-reverser"; 13 | import { WxapkgUnpack } from "@/classes/wxapkg-unpack"; 14 | import { IUnPack, UnpackInfo, UnpackOptions } from "@miniprogram-track/shared"; 15 | 16 | @injectable() 17 | export class UnpackWindowsService implements IUnPack { 18 | async unpack(options: UnpackOptions): Promise { 19 | const { 20 | appid, 21 | pkgPath, 22 | miniprogramDir, 23 | restoreCode, 24 | targetDir = path.join(process.cwd(), appid) 25 | } = options; 26 | 27 | const wxapkgPaths: string[] = []; 28 | 29 | if (!miniprogramDir) { 30 | if (!pkgPath) { 31 | throw new Error("pkgPath is required"); 32 | } 33 | if (!fs.existsSync(pkgPath)) { 34 | throw new Error(`pkgPath ${pkgPath} 不存在`); 35 | } 36 | wxapkgPaths.push(pkgPath); 37 | } else { 38 | if (!fs.existsSync(miniprogramDir)) { 39 | throw new Error(`miniprogramDir "${miniprogramDir}" 不存在`); 40 | } 41 | 42 | for (const it of globSync(["**/*.wxapkg"], { 43 | cwd: miniprogramDir 44 | })) { 45 | wxapkgPaths.push(path.join(miniprogramDir, it)); 46 | } 47 | } 48 | for (const it of wxapkgPaths) { 49 | const wxapkg = new WxapkgUnpack({ pkgPath: it, appid }); 50 | const wxapkgBundle = wxapkg.unpack(); 51 | const finallyBundle = restoreCode 52 | ? await new CodeReverser(wxapkgBundle).reverse() 53 | : wxapkgBundle; 54 | 55 | if (restoreCode) { 56 | await finallyBundle.saveTo(path.join(targetDir)); 57 | } else { 58 | await finallyBundle.saveTo(path.join(targetDir, path.parse(it).name)); 59 | } 60 | } 61 | 62 | return { 63 | path: targetDir 64 | }; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/wechat/src/utils/weapp-info.utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 3 | * @author AEPKILL 4 | * @created 2024-04-14 10:39:01 5 | */ 6 | 7 | import { MiniprogramInfo } from "@miniprogram-track/shared"; 8 | 9 | export async function getPartialMiniprogramInfoByAppid( 10 | appid: string 11 | ): Promise> { 12 | type ServerResponse = { 13 | nickname: string; 14 | username: string; 15 | description: string; 16 | avatar: string; 17 | principal_name: string; 18 | appid: string; 19 | }; 20 | 21 | const response = (await fetch("https://kainy.cn/api/weapp/info/", { 22 | body: JSON.stringify({ 23 | appid 24 | }), 25 | method: "POST", 26 | headers: { 27 | "Content-Type": "application/json" 28 | } 29 | }) 30 | .then((r) => r.json()) 31 | .then((r) => r.data || {}) 32 | .catch(() => { 33 | return {}; 34 | })) as ServerResponse; 35 | 36 | return { 37 | appid, 38 | name: response.nickname, 39 | description: response.description, 40 | avatar: response.avatar, 41 | author: response.principal_name, 42 | authorId: response.username 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /packages/wechat/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@miniprogram-track/shared/tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src", 6 | "tsBuildInfoFile": "./dist/.tsbuildinfo", 7 | "declaration": true, 8 | "composite": true, 9 | "baseUrl": "src", 10 | "paths": { 11 | "@/*": ["*"] 12 | } 13 | }, 14 | "include": ["src"] 15 | } 16 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Miniprogram track 2 | 3 | Miniprogram track 是用于小程序安全审计的工具包 4 | 5 | ## 免责声明 6 | 7 | 本工具仅用于安全审计。用户在使用此工具时,应遵守所有适用的法律和法规。用户需对其使用此工具的行为负责,作者不承担因用户的行为产生的任何法律责任。 8 | 9 | ## 预览 10 | 11 | ![example](./docs/assets/images/example.gif) 12 | 13 | ## 安装 14 | 15 | `npm install @miniprogram-track/cli -g` 16 | 17 | ## 快速开始 18 | 19 | 1. 使用 `miniprogram-track scan` 命令扫描小程序 20 | 2. 使用 `miniprogram-track unpack --pkgPath ` 命令解包小程序,解包后的小程序会在当前目录下生成一个名为 `` 的文件夹 21 | 22 | ## Roadmap 23 | 24 | - 微信小程序 25 | - [x] windows 端扫描小程序 26 | - [x] 反编译小程序 27 | - [x] 反编译小程序分包 28 | - [ ] 还原小程序目录结构 29 | - [ ] windows 端打开小程序调试器 30 | 31 | ## 参考资料 32 | 33 | - [微信小程序源码阅读笔记1](https://lrdcq.com/me/read.php/66.htm) 34 | - [wxapkg - 微信小程序反编译工具,.wxapkg 文件扫描 + 解密 + 解包工具](https://github.com/wux1an/wxapkg) 35 | - [将微信小程序(.wxapkg)解包及将包内内容还原为"编译"前的内容的"反编译"器](https://bbs.kanxue.com/thread-225289.htm) 36 | - [SS.UnWxapkg](https://github.com/chenrensong/SS.UnWxapkg) 37 | - [手把手教你—微信小程序抓包解密与反编译工具的使用全过程(超详细)](https://juejin.cn/post/7312678013559636006) 38 | - [【Web实战】零基础微信小程序逆向](https://forum.butian.net/share/2570) 39 | - [windows pc端wxpkg文件解密(非解包)](https://github.com/BlackTrace/pc_wxapkg_decrypt) 40 | - [实战微信逆向教程](https://www.youtube.com/playlist?list=PLwIrqQCQ5pQmA0OBlMtRCx1FLa3KMQXIJ) 41 | - [PcWeChatHooK](https://github.com/zmrbak/PcWeChatHooK) 42 | - [WeChat-Hook](https://github.com/aixed/WeChat-Hook) 43 | - [RevokeMsgPatcher](https://github.com/huiyadanli/RevokeMsgPatcher) 44 | - [unveilr-v2.0.0](https://github.com/junxiaqiao/unveilr-v2.0.0) 45 | 46 | ## License 47 | 48 | MIT © AEPKILL 49 | -------------------------------------------------------------------------------- /wechat-render-op-code.md: -------------------------------------------------------------------------------- 1 | # WechatRenderOpCode 2 | 3 | 微信 Render 函数的操作指令 4 | 5 | ## 操作指令 6 | 7 | - `_` 添加子节点 8 | 9 | ```text 10 | _(r, oD), 等同于: 11 | 12 | 13 | 14 | 15 | ``` 16 | 17 | - `_ai` import 一个组件 18 | 19 | ```text 20 | 21 | _ai(xC, ...others) others 不用处理,xC 就是 src 22 | 23 | 24 | 25 | ``` 26 | 27 | - `_v` 创建一个 Virtual 节点 28 | 29 | ```text 30 | 31 | ``` 32 | 33 | - `_oz` 执行一条指令 34 | 35 | ```text 36 | 37 | _oz(z, opindex, env, scope, global) 38 | 39 | z 是缓存好指令集 40 | opindex 是指令集的索引 41 | env 是环境变量 42 | scope 是 data 对象 43 | global 是 globalData 对象 44 | 45 | 46 | 这个比较复杂,要根据 z 数组里面的指令来还原 47 | 48 | ``` 49 | 50 | - `_n` 创建一个节点 51 | 52 | ```text 53 | _n('view') 54 | 55 | 56 | ``` 57 | 58 | ## 帮助函数 59 | 60 | - `grb` 61 | - `$gbc` 这个在做一个深拷贝 62 | - `wh` 相关是在给对象做包装和判断 63 | - `_ca` 是在判断是否是包装对象 64 | --------------------------------------------------------------------------------