├── .github └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.sh ├── package-lock.json ├── package.json ├── patches └── scratch-vm+2.1.46.patch ├── src ├── build │ ├── noop-loader.js │ ├── noop-module │ │ └── index.js │ ├── scratch-vm │ │ ├── engine │ │ │ └── tw-font-manager.js │ │ ├── extension-support │ │ │ └── extension-manager.js │ │ ├── import │ │ │ ├── load-costume.js │ │ │ └── load-sound.js │ │ ├── io │ │ │ ├── clock.js │ │ │ ├── cloud.js │ │ │ ├── keyboard.js │ │ │ ├── mouse.js │ │ │ ├── mouseWheel.js │ │ │ ├── userData.js │ │ │ └── video.js │ │ ├── serialization │ │ │ ├── deserialize-assets.js │ │ │ ├── serialize-assets.js │ │ │ └── tw-costume-import-export.js │ │ └── util │ │ │ └── log.js │ └── text-encoding │ │ └── index.js ├── index.js └── kattio.js ├── tests ├── aplusb.sb ├── aplusb.sb2 ├── aplusb.sb3 ├── echo.json ├── echo.sb3 ├── invalid.sb3 ├── music_extension.sb3 ├── permutation.sb3 ├── say_think.sb3 ├── stopall.sb3 ├── sum_1ton.sb3 └── test.py └── webpack.config.js /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | release: 7 | types: [published] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [20.x] 16 | python-version: ['3.11'] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Set up Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | - name: Compile ldid 29 | run: | 30 | sudo apt install -y libplist-dev 31 | git clone git://git.saurik.com/ldid.git 32 | g++ -pipe -o ldid/ldid ldid/ldid.cpp -I. -x c ldid/lookup2.c -lcrypto -lplist -Os -fwhole-program -flto -s 33 | echo "${{github.workspace}}/ldid" >> $GITHUB_PATH 34 | - run: npm ci 35 | - run: npm run build 36 | - run: python tests/test.py 37 | - name: Upload to Artifacts 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: Artifacts 41 | path: bin/*.zip 42 | - name: Upload to Release 43 | if: github.event_name == 'release' 44 | uses: svenstaro/upload-release-action@v2 45 | with: 46 | repo_token: ${{ secrets.GITHUB_TOKEN }} 47 | file: bin/*.zip 48 | tag: ${{ github.ref }} 49 | file_glob: true 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bin 3 | dist 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # scratch-run changelog 2 | 3 | ### v0.1.5 4 | 5 | - Fix `fs.readSync` potentially throwing EOF error on Windows 6 | 7 | ### v0.1.4 8 | 9 | - Patch compiler to squeeze more performance 10 | 11 | ### v0.1.3 12 | 13 | - Fix infinite loop when `control.stopAll` is used 14 | - Add argument `--print-generated-js` for printing generated JavaScript code 15 | 16 | ### v0.1.2 17 | 18 | - Patch compiler to squeeze more performance 19 | 20 | ### v0.1.1 21 | 22 | - Fix `--version` not printing version 23 | 24 | ### v0.1.0 25 | 26 | - Add argument `--buffer-stdout` 27 | - Use [TurboWarp/scratch-vm](https://github.com/TurboWarp/scratch-vm) 28 | - Patch `scratch-vm` to remove unused functionality 29 | - Significantly reduce bundle size 30 | 31 | As a result, `scratch-run` nows runs much faster while consuming less memory. 32 | 33 | ### v0.0.11 34 | 35 | - Use Node 16.16.0 36 | 37 | ### v0.0.10 38 | 39 | - Use custom Queue for lines and ask_queue 40 | - Hack to speeding up `vm.runtime._step` calls 41 | 42 | Thank @quangloc99 for the idea and implementation. 43 | 44 | ### v0.0.9 45 | 46 | - Use Node 16.14.2 47 | - Block extensions (e.g., music) 48 | 49 | ### v0.0.8 50 | 51 | - Use Node 16.13.2 52 | - Fix bug when using block Think with a number 53 | 54 | ### v0.0.7 55 | 56 | - Use Node 16.13.0 57 | - Add binaries for arm64 58 | 59 | #### v0.0.6 60 | 61 | - Add argument `--check` for validating Scratch file but not running. 62 | 63 | #### v0.0.5 64 | 65 | - Do not print newline character for Think block 66 | - Print error to stderr 67 | - Fix error `Cannot find module 'text-encoding'` 68 | 69 | #### v0.0.4 70 | 71 | - Minify code with ncc before passing to pkg for compiling 72 | - Correctly check whitespace characters (space, horizontal tab, vertical tab, form feed) 73 | 74 | #### v0.0.3 75 | 76 | - Disable bytecode generation 77 | 78 | #### v0.0.2 79 | 80 | - Switch to `readline` as `readline-sync` does not work inside sandbox. 81 | - Default to reading line by line. To read token by token, set the question for the `Ask () and Wait` block to `read_token`. 82 | 83 | #### v0.0.1 84 | 85 | - Initial release 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 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 Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scratch-run [![Build Status](https://github.com/VNOI-Admin/scratch-run/actions/workflows/main.yml/badge.svg)](https://github.com/VNOI-Admin/scratch-run/actions/) 2 | 3 | scratch-run is a CLI interpreter for Scratch based on [scratch-vm](https://github.com/TurboWarp/scratch-vm). 4 | 5 | scratch-run was created to judge solutions written in Scratch. It is used mainly in our official online judge [VNOJ](https://github.com/VNOI-Admin/OJ), but it can also be used separately. 6 | 7 | scratch-run is written in Node.js and packed with [pkg](https://github.com/vercel/pkg). No dependencies are required for running. 8 | 9 | ## Installation 10 | 11 | Prebuilt binaries are available in [Releases](https://github.com/VNOI-Admin/scratch-run/releases). 12 | 13 | ## Usage 14 | 15 | ```bash 16 | scratch-run [scratch file] 17 | ``` 18 | 19 | For example: 20 | 21 | ```bash 22 | scratch-run tests/echo.sb3 23 | ``` 24 | 25 | Type in something and it will be echoed back! 26 | 27 | For more detailed instructions, see our [wiki page](https://github.com/VNOI-Admin/scratch-run/wiki). 28 | 29 | ## Build Instructions 30 | 31 | You need Node.js and npm to build. 32 | 33 | ```bash 34 | git clone https://github.com/VNOI-Admin/scratch-run.git 35 | cd scratch-run 36 | npm install 37 | npm run build 38 | ``` 39 | 40 | Built binaries will be saved in `build` directory. 41 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | NODE_VERSION=16.16.0 4 | 5 | VERSION=$(node -p -e "require('./package.json').version") 6 | BUILD_CMD="npx pkg ../dist/index.js" 7 | 8 | rm -rf bin dist 9 | npx webpack 10 | 11 | mkdir bin 12 | cd bin 13 | 14 | # Linux amd64 15 | $BUILD_CMD -t node"$NODE_VERSION"-linux-x64 --out-path linux-amd64 16 | cd linux-amd64 17 | mv index scratch-run 18 | zip "../scratch-run_"$VERSION"_linux_amd64.zip" scratch-run 19 | cd .. 20 | 21 | # Linux arm64 22 | $BUILD_CMD -t node"$NODE_VERSION"-linux-arm64 --out-path linux-arm64 23 | cd linux-arm64 24 | mv index scratch-run 25 | zip "../scratch-run_"$VERSION"_linux_arm64.zip" scratch-run 26 | cd .. 27 | 28 | # macOS amd64 29 | $BUILD_CMD -t node"$NODE_VERSION"-macos-x64 --out-path macos-amd64 30 | cd macos-amd64 31 | mv index scratch-run 32 | zip "../scratch-run_"$VERSION"_macos_amd64.zip" scratch-run 33 | cd .. 34 | 35 | # macOS arm64 36 | $BUILD_CMD -t node"$NODE_VERSION"-macos-arm64 --out-path macos-arm64 37 | cd macos-arm64 38 | mv index scratch-run 39 | zip "../scratch-run_"$VERSION"_macos_arm64.zip" scratch-run 40 | cd .. 41 | 42 | # Windows amd64 43 | $BUILD_CMD -t node"$NODE_VERSION"-win-x64 --out-path win-amd64 44 | cd win-amd64 45 | mv index.exe scratch-run.exe 46 | zip "../scratch-run_"$VERSION"_win_amd64.zip" scratch-run.exe 47 | cd .. 48 | 49 | # Windows arm64 50 | $BUILD_CMD -t node"$NODE_VERSION"-win-arm64 --out-path win-arm64 51 | cd win-arm64 52 | mv index.exe scratch-run.exe 53 | zip "../scratch-run_"$VERSION"_win_arm64.zip" scratch-run.exe 54 | cd .. 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scratch-run", 3 | "version": "0.1.5", 4 | "description": "Run Scratch from command line", 5 | "main": "./index.js", 6 | "scripts": { 7 | "start": "node ./index.js", 8 | "build": "./build.sh", 9 | "test": "python tests/test.py", 10 | "postinstall": "patch-package" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/VNOI-Admin/scratch-run.git" 15 | }, 16 | "keywords": [ 17 | "scratch" 18 | ], 19 | "author": "Le Bao Hiep ", 20 | "license": "agpl-3.0", 21 | "bugs": { 22 | "url": "https://github.com/VNOI-Admin/scratch-run/issues" 23 | }, 24 | "homepage": "https://github.com/VNOI-Admin/scratch-run#readme", 25 | "dependencies": { 26 | "fastestsmallesttextencoderdecoder": "^1.0.22", 27 | "minimist": "^1.2.8", 28 | "scratch-vm": "git+https://github.com/TurboWarp/scratch-vm.git#e4e461ac84b1d61a3e7468aa6dbc8b56a2ece35e" 29 | }, 30 | "devDependencies": { 31 | "esbuild-loader": "^4.0.2", 32 | "patch-package": "^8.0.0", 33 | "pkg": "^5.8.1", 34 | "webpack": "4.47.0", 35 | "webpack-bundle-analyzer": "^4.10.1", 36 | "webpack-cli": "4.10.0" 37 | }, 38 | "overrides": { 39 | "webpack@4.47.0": { 40 | "terser-webpack-plugin": "^4.2.3" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /patches/scratch-vm+2.1.46.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/scratch-vm/src/compiler/compat-blocks.js b/node_modules/scratch-vm/src/compiler/compat-blocks.js 2 | index 1c9d8f5..67f2221 100644 3 | --- a/node_modules/scratch-vm/src/compiler/compat-blocks.js 4 | +++ b/node_modules/scratch-vm/src/compiler/compat-blocks.js 5 | @@ -8,12 +8,8 @@ 6 | const stacked = [ 7 | 'looks_changestretchby', 8 | 'looks_hideallsprites', 9 | - 'looks_say', 10 | - 'looks_sayforsecs', 11 | 'looks_setstretchto', 12 | 'looks_switchbackdroptoandwait', 13 | - 'looks_think', 14 | - 'looks_thinkforsecs', 15 | 'motion_align_scene', 16 | 'motion_glidesecstoxy', 17 | 'motion_glideto', 18 | @@ -21,7 +17,6 @@ const stacked = [ 19 | 'motion_pointtowards', 20 | 'motion_scroll_right', 21 | 'motion_scroll_up', 22 | - 'sensing_askandwait', 23 | 'sensing_setdragmode', 24 | 'sound_changeeffectby', 25 | 'sound_changevolumeby', 26 | diff --git a/node_modules/scratch-vm/src/compiler/irgen.js b/node_modules/scratch-vm/src/compiler/irgen.js 27 | index c2cf19d..1ea3813 100644 28 | --- a/node_modules/scratch-vm/src/compiler/irgen.js 29 | +++ b/node_modules/scratch-vm/src/compiler/irgen.js 30 | @@ -1147,6 +1147,26 @@ class ScriptTreeGenerator { 31 | kind: 'timer.reset' 32 | }; 33 | 34 | + case 'looks_say': 35 | + case 'looks_sayforsecs': 36 | + return { 37 | + kind: 'looks.say', 38 | + message: this.descendInputOfBlock(block, 'MESSAGE') 39 | + } 40 | + 41 | + case 'looks_think': 42 | + case 'looks_thinkforsecs': 43 | + return { 44 | + kind: 'looks.think', 45 | + message: this.descendInputOfBlock(block, 'MESSAGE') 46 | + } 47 | + 48 | + case 'sensing_askandwait': 49 | + return { 50 | + kind: 'sensing.ask', 51 | + question: this.descendInputOfBlock(block, 'QUESTION') 52 | + }; 53 | + 54 | default: { 55 | const opcodeFunction = this.runtime.getOpcodeFunction(block.opcode); 56 | if (opcodeFunction) { 57 | @@ -1363,7 +1383,7 @@ class ScriptTreeGenerator { 58 | } 59 | 60 | // Non-warp direct recursion yields. 61 | - if (!this.script.isWarp) { 62 | + if (false && !this.script.isWarp) { 63 | if (procedureCode === this.script.procedureCode) { 64 | this.script.yields = true; 65 | } 66 | @@ -1435,9 +1455,9 @@ class ScriptTreeGenerator { 67 | } 68 | 69 | analyzeLoop () { 70 | - if (!this.script.isWarp || this.script.warpTimer) { 71 | - this.script.yields = true; 72 | - } 73 | + // if (!this.script.isWarp || this.script.warpTimer) { 74 | + // this.script.yields = true; 75 | + // } 76 | } 77 | 78 | readTopBlockComment (commentId) { 79 | diff --git a/node_modules/scratch-vm/src/compiler/jsexecute.js b/node_modules/scratch-vm/src/compiler/jsexecute.js 80 | index ec7d683..e97236b 100644 81 | --- a/node_modules/scratch-vm/src/compiler/jsexecute.js 82 | +++ b/node_modules/scratch-vm/src/compiler/jsexecute.js 83 | @@ -445,7 +445,6 @@ runtimeFunctions.listReplace = `const listReplace = (list, idx, value) => { 84 | return; 85 | } 86 | list.value[index] = value; 87 | - list._monitorUpToDate = false; 88 | }`; 89 | 90 | /** 91 | @@ -460,7 +459,6 @@ runtimeFunctions.listInsert = `const listInsert = (list, idx, value) => { 92 | return; 93 | } 94 | list.value.splice(index, 0, value); 95 | - list._monitorUpToDate = false; 96 | }`; 97 | 98 | /** 99 | @@ -478,7 +476,6 @@ runtimeFunctions.listDelete = `const listDelete = (list, idx) => { 100 | return; 101 | } 102 | list.value.splice(index, 1); 103 | - list._monitorUpToDate = false; 104 | }`; 105 | 106 | /** 107 | diff --git a/node_modules/scratch-vm/src/compiler/jsgen.js b/node_modules/scratch-vm/src/compiler/jsgen.js 108 | index bf9cbf3..750da73 100644 109 | --- a/node_modules/scratch-vm/src/compiler/jsgen.js 110 | +++ b/node_modules/scratch-vm/src/compiler/jsgen.js 111 | @@ -781,7 +781,7 @@ class JSGenerator { 112 | } 113 | this.source += '}\n'; // close switch 114 | this.source += `if (!${branchVariable}.isLoop) break;\n`; 115 | - this.yieldLoop(); 116 | + // this.yieldLoop(); 117 | this.source += '}\n'; // close while 118 | } else { 119 | throw new Error(`Unknown block type: ${blockType}`); 120 | @@ -811,7 +811,7 @@ class JSGenerator { 121 | this.source += `${index}++; `; 122 | this.source += `${this.referenceVariable(node.variable)}.value = ${index};\n`; 123 | this.descendStack(node.do, new Frame(true)); 124 | - this.yieldLoop(); 125 | + // this.yieldLoop(); 126 | this.source += '}\n'; 127 | break; 128 | } 129 | @@ -830,7 +830,7 @@ class JSGenerator { 130 | const i = this.localVariables.next(); 131 | this.source += `for (var ${i} = ${this.descendInput(node.times).asNumber()}; ${i} >= 0.5; ${i}--) {\n`; 132 | this.descendStack(node.do, new Frame(true)); 133 | - this.yieldLoop(); 134 | + // this.yieldLoop(); 135 | this.source += `}\n`; 136 | break; 137 | } 138 | @@ -868,11 +868,11 @@ class JSGenerator { 139 | this.resetVariableInputs(); 140 | this.source += `while (${this.descendInput(node.condition).asBoolean()}) {\n`; 141 | this.descendStack(node.do, new Frame(true)); 142 | - if (node.warpTimer) { 143 | - this.yieldStuckOrNotWarp(); 144 | - } else { 145 | - this.yieldLoop(); 146 | - } 147 | + // if (node.warpTimer) { 148 | + // this.yieldStuckOrNotWarp(); 149 | + // } else { 150 | + // this.yieldLoop(); 151 | + // } 152 | this.source += `}\n`; 153 | break; 154 | 155 | @@ -921,7 +921,6 @@ class JSGenerator { 156 | case 'list.add': { 157 | const list = this.referenceVariable(node.list); 158 | this.source += `${list}.value.push(${this.descendInput(node.item).asSafe()});\n`; 159 | - this.source += `${list}._monitorUpToDate = false;\n`; 160 | break; 161 | } 162 | case 'list.delete': { 163 | @@ -930,12 +929,10 @@ class JSGenerator { 164 | if (index instanceof ConstantInput) { 165 | if (index.constantValue === 'last') { 166 | this.source += `${list}.value.pop();\n`; 167 | - this.source += `${list}._monitorUpToDate = false;\n`; 168 | break; 169 | } 170 | if (+index.constantValue === 1) { 171 | this.source += `${list}.value.shift();\n`; 172 | - this.source += `${list}._monitorUpToDate = false;\n`; 173 | break; 174 | } 175 | // do not need a special case for all as that is handled in IR generation (list.deleteAll) 176 | @@ -955,7 +952,6 @@ class JSGenerator { 177 | const item = this.descendInput(node.item); 178 | if (index instanceof ConstantInput && +index.constantValue === 1) { 179 | this.source += `${list}.value.unshift(${item.asSafe()});\n`; 180 | - this.source += `${list}._monitorUpToDate = false;\n`; 181 | break; 182 | } 183 | this.source += `listInsert(${list}, ${index.asUnknown()}, ${item.asSafe()});\n`; 184 | @@ -1111,10 +1107,10 @@ class JSGenerator { 185 | break; 186 | } 187 | 188 | - const yieldForRecursion = !this.isWarp && procedureCode === this.script.procedureCode; 189 | - if (yieldForRecursion) { 190 | - this.yieldNotWarp(); 191 | - } 192 | + // const yieldForRecursion = !this.isWarp && procedureCode === this.script.procedureCode; 193 | + // if (yieldForRecursion) { 194 | + // this.yieldNotWarp(); 195 | + // } 196 | 197 | if (procedureData.yields) { 198 | this.source += 'yield* '; 199 | @@ -1167,6 +1163,18 @@ class JSGenerator { 200 | break; 201 | } 202 | 203 | + case 'sensing.ask': 204 | + this.source += `runtime.ext_scratch3_sensing._answer = runtime._scratch_run_ask(${this.descendInput(node.question).asString()});\n`; 205 | + break; 206 | + 207 | + case 'looks.say': 208 | + this.source += `runtime._scratch_run_say(${this.descendInput(node.message).asString()});\n`; 209 | + break; 210 | + 211 | + case 'looks.think': 212 | + this.source += `runtime._scratch_run_think(${this.descendInput(node.message).asString()});\n`; 213 | + break; 214 | + 215 | default: 216 | log.warn(`JS: Unknown stacked block: ${node.kind}`, node); 217 | throw new Error(`JS: Unknown stacked block: ${node.kind}`); 218 | diff --git a/node_modules/scratch-vm/src/engine/blocks.js b/node_modules/scratch-vm/src/engine/blocks.js 219 | index 71ace3a..7e6357b 100644 220 | --- a/node_modules/scratch-vm/src/engine/blocks.js 221 | +++ b/node_modules/scratch-vm/src/engine/blocks.js 222 | @@ -1,9 +1,9 @@ 223 | const adapter = require('./adapter'); 224 | const mutationAdapter = require('./mutation-adapter'); 225 | const xmlEscape = require('../util/xml-escape'); 226 | -const MonitorRecord = require('./monitor-record'); 227 | +// const MonitorRecord = require('./monitor-record'); 228 | const Clone = require('../util/clone'); 229 | -const {Map} = require('immutable'); 230 | +// const {Map} = require('immutable'); 231 | const BlocksExecuteCache = require('./blocks-execute-cache'); 232 | const BlocksRuntimeCache = require('./blocks-runtime-cache'); 233 | const log = require('../util/log'); 234 | @@ -709,8 +709,8 @@ class Blocks { 235 | this.runtime.requestBlocksUpdate(); 236 | } 237 | 238 | - const flyoutBlock = block.shadow && block.parent ? this._blocks[block.parent] : block; 239 | - if (flyoutBlock.isMonitored) { 240 | + // const flyoutBlock = block.shadow && block.parent ? this._blocks[block.parent] : block; 241 | + if (false && flyoutBlock.isMonitored) { 242 | this.runtime.requestUpdateMonitor(Map({ 243 | id: flyoutBlock.id, 244 | params: this._getBlockParams(flyoutBlock) 245 | @@ -768,9 +768,9 @@ class Blocks { 246 | block.targetId = null; 247 | } 248 | 249 | - if (wasMonitored && !block.isMonitored) { 250 | + if (false && wasMonitored && !block.isMonitored) { 251 | this.runtime.requestHideMonitor(block.id); 252 | - } else if (!wasMonitored && block.isMonitored) { 253 | + } else if (false && !wasMonitored && block.isMonitored) { 254 | // Tries to show the monitor for specified block. If it doesn't exist, add the monitor. 255 | if (!this.runtime.requestShowMonitor(block.id)) { 256 | this.runtime.requestAddMonitor(MonitorRecord({ 257 | diff --git a/node_modules/scratch-vm/src/engine/execute.js b/node_modules/scratch-vm/src/engine/execute.js 258 | index 7e94ee9..4082784 100644 259 | --- a/node_modules/scratch-vm/src/engine/execute.js 260 | +++ b/node_modules/scratch-vm/src/engine/execute.js 261 | @@ -2,7 +2,7 @@ const BlockUtility = require('./block-utility'); 262 | const BlocksExecuteCache = require('./blocks-execute-cache'); 263 | const log = require('../util/log'); 264 | const Thread = require('./thread'); 265 | -const {Map} = require('immutable'); 266 | +// const {Map} = require('immutable'); 267 | const cast = require('../util/cast'); 268 | 269 | /** 270 | @@ -100,7 +100,7 @@ const handleReport = function (resolvedValue, sequencer, thread, blockCached, la 271 | // Target no longer exists 272 | return; 273 | } 274 | - sequencer.runtime.requestUpdateMonitor(Map({ 275 | + if (false) sequencer.runtime.requestUpdateMonitor(Map({ 276 | id: currentBlockId, 277 | spriteName: targetId ? sequencer.runtime.getTargetById(targetId).getName() : null, 278 | value: resolvedValue 279 | diff --git a/node_modules/scratch-vm/src/engine/runtime.js b/node_modules/scratch-vm/src/engine/runtime.js 280 | index 6cfe302..c7f92f8 100644 281 | --- a/node_modules/scratch-vm/src/engine/runtime.js 282 | +++ b/node_modules/scratch-vm/src/engine/runtime.js 283 | @@ -1,5 +1,5 @@ 284 | const EventEmitter = require('events'); 285 | -const {OrderedMap} = require('immutable'); 286 | +// const {OrderedMap} = require('immutable'); 287 | const ExtendedJSON = require('@turbowarp/json'); 288 | const uuid = require('uuid'); 289 | 290 | @@ -320,12 +320,12 @@ class Runtime extends EventEmitter { 291 | /** 292 | * Ordered map of all monitors, which are MonitorReporter objects. 293 | */ 294 | - this._monitorState = OrderedMap({}); 295 | + // this._monitorState = OrderedMap({}); 296 | 297 | /** 298 | * Monitor state from last tick 299 | */ 300 | - this._prevMonitorState = OrderedMap({}); 301 | + // this._prevMonitorState = OrderedMap({}); 302 | 303 | /** 304 | * Whether the project is in "turbo mode." 305 | @@ -2247,8 +2247,8 @@ class Runtime extends EventEmitter { 306 | this.targets.map(this.disposeTarget, this); 307 | this.extensionStorage = {}; 308 | // tw: explicitly emit a MONITORS_UPDATE instead of relying on implicit behavior of _step() 309 | - const emptyMonitorState = OrderedMap({}); 310 | - if (!emptyMonitorState.equals(this._monitorState)) { 311 | + // const emptyMonitorState = OrderedMap({}); 312 | + if (false && !emptyMonitorState.equals(this._monitorState)) { 313 | this._monitorState = emptyMonitorState; 314 | this.emit(Runtime.MONITORS_UPDATE, this._monitorState); 315 | } 316 | @@ -2484,6 +2484,7 @@ class Runtime extends EventEmitter { 317 | // Clean up threads that were told to stop during or since the last step 318 | this.threads = this.threads.filter(thread => !thread.isKilled); 319 | this.updateThreadMap(); 320 | + this._emitProjectRunStatus(this.threads.length - this._getMonitorThreadCount(this.threads)); 321 | 322 | // Find all edge-activated hats, and add them to threads to be evaluated. 323 | for (const hatType in this._hats) { 324 | @@ -2494,7 +2495,7 @@ class Runtime extends EventEmitter { 325 | } 326 | } 327 | this.redrawRequested = false; 328 | - this._pushMonitors(); 329 | + // this._pushMonitors(); 330 | if (this.profiler !== null) { 331 | if (stepThreadsProfilerId === -1) { 332 | stepThreadsProfilerId = this.profiler.idByName('Sequencer.stepThreads'); 333 | @@ -2540,7 +2541,7 @@ class Runtime extends EventEmitter { 334 | this._refreshTargets = false; 335 | } 336 | 337 | - if (!this._prevMonitorState.equals(this._monitorState)) { 338 | + if (false && !this._prevMonitorState.equals(this._monitorState)) { 339 | this.emit(Runtime.MONITORS_UPDATE, this._monitorState); 340 | this._prevMonitorState = this._monitorState; 341 | } 342 | @@ -2662,10 +2663,10 @@ class Runtime extends EventEmitter { 343 | width = Math.round(Math.max(1, width)); 344 | height = Math.round(Math.max(1, height)); 345 | if (this.stageWidth !== width || this.stageHeight !== height) { 346 | - const deltaX = width - this.stageWidth; 347 | - const deltaY = height - this.stageHeight; 348 | + // const deltaX = width - this.stageWidth; 349 | + // const deltaY = height - this.stageHeight; 350 | // Preserve monitor location relative to the center of the stage 351 | - if (this._monitorState.size > 0) { 352 | + if (false && this._monitorState.size > 0) { 353 | const offsetX = deltaX / 2; 354 | const offsetY = deltaY / 2; 355 | for (const monitor of this._monitorState.valueSeq()) { 356 | @@ -3053,6 +3054,7 @@ class Runtime extends EventEmitter { 357 | * @param {!MonitorRecord} monitor Monitor to add. 358 | */ 359 | requestAddMonitor (monitor) { 360 | + return; 361 | const id = monitor.get('id'); 362 | if (!this.requestUpdateMonitor(monitor)) { // update monitor if it exists in the state 363 | // if the monitor did not exist in the state, add it 364 | @@ -3068,6 +3070,7 @@ class Runtime extends EventEmitter { 365 | * @return {boolean} true if monitor exists in the state and was updated, false if it did not exist. 366 | */ 367 | requestUpdateMonitor (monitor) { 368 | + return true; 369 | const id = monitor.get('id'); 370 | if (this._monitorState.has(id)) { 371 | this._monitorState = 372 | @@ -3089,6 +3092,7 @@ class Runtime extends EventEmitter { 373 | * @param {!string} monitorId ID of the monitor to remove. 374 | */ 375 | requestRemoveMonitor (monitorId) { 376 | + return; 377 | this._monitorState = this._monitorState.delete(monitorId); 378 | } 379 | 380 | @@ -3098,6 +3102,7 @@ class Runtime extends EventEmitter { 381 | * @return {boolean} true if monitor exists and was updated, false otherwise 382 | */ 383 | requestHideMonitor (monitorId) { 384 | + return true; 385 | return this.requestUpdateMonitor(new Map([ 386 | ['id', monitorId], 387 | ['visible', false] 388 | @@ -3111,6 +3116,7 @@ class Runtime extends EventEmitter { 389 | * @return {boolean} true if monitor exists and was updated, false otherwise 390 | */ 391 | requestShowMonitor (monitorId) { 392 | + return true; 393 | return this.requestUpdateMonitor(new Map([ 394 | ['id', monitorId], 395 | ['visible', true] 396 | @@ -3123,6 +3129,7 @@ class Runtime extends EventEmitter { 397 | * @param {!string} targetId Remove all monitors with given target ID. 398 | */ 399 | requestRemoveMonitorByTargetId (targetId) { 400 | + return; 401 | this._monitorState = this._monitorState.filterNot(value => value.targetId === targetId); 402 | } 403 | 404 | diff --git a/node_modules/scratch-vm/src/engine/target.js b/node_modules/scratch-vm/src/engine/target.js 405 | index e9cecfe..ef24579 100644 406 | --- a/node_modules/scratch-vm/src/engine/target.js 407 | +++ b/node_modules/scratch-vm/src/engine/target.js 408 | @@ -4,7 +4,7 @@ const Blocks = require('./blocks'); 409 | const Variable = require('../engine/variable'); 410 | const Comment = require('../engine/comment'); 411 | const uid = require('../util/uid'); 412 | -const {Map} = require('immutable'); 413 | +// const {Map} = require('immutable'); 414 | const log = require('../util/log'); 415 | const StringUtil = require('../util/string-util'); 416 | const VariableUtil = require('../util/variable-util'); 417 | @@ -350,8 +350,8 @@ class Target extends EventEmitter { 418 | name: variable.type === Variable.LIST_TYPE ? 'LIST' : 'VARIABLE', 419 | value: id 420 | }, this.runtime); 421 | - const monitorBlock = blocks.getBlock(variable.id); 422 | - if (monitorBlock) { 423 | + // const monitorBlock = blocks.getBlock(variable.id + 'lebaohiep'); 424 | + if (false && monitorBlock) { 425 | this.runtime.requestUpdateMonitor(Map({ 426 | id: id, 427 | params: blocks._getBlockParams(monitorBlock) 428 | diff --git a/node_modules/scratch-vm/src/serialization/sb2.js b/node_modules/scratch-vm/src/serialization/sb2.js 429 | index 6dc9cef..26e97f9 100644 430 | --- a/node_modules/scratch-vm/src/serialization/sb2.js 431 | +++ b/node_modules/scratch-vm/src/serialization/sb2.js 432 | @@ -16,7 +16,7 @@ const MathUtil = require('../util/math-util'); 433 | const specMap = require('./sb2_specmap'); 434 | const Comment = require('../engine/comment'); 435 | const Variable = require('../engine/variable'); 436 | -const MonitorRecord = require('../engine/monitor-record'); 437 | +// const MonitorRecord = require('../engine/monitor-record'); 438 | const StageLayering = require('../engine/stage-layering'); 439 | const ScratchXUtilities = require('../extension-support/tw-scratchx-utilities'); 440 | 441 | @@ -300,6 +300,7 @@ const globalBroadcastMsgStateGenerator = (function () { 442 | */ 443 | 444 | const parseMonitorObject = (object, runtime, targets, extensions) => { 445 | + return; 446 | // If we can't find the block in the spec map, ignore it. 447 | // This happens for things like Lego Wedo 1.0 monitors. 448 | const mapped = specMap[object.cmd]; 449 | diff --git a/node_modules/scratch-vm/src/serialization/sb3.js b/node_modules/scratch-vm/src/serialization/sb3.js 450 | index b760e05..aa76a63 100644 451 | --- a/node_modules/scratch-vm/src/serialization/sb3.js 452 | +++ b/node_modules/scratch-vm/src/serialization/sb3.js 453 | @@ -8,7 +8,7 @@ const Blocks = require('../engine/blocks'); 454 | const Sprite = require('../sprites/sprite'); 455 | const Variable = require('../engine/variable'); 456 | const Comment = require('../engine/comment'); 457 | -const MonitorRecord = require('../engine/monitor-record'); 458 | +// const MonitorRecord = require('../engine/monitor-record'); 459 | const StageLayering = require('../engine/stage-layering'); 460 | const log = require('../util/log'); 461 | const uid = require('../util/uid'); 462 | @@ -1324,6 +1324,7 @@ const parseScratchObject = function (object, runtime, extensions, zip, assets) { 463 | }; 464 | 465 | const deserializeMonitor = function (monitorData, runtime, targets, extensions) { 466 | + return; 467 | // Monitors position is always stored as position from top-left corner in 480x360 stage. 468 | const xOffset = (runtime.stageWidth - 480) / 2; 469 | const yOffset = (runtime.stageHeight - 360) / 2; 470 | -------------------------------------------------------------------------------- /src/build/noop-loader.js: -------------------------------------------------------------------------------- 1 | module.exports = function noopLoader() { 2 | return 'module.exports = null;'; 3 | }; 4 | -------------------------------------------------------------------------------- /src/build/noop-module/index.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /src/build/scratch-vm/engine/tw-font-manager.js: -------------------------------------------------------------------------------- 1 | class FontManager { 2 | clear() {} 3 | 4 | serializeJSON() { 5 | return null; 6 | } 7 | 8 | serializeAssets() { 9 | return []; 10 | } 11 | 12 | async deserialize() {} 13 | } 14 | 15 | module.exports = FontManager; 16 | -------------------------------------------------------------------------------- /src/build/scratch-vm/extension-support/extension-manager.js: -------------------------------------------------------------------------------- 1 | class SecurityManager { 2 | canLoadExtensionFromProject() { 3 | return Promise.resolve(false); 4 | } 5 | } 6 | 7 | class ExtensionManager { 8 | constructor() { 9 | this.securityManager = new SecurityManager(); 10 | } 11 | 12 | isExtensionLoaded() { 13 | return false; 14 | } 15 | 16 | isBuiltinExtension() { 17 | return true; 18 | } 19 | 20 | loadExtensionIdSync() {} 21 | 22 | async loadExtensionURL() {} 23 | 24 | allAsyncExtensionsLoaded() {} 25 | 26 | refreshBlocks() {} 27 | 28 | getExtensionURLs() { 29 | return {}; 30 | } 31 | 32 | isExtensionURLLoaded() { 33 | return false; 34 | } 35 | } 36 | 37 | module.exports = ExtensionManager; 38 | -------------------------------------------------------------------------------- /src/build/scratch-vm/import/load-costume.js: -------------------------------------------------------------------------------- 1 | const loadCostume = function (md5ext, costume) { 2 | return Promise.resolve(costume); 3 | }; 4 | 5 | const loadCostumeFromAsset = function (costume) { 6 | return Promise.resolve(costume); 7 | }; 8 | 9 | module.exports = { 10 | loadCostume, 11 | loadCostumeFromAsset 12 | }; 13 | -------------------------------------------------------------------------------- /src/build/scratch-vm/import/load-sound.js: -------------------------------------------------------------------------------- 1 | const loadSoundFromAsset = function (sound) { 2 | return Promise.resolve(sound); 3 | }; 4 | 5 | const loadSound = function (sound) { 6 | return Promise.resolve(sound); 7 | }; 8 | 9 | module.exports = { 10 | loadSound, 11 | loadSoundFromAsset 12 | }; 13 | -------------------------------------------------------------------------------- /src/build/scratch-vm/io/clock.js: -------------------------------------------------------------------------------- 1 | class Clock { 2 | constructor() {} 3 | 4 | projectTimer() { 5 | return 0; 6 | } 7 | 8 | resetProjectTimer() {} 9 | } 10 | 11 | module.exports = Clock; 12 | -------------------------------------------------------------------------------- /src/build/scratch-vm/io/cloud.js: -------------------------------------------------------------------------------- 1 | class Cloud { 2 | constructor() {} 3 | 4 | setProvider() {} 5 | 6 | setStage() {} 7 | 8 | postData() {} 9 | 10 | requestCreateVariable() {} 11 | 12 | requestUpdateVariable() {} 13 | 14 | requestRenameVariable() {} 15 | 16 | requestDeleteVariable() {} 17 | 18 | clear() {} 19 | } 20 | 21 | module.exports = Cloud; 22 | -------------------------------------------------------------------------------- /src/build/scratch-vm/io/keyboard.js: -------------------------------------------------------------------------------- 1 | class Keyboard { 2 | constructor() {} 3 | 4 | postData() {} 5 | 6 | getKeyIsDown() { 7 | return false; 8 | } 9 | 10 | getLastKeyPressed() { 11 | return ''; 12 | } 13 | } 14 | 15 | module.exports = Keyboard; 16 | -------------------------------------------------------------------------------- /src/build/scratch-vm/io/mouse.js: -------------------------------------------------------------------------------- 1 | class Mouse { 2 | constructor() {} 3 | 4 | postData() {} 5 | 6 | getClientX() { 7 | return 0; 8 | } 9 | 10 | getClientY() { 11 | return 0; 12 | } 13 | 14 | getScratchX() { 15 | return 0; 16 | } 17 | 18 | getScratchY() { 19 | return 0; 20 | } 21 | 22 | getIsDown() { 23 | return false; 24 | } 25 | 26 | getButtonIsDown() { 27 | return false; 28 | } 29 | } 30 | 31 | module.exports = Mouse; 32 | -------------------------------------------------------------------------------- /src/build/scratch-vm/io/mouseWheel.js: -------------------------------------------------------------------------------- 1 | class MouseWheel { 2 | constructor() {} 3 | 4 | postData() {} 5 | } 6 | 7 | module.exports = MouseWheel; 8 | -------------------------------------------------------------------------------- /src/build/scratch-vm/io/userData.js: -------------------------------------------------------------------------------- 1 | class UserData { 2 | constructor() {} 3 | 4 | postData() {} 5 | 6 | getUsername() { 7 | return ''; 8 | } 9 | } 10 | 11 | module.exports = UserData; 12 | -------------------------------------------------------------------------------- /src/build/scratch-vm/io/video.js: -------------------------------------------------------------------------------- 1 | class Video { 2 | constructor() {} 3 | 4 | setProvider() {} 5 | 6 | postData() {} 7 | } 8 | 9 | module.exports = Video; 10 | -------------------------------------------------------------------------------- /src/build/scratch-vm/serialization/deserialize-assets.js: -------------------------------------------------------------------------------- 1 | const deserializeSound = function () { 2 | return Promise.resolve(null); 3 | }; 4 | 5 | const deserializeCostume = function () { 6 | return Promise.resolve(null); 7 | }; 8 | 9 | module.exports = { 10 | deserializeSound, 11 | deserializeCostume 12 | }; 13 | -------------------------------------------------------------------------------- /src/build/scratch-vm/serialization/serialize-assets.js: -------------------------------------------------------------------------------- 1 | const serializeSounds = function () { 2 | return []; 3 | }; 4 | 5 | const serializeCostumes = function () { 6 | return []; 7 | }; 8 | 9 | module.exports = { 10 | serializeSounds, 11 | serializeCostumes 12 | }; 13 | -------------------------------------------------------------------------------- /src/build/scratch-vm/serialization/tw-costume-import-export.js: -------------------------------------------------------------------------------- 1 | const parseVectorMetadata = () => { 2 | return null; 3 | }; 4 | 5 | const exportCostume = (costume) => { 6 | return costume.asset.data; 7 | }; 8 | 9 | module.exports = { 10 | parseVectorMetadata, 11 | exportCostume 12 | }; 13 | -------------------------------------------------------------------------------- /src/build/scratch-vm/util/log.js: -------------------------------------------------------------------------------- 1 | const noop = () => {}; 2 | 3 | module.exports = { 4 | debug: noop, 5 | info: noop, 6 | log: noop, 7 | warn: noop, 8 | error: noop 9 | }; 10 | -------------------------------------------------------------------------------- /src/build/text-encoding/index.js: -------------------------------------------------------------------------------- 1 | require('fastestsmallesttextencoderdecoder'); 2 | 3 | module.exports = { 4 | TextEncoder, 5 | TextDecoder 6 | }; 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Suppress all warnings 2 | process.removeAllListeners('warning'); 3 | 4 | const fs = require('fs'); 5 | 6 | function writeStdoutSync(text) { 7 | fs.writeSync(process.stdout.fd, text); 8 | } 9 | 10 | function writeStderrSync(text) { 11 | fs.writeSync(process.stderr.fd, text); 12 | } 13 | 14 | const argv = require('minimist')(process.argv.slice(2), { 15 | boolean: ['help', 'version', 'check', 'buffer-stdout', 'print-generated-js'] 16 | }); 17 | 18 | if (argv.version) { 19 | const { version } = require('../package.json'); 20 | writeStdoutSync(version + '\n'); 21 | process.exit(0); 22 | } 23 | 24 | if (argv._.length == 0 || argv.help) { 25 | writeStdoutSync(`Usage: scratch-run project-file [OPTIONS] 26 | Options: 27 | --help print this message 28 | --version print the version 29 | --check validate project file 30 | --buffer-stdout buffer stdout for better performance 31 | --print-generated-js print the generated JavaScript code 32 | `); 33 | process.exit(0); 34 | } 35 | 36 | const scratchVM = require('scratch-vm'); 37 | const Kattio = require('./kattio'); 38 | 39 | function construct_vm() { 40 | const vm = new scratchVM(); 41 | vm.convertToPackagedRuntime(); 42 | vm.setTurboMode(true); 43 | vm.setFramerate(250); 44 | 45 | // Block loading extensions (e.g., music) 46 | vm.extensionManager.loadExtensionIdSync = 47 | vm.extensionManager.loadExtensionURL = (id) => { 48 | writeStderrSync( 49 | 'Not a valid Scratch file: Can not use extension ' + id + '\n' 50 | ); 51 | process.exit(1); 52 | }; 53 | 54 | return vm; 55 | } 56 | 57 | function check_scratch_file(filename) { 58 | const vm = construct_vm(); 59 | fs.readFile(filename, function (err, data) { 60 | if (err) { 61 | writeStderrSync(err + '\n'); 62 | process.exit(1); 63 | } 64 | 65 | vm.loadProject(data) 66 | .then(() => { 67 | process.exit(0); 68 | }) 69 | .catch(function (err) { 70 | writeStderrSync('Not a valid Scratch file: ' + err + '\n'); 71 | process.exit(1); 72 | }); 73 | }); 74 | } 75 | 76 | function print_generated_js(filename) { 77 | const vm = construct_vm(); 78 | 79 | const generatedJS = []; 80 | require('scratch-vm/src/compiler/jsgen').testingApparatus = { 81 | report: (jsgen, factorySource) => { 82 | const targetName = jsgen.target.getName(); 83 | const scriptName = jsgen.script.procedureCode || 'script'; 84 | generatedJS.push(`// ${targetName} ${scriptName}\n${factorySource}`); 85 | } 86 | }; 87 | 88 | const errors = []; 89 | vm.on('COMPILE_ERROR', (target, error) => { 90 | errors.push({ target, error }); 91 | }); 92 | 93 | fs.readFile(filename, function (err, data) { 94 | if (err) { 95 | writeStderrSync(err + '\n'); 96 | process.exit(1); 97 | } 98 | 99 | vm.loadProject(data) 100 | .then(() => { 101 | vm.runtime.precompile(); 102 | 103 | let result = ''; 104 | if (errors.length) { 105 | result += '// Errors:\n'; 106 | result += errors.map((i) => `// ${i.target.getName()}: ${i.error}\n`); 107 | result += '\n'; 108 | } 109 | result += generatedJS.join('\n\n'); 110 | result += '\n'; 111 | writeStdoutSync(result); 112 | }) 113 | .catch(function (err) { 114 | writeStderrSync('scratch-vm encountered an error: ' + err + '\n'); 115 | process.exit(1); 116 | }); 117 | }); 118 | } 119 | 120 | function run_scratch_file(filename) { 121 | const vm = construct_vm(); 122 | 123 | // _scratch_run_* are called from the generated code by scratch-vm's compiler 124 | let stdoutBuffer = ''; 125 | if (argv['buffer-stdout']) { 126 | vm.runtime._scratch_run_say = function (text) { 127 | stdoutBuffer += text + '\n'; 128 | }; 129 | vm.runtime._scratch_run_think = function (text) { 130 | stdoutBuffer += text; 131 | }; 132 | } else { 133 | vm.runtime._scratch_run_say = function (text) { 134 | process.stdout.write(text + '\n'); 135 | }; 136 | vm.runtime._scratch_run_think = function (text) { 137 | process.stdout.write(text); 138 | }; 139 | } 140 | vm.runtime._scratch_run_ask = function (question) { 141 | try { 142 | if (question === 'read_token') { 143 | return Kattio.nextToken(); 144 | } else { 145 | return Kattio.nextLine(); 146 | } 147 | } catch (e) { 148 | writeStderrSync( 149 | 'scratch-vm encountered an error: Could not read input: ' + 150 | e.message + 151 | '\n' 152 | ); 153 | process.exit(1); 154 | } 155 | }; 156 | 157 | vm.runtime.on('PROJECT_RUN_STOP', function () { 158 | vm.runtime.quit(); 159 | process.stdout.write(stdoutBuffer, () => process.exit(0)); 160 | }); 161 | 162 | fs.readFile(filename, function (err, data) { 163 | if (err) { 164 | writeStderrSync(err + '\n'); 165 | process.exit(1); 166 | } 167 | 168 | vm.loadProject(data) 169 | .then(() => { 170 | for (const target of vm.runtime.targets) { 171 | target.setVisible(false); 172 | } 173 | vm.runtime.precompile(); 174 | vm.start(); 175 | vm.greenFlag(); 176 | }) 177 | .catch(function (err) { 178 | writeStderrSync('scratch-vm encountered an error: ' + err + '\n'); 179 | process.exit(1); 180 | }); 181 | }); 182 | } 183 | 184 | const filename = argv._[0]; 185 | if (argv.check) { 186 | check_scratch_file(filename); 187 | } else if (argv['print-generated-js']) { 188 | print_generated_js(filename); 189 | } else { 190 | run_scratch_file(filename); 191 | } 192 | -------------------------------------------------------------------------------- /src/kattio.js: -------------------------------------------------------------------------------- 1 | // https://gist.github.com/simonlindholm/38e1a3ff5d99fdbceda541f112f9daf3 2 | 3 | const fs = require('fs'); 4 | 5 | const Kattio = { 6 | _buf: Buffer.alloc(1 << 20), 7 | _bufPos: 0, 8 | _bufLen: 0, 9 | _ensure: function () { 10 | if (this._bufPos === this._bufLen) { 11 | this._bufPos = 0; 12 | try { 13 | this._bufLen = fs.readSync(0, this._buf, 0, this._buf.length, null); 14 | } catch (error) { 15 | // Ref: https://github.com/nodejs/node/issues/35997 16 | if (error.code === 'EOF') { 17 | this._bufLen = 0; 18 | return; 19 | } 20 | 21 | throw error; 22 | } 23 | } 24 | }, 25 | 26 | _isws: function (ch) { 27 | return ch === 32 || (9 <= ch && ch <= 13); 28 | }, 29 | 30 | _islf: function (ch) { 31 | return ch === 10 || ch === 13; 32 | }, 33 | 34 | _peekChar: function () { 35 | this._ensure(); 36 | return this._bufPos === this._bufLen ? 0 : this._buf[this._bufPos]; 37 | }, 38 | 39 | _skipWs: function () { 40 | while (this._isws(this._peekChar())) this._bufPos++; 41 | }, 42 | 43 | _readUntil: function (stop) { 44 | this._ensure(); 45 | if (this._bufPos === this._bufLen) { 46 | throw new Error('End of file reached'); 47 | } 48 | 49 | var start = this._bufPos; 50 | var before = null; 51 | for (;;) { 52 | if (this._bufPos === this._bufLen) { 53 | // Hit the end; need to switch buffers. Thus, stash away all we have so far 54 | // into the 'before' buffer. 55 | var len = this._bufPos - start, 56 | preLen = before ? before.length : 0; 57 | var nbuf = Buffer.alloc(len + preLen); 58 | if (before) before.copy(nbuf); 59 | before = nbuf; 60 | this._buf.copy(before, preLen, start); 61 | this._ensure(); 62 | start = this._bufPos; 63 | } 64 | if (this._bufPos === this._bufLen || stop(this._buf[this._bufPos])) break; 65 | this._bufPos++; 66 | } 67 | if (!before) { 68 | return this._buf.toString('utf8', start, this._bufPos); 69 | } 70 | var after = this._buf.subarray(start, this._bufPos); 71 | var res = Buffer.alloc(before.length + after.length); 72 | before.copy(res); 73 | after.copy(res, before.length); 74 | return res.toString('utf8'); 75 | }, 76 | 77 | nextToken: function () { 78 | this._skipWs(); 79 | return this._readUntil(this._isws); 80 | }, 81 | 82 | nextLine: function () { 83 | var line = this._readUntil(this._islf); 84 | if (this._peekChar() === 13) this._bufPos++; 85 | if (this._peekChar() === 10) this._bufPos++; 86 | return line; 87 | } 88 | }; 89 | 90 | module.exports = Kattio; 91 | -------------------------------------------------------------------------------- /tests/aplusb.sb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VNOI-Admin/scratch-run/af913cb1c5021b5aad72204938a74fc80ccf10c7/tests/aplusb.sb -------------------------------------------------------------------------------- /tests/aplusb.sb2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VNOI-Admin/scratch-run/af913cb1c5021b5aad72204938a74fc80ccf10c7/tests/aplusb.sb2 -------------------------------------------------------------------------------- /tests/aplusb.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VNOI-Admin/scratch-run/af913cb1c5021b5aad72204938a74fc80ccf10c7/tests/aplusb.sb3 -------------------------------------------------------------------------------- /tests/echo.json: -------------------------------------------------------------------------------- 1 | {"targets":[{"isStage":true,"name":"Stage","variables":{"`jEk@4|i[#Fk?(8x)AV.-my variable":["text","thuk"]},"lists":{},"broadcasts":{},"blocks":{},"comments":{},"currentCostume":0,"costumes":[{"assetId":"cd21514d0531fdffb22204e0ec5ed84a","name":"backdrop1","md5ext":"cd21514d0531fdffb22204e0ec5ed84a.svg","dataFormat":"svg","rotationCenterX":240,"rotationCenterY":180}],"sounds":[{"assetId":"83a9787d4cb6f3b7632b4ddfebf74367","name":"pop","dataFormat":"wav","format":"","rate":48000,"sampleCount":1123,"md5ext":"83a9787d4cb6f3b7632b4ddfebf74367.wav"}],"volume":100,"layerOrder":0,"tempo":60,"videoTransparency":50,"videoState":"on","textToSpeechLanguage":null},{"isStage":false,"name":"Sprite1","variables":{},"lists":{},"broadcasts":{},"blocks":{"DM)/T}QPiS?mo~^bij2;":{"opcode":"event_whenflagclicked","next":"A24MyL#=n+b=;r:[za~%","parent":null,"inputs":{},"fields":{},"shadow":false,"topLevel":true,"x":444,"y":39},"A24MyL#=n+b=;r:[za~%":{"opcode":"sensing_askandwait","next":")bJ6((r8M*ExyCq=1rCJ","parent":"DM)/T}QPiS?mo~^bij2;","inputs":{"QUESTION":[1,[10,"What's your name?"]]},"fields":{},"shadow":false,"topLevel":false},"Z5GRmk8DH|*0x(y/u1!m":{"opcode":"sensing_answer","next":null,"parent":")bJ6((r8M*ExyCq=1rCJ","inputs":{},"fields":{},"shadow":false,"topLevel":false},")bJ6((r8M*ExyCq=1rCJ":{"opcode":"looks_say","next":null,"parent":"A24MyL#=n+b=;r:[za~%","inputs":{"MESSAGE":[3,"Z5GRmk8DH|*0x(y/u1!m",[10,"Hello!"]]},"fields":{},"shadow":false,"topLevel":false}},"comments":{},"currentCostume":0,"costumes":[{"assetId":"bcf454acf82e4504149f7ffe07081dbc","name":"costume1","bitmapResolution":1,"md5ext":"bcf454acf82e4504149f7ffe07081dbc.svg","dataFormat":"svg","rotationCenterX":48,"rotationCenterY":50},{"assetId":"0fb9be3e8397c983338cb71dc84d0b25","name":"costume2","bitmapResolution":1,"md5ext":"0fb9be3e8397c983338cb71dc84d0b25.svg","dataFormat":"svg","rotationCenterX":46,"rotationCenterY":53}],"sounds":[{"assetId":"83c36d806dc92327b9e7049a565c6bff","name":"Meo","dataFormat":"wav","format":"","rate":48000,"sampleCount":40681,"md5ext":"83c36d806dc92327b9e7049a565c6bff.wav"}],"volume":100,"layerOrder":1,"visible":true,"x":0,"y":0,"size":100,"direction":90,"draggable":false,"rotationStyle":"all around"}],"monitors":[{"id":"answer","mode":"default","opcode":"sensing_answer","params":{},"spriteName":null,"value":"thuk","width":0,"height":0,"x":5,"y":5,"visible":true,"sliderMin":0,"sliderMax":100,"isDiscrete":true}],"extensions":[],"meta":{"semver":"3.0.0","vm":"0.2.0-prerelease.20210811102104","agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36"}} -------------------------------------------------------------------------------- /tests/echo.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VNOI-Admin/scratch-run/af913cb1c5021b5aad72204938a74fc80ccf10c7/tests/echo.sb3 -------------------------------------------------------------------------------- /tests/invalid.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VNOI-Admin/scratch-run/af913cb1c5021b5aad72204938a74fc80ccf10c7/tests/invalid.sb3 -------------------------------------------------------------------------------- /tests/music_extension.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VNOI-Admin/scratch-run/af913cb1c5021b5aad72204938a74fc80ccf10c7/tests/music_extension.sb3 -------------------------------------------------------------------------------- /tests/permutation.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VNOI-Admin/scratch-run/af913cb1c5021b5aad72204938a74fc80ccf10c7/tests/permutation.sb3 -------------------------------------------------------------------------------- /tests/say_think.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VNOI-Admin/scratch-run/af913cb1c5021b5aad72204938a74fc80ccf10c7/tests/say_think.sb3 -------------------------------------------------------------------------------- /tests/stopall.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VNOI-Admin/scratch-run/af913cb1c5021b5aad72204938a74fc80ccf10c7/tests/stopall.sb3 -------------------------------------------------------------------------------- /tests/sum_1ton.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VNOI-Admin/scratch-run/af913cb1c5021b5aad72204938a74fc80ccf10c7/tests/sum_1ton.sb3 -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import unittest 4 | from subprocess import PIPE, Popen, TimeoutExpired 5 | 6 | __DIR__ = os.path.dirname(os.path.realpath(__file__)) 7 | os.chdir(__DIR__) 8 | 9 | 10 | def get_executable(): 11 | arch = 'arm64' if platform.machine() == 'aarch64' else 'amd64' 12 | if platform.system() == 'Linux': 13 | return f'../bin/linux-{arch}/scratch-run' 14 | elif platform.system() == 'Darwin': 15 | return f'../bin/macos-{arch}/scratch-run' 16 | elif platform.system() == 'Windows': 17 | return f'../bin/win-{arch}/scratch-run' 18 | else: 19 | raise RuntimeError('Unsupported platform: {}'.format(platform.system())) 20 | 21 | 22 | class TestScratchRun(unittest.TestCase): 23 | executable = get_executable() 24 | 25 | def test_say_think(self): 26 | proc = Popen([self.executable, 'say_think.sb3'], stdin=PIPE, stdout=PIPE, stderr=PIPE) 27 | stdout, stderr = proc.communicate() 28 | 29 | self.assertEqual(proc.returncode, 0) 30 | self.assertEqual(stdout, b'Hello world!\n') 31 | self.assertEqual(stderr, b'') 32 | 33 | def test_echo(self): 34 | test_message = b'echo: Hello, World!\n' 35 | 36 | proc = Popen([self.executable, 'echo.sb3'], stdin=PIPE, stdout=PIPE, stderr=PIPE) 37 | stdout, stderr = proc.communicate(test_message) 38 | 39 | self.assertEqual(proc.returncode, 0) 40 | self.assertEqual(stdout, test_message) 41 | self.assertEqual(stderr, b'') 42 | 43 | def test_echo_json(self): 44 | test_message = b'echo: Hello, World!\n' 45 | 46 | proc = Popen([self.executable, 'echo.json'], stdin=PIPE, stdout=PIPE, stderr=PIPE) 47 | stdout, stderr = proc.communicate(test_message) 48 | 49 | self.assertEqual(proc.returncode, 0) 50 | self.assertEqual(stdout, test_message) 51 | self.assertEqual(stderr, b'') 52 | 53 | def test_aplusb_sb_token(self): 54 | proc = Popen([self.executable, 'aplusb.sb'], stdin=PIPE, stdout=PIPE, stderr=PIPE) 55 | stdout, stderr = proc.communicate(b'123 456\n') 56 | 57 | self.assertEqual(proc.returncode, 0) 58 | self.assertEqual(stdout, b'579\n') 59 | self.assertEqual(stderr, b'') 60 | 61 | def test_aplusb_sb_line(self): 62 | proc = Popen([self.executable, 'aplusb.sb'], stdin=PIPE, stdout=PIPE, stderr=PIPE) 63 | stdout, stderr = proc.communicate(b'123\n456\n') 64 | 65 | self.assertEqual(proc.returncode, 0) 66 | self.assertEqual(stdout, b'579\n') 67 | self.assertEqual(stderr, b'') 68 | 69 | def test_aplusb_sb2_token(self): 70 | proc = Popen([self.executable, 'aplusb.sb2'], stdin=PIPE, stdout=PIPE, stderr=PIPE) 71 | stdout, stderr = proc.communicate(b'123 456\n') 72 | 73 | self.assertEqual(proc.returncode, 0) 74 | self.assertEqual(stdout, b'579\n') 75 | self.assertEqual(stderr, b'') 76 | 77 | def test_aplusb_sb2_line(self): 78 | proc = Popen([self.executable, 'aplusb.sb2'], stdin=PIPE, stdout=PIPE, stderr=PIPE) 79 | stdout, stderr = proc.communicate(b'123\n456\n') 80 | 81 | self.assertEqual(proc.returncode, 0) 82 | self.assertEqual(stdout, b'579\n') 83 | self.assertEqual(stderr, b'') 84 | 85 | def test_aplusb_sb3_token(self): 86 | proc = Popen([self.executable, 'aplusb.sb3'], stdin=PIPE, stdout=PIPE, stderr=PIPE) 87 | stdout, stderr = proc.communicate(b'123 456\n') 88 | 89 | self.assertEqual(proc.returncode, 0) 90 | self.assertEqual(stdout, b'579\n') 91 | self.assertEqual(stderr, b'') 92 | 93 | def test_aplusb_sb3_line(self): 94 | proc = Popen([self.executable, 'aplusb.sb3'], stdin=PIPE, stdout=PIPE, stderr=PIPE) 95 | stdout, stderr = proc.communicate(b'123\n456\n') 96 | 97 | self.assertEqual(proc.returncode, 0) 98 | self.assertEqual(stdout, b'579\n') 99 | self.assertEqual(stderr, b'') 100 | 101 | def test_permutation(self): 102 | proc = Popen([self.executable, 'permutation.sb3'], stdin=PIPE, stdout=PIPE, stderr=PIPE) 103 | stdout, stderr = proc.communicate(b'3\n') 104 | 105 | self.assertEqual(proc.returncode, 0) 106 | self.assertEqual(stdout, b'123\n132\n213\n231\n312\n321\n') 107 | self.assertEqual(stderr, b'') 108 | 109 | def test_stopall(self): 110 | proc = Popen([self.executable, 'stopall.sb3'], stdin=PIPE, stdout=PIPE, stderr=PIPE) 111 | try: 112 | stdout, stderr = proc.communicate(timeout=1) 113 | except TimeoutExpired: 114 | proc.kill() 115 | raise 116 | 117 | self.assertEqual(proc.returncode, 0) 118 | self.assertEqual(stdout, b'') 119 | self.assertEqual(stderr, b'') 120 | 121 | def test_sum_1ton(self): 122 | N = 100000 123 | inp = f'{N}\n' 124 | for i in range(N): 125 | inp += str(i + 1) + '\n' 126 | 127 | proc = Popen([self.executable, 'sum_1ton.sb3'], stdin=PIPE, stdout=PIPE, stderr=PIPE) 128 | try: 129 | stdout, stderr = proc.communicate(inp.encode(), timeout=2) 130 | except TimeoutExpired: 131 | proc.kill() 132 | raise 133 | 134 | self.assertEqual(proc.returncode, 0) 135 | self.assertEqual(stdout, (str(N * (N + 1) // 2) + '\n').encode()) 136 | self.assertEqual(stderr, b'') 137 | 138 | def test_invalid_file(self): 139 | proc = Popen([self.executable, 'invalid.sb3'], stdin=PIPE, stdout=PIPE, stderr=PIPE) 140 | stdout, stderr = proc.communicate() 141 | 142 | self.assertEqual(proc.returncode, 1) 143 | self.assertEqual(stdout, b'') 144 | self.assertEqual(stderr, b'scratch-vm encountered an error: SyntaxError: Unexpected end of JSON input\n') 145 | 146 | def test_check_invalid_file(self): 147 | proc = Popen([self.executable, '--check', 'invalid.sb3'], stdin=PIPE, stdout=PIPE, stderr=PIPE) 148 | stdout, stderr = proc.communicate() 149 | 150 | self.assertEqual(proc.returncode, 1) 151 | self.assertEqual(stdout, b'') 152 | self.assertEqual(stderr, b'Not a valid Scratch file: SyntaxError: Unexpected end of JSON input\n') 153 | 154 | def test_music_extension(self): 155 | proc = Popen([self.executable, 'music_extension.sb3'], stdin=PIPE, stdout=PIPE, stderr=PIPE) 156 | stdout, stderr = proc.communicate() 157 | 158 | self.assertEqual(proc.returncode, 1) 159 | self.assertEqual(stdout, b'') 160 | self.assertEqual(stderr, b'Not a valid Scratch file: Can not use extension music\n') 161 | 162 | def test_check_music_extension(self): 163 | proc = Popen([self.executable, '--check', 'music_extension.sb3'], stdin=PIPE, stdout=PIPE, stderr=PIPE) 164 | stdout, stderr = proc.communicate() 165 | 166 | self.assertEqual(proc.returncode, 1) 167 | self.assertEqual(stdout, b'') 168 | self.assertEqual(stderr, b'Not a valid Scratch file: Can not use extension music\n') 169 | 170 | 171 | if __name__ == '__main__': 172 | unittest.main() 173 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const BundleAnalyzerPlugin = 3 | require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 4 | const path = require('path'); 5 | 6 | const noop_module_path = path.resolve(__dirname, 'src', 'build', 'noop-module'); 7 | 8 | module.exports = { 9 | mode: 'production', 10 | devtool: '', 11 | target: 'node', 12 | output: { 13 | filename: 'index.js', 14 | path: path.resolve(__dirname, 'dist') 15 | }, 16 | entry: './src/index.js', 17 | resolve: { 18 | alias: { 19 | // Replace with fastestsmallesttextencoderdecoder 20 | 'text-encoding$': path.resolve( 21 | __dirname, 22 | 'src', 23 | 'build', 24 | 'text-encoding' 25 | ), 26 | 27 | // Force webpack to rebuild scratch-sb1-converter 28 | 'scratch-sb1-converter$': path.resolve( 29 | __dirname, 30 | 'node_modules', 31 | 'scratch-sb1-converter', 32 | 'index.js' 33 | ), 34 | 35 | // Remove dead modules 36 | htmlparser2$: noop_module_path, 37 | 'canvas-toBlob$': noop_module_path, 38 | './extension-support/tw-default-extension-urls$': noop_module_path, 39 | '../util/scratch-link-websocket$': noop_module_path 40 | } 41 | }, 42 | plugins: [ 43 | // Remove extensions 44 | new webpack.NormalModuleReplacementPlugin( 45 | /\/extension-manager$/, 46 | '/src/build/scratch-vm/extension-support/extension-manager' 47 | ), 48 | 49 | // Remove log 50 | new webpack.NormalModuleReplacementPlugin( 51 | /\/log$/, 52 | '/src/build/scratch-vm/util/log' 53 | ), 54 | 55 | // Remove I/O modules 56 | new webpack.NormalModuleReplacementPlugin(/^\.\.\/io\//, (resource) => { 57 | resource.request = resource.request.replace( 58 | '..', 59 | '/src/build/scratch-vm' 60 | ); 61 | }), 62 | 63 | // Remove load-costume and load-sound 64 | new webpack.NormalModuleReplacementPlugin( 65 | /\.\/import\/load-(costume|sound)/, 66 | (resource) => { 67 | resource.request = 68 | '/src/build/scratch-vm/import/load-' + 69 | resource.request.match(/\.\/import\/load-(costume|sound)/)[1]; 70 | } 71 | ), 72 | 73 | // Remove deserialize-assets, serialize-assets, and tw-costume-import-export 74 | new webpack.NormalModuleReplacementPlugin( 75 | /\.\/(?:serialization\/)?(deserialize-assets|serialize-assets|tw-costume-import-export)/, 76 | (resource) => { 77 | resource.request = 78 | '/src/build/scratch-vm/serialization/' + 79 | resource.request.match( 80 | /(deserialize-assets|serialize-assets|tw-costume-import-export)/ 81 | )[1]; 82 | } 83 | ), 84 | 85 | // Remove FontManager 86 | new webpack.NormalModuleReplacementPlugin( 87 | /\.\/tw-font-manager$/, 88 | '/src/build/scratch-vm/engine/tw-font-manager' 89 | ), 90 | 91 | new BundleAnalyzerPlugin({ 92 | analyzerMode: 'static', 93 | openAnalyzer: false 94 | }) 95 | ], 96 | module: { 97 | rules: [ 98 | { 99 | test: /\.[jt]sx?$/, 100 | loader: 'esbuild-loader' 101 | } 102 | ] 103 | } 104 | }; 105 | --------------------------------------------------------------------------------