├── .DS_Store ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── decryption-tool ├── UnpackMiniApp.exe └── wxpack │ └── 这个是解密后的输出文件夹.txt ├── package.json ├── pnpm-lock.yaml ├── src ├── bin │ └── wedecode │ │ ├── common.ts │ │ ├── enum.ts │ │ ├── inquirer.ts │ │ ├── main-commander-process.ts │ │ ├── scan.ts │ │ └── wedecode.ts ├── constant │ └── index.ts ├── decompilation-controller.ts ├── interface │ ├── app-decompilation.ts │ ├── base-decompilation.ts │ ├── game-decompilation.ts │ ├── ployfill-cover.ts │ └── unpack-wxapkg.ts ├── polyfill │ └── @babel │ │ └── runtime │ │ └── helpers │ │ └── typeof.js ├── type │ └── index.ts └── utils │ ├── common.ts │ ├── create-vm.ts │ ├── decompile-wxml.ts │ ├── fs-process.ts │ ├── get-pack-codeInfo.ts │ ├── get-z.ts │ └── wx-dom.ts ├── test ├── command.mjs ├── command1.js └── index.html ├── tsconfig.json └── vite.config.ts /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biggerstar/wedecode/06f10b2fb8a529d4fb5ce3ad7dca2a46d6fce6df/.DS_Store -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = false 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | obj/ 2 | gen/ 3 | pkg/* 4 | .ant-targets-build.xml 5 | project.properties 6 | auto.prop 7 | .classpath 8 | .settings/ 9 | *.iml 10 | dist 11 | .idea/ 12 | local.properties 13 | build.xml 14 | 6.1-main.iml 15 | .gradle/ 16 | build/ 17 | captures/ 18 | node_modules 19 | wxapkg 20 | /pkg/ 21 | test/js/ 22 | OUTPUT 23 | OUTPUT-GAME 24 | .DS_Store 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Wedecode

2 |
3 |

4 | 5 | npm package 6 | 7 | 8 | license 9 | 10 | license 11 | 12 |

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

10 | 开始调试吧 => 11 |

12 | 18 | 19 | 20 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "lib": [ 6 | "esnext", 7 | "dom" 8 | ], 9 | "baseUrl": ".", 10 | "outDir": "dist", 11 | "declaration": false, 12 | "downlevelIteration": true, 13 | "strict": false, 14 | "allowJs": true, 15 | "noImplicitThis": true, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "resolveJsonModule": true, 19 | "noUnusedLocals": false, 20 | "noFallthroughCasesInSwitch": true, 21 | "allowSyntheticDefaultImports": true, 22 | "esModuleInterop": true, 23 | "experimentalDecorators": true, 24 | "noImplicitAny": false, 25 | "strictNullChecks": false, 26 | "moduleResolution": "node", 27 | "importHelpers": true, 28 | "paths": { 29 | "@/*": [ 30 | "./src/*" 31 | ] 32 | } 33 | }, 34 | "compileOnSave": false, 35 | "include": [ 36 | "src", 37 | "test", 38 | "typings" 39 | ], 40 | "exclude": [ 41 | "dist", 42 | "pkg", 43 | "node_modules" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import {resolve} from 'node:path'; 2 | import {cwd} from 'node:process' 3 | import copy from "rollup-plugin-copy"; 4 | import {defineConfig} from "vite"; 5 | import { builtinModules } from 'node:module' 6 | import pkg from './package.json' 7 | 8 | const external = [ 9 | ...builtinModules, 10 | ...builtinModules.map(name=> `node:${name}`), 11 | ...Object.keys(pkg.dependencies), 12 | ] 13 | 14 | export default defineConfig( { 15 | resolve: { 16 | extensions: [".ts", ".js", '.tsx', '.mjs'], 17 | alias: { 18 | "@": resolve(cwd(), 'src'), 19 | types: resolve(cwd(), 'src/types') 20 | } 21 | }, 22 | build: { 23 | emptyOutDir: false, 24 | minify: false, 25 | outDir: resolve(cwd(), 'dist'), 26 | rollupOptions: { 27 | external, 28 | output: { 29 | sourcemap: false, 30 | globals: {} 31 | }, 32 | treeshake: true 33 | }, 34 | lib: { 35 | entry: resolve(cwd(), './src/bin/wedecode/wedecode.ts'), 36 | formats: ['es'], 37 | name: 'wedecode', 38 | fileName: () => 'wedecode.js', 39 | }, 40 | }, 41 | plugins: [ 42 | copy({ 43 | targets: [ 44 | // 复制内置 polyfill 45 | { src: 'src/polyfill', dest: 'dist' }, 46 | ] 47 | }), 48 | ] 49 | }) 50 | 51 | --------------------------------------------------------------------------------