diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c26843c8e..3859cfdc3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -47,8 +47,6 @@ "dbaeumer.vscode-eslint", "kumar-harsh.graphql-for-vscode", "hashicorp.terraform", - "ivangabriele.vscode-heroku", - "pkosta2005.heroku-command", "yzhang.markdown-all-in-one", "mikestead.dotenv", "ms-vscode.remote-repositories", diff --git a/.env.example b/.env.example index cde715804..1606c26d5 100644 --- a/.env.example +++ b/.env.example @@ -4,8 +4,8 @@ REACT_APP_SENTRY_ENV='local' PUBLIC_URL='http://localhost:3011' ASSETS_URL='http://localhost:3011' HTML_RENDERER_URL='http://localhost:3011' +REACT_APP_SCRATCH_FRAME_URL='http://localhost:3014' REACT_APP_GOOGLE_TAG_MANAGER_ID='' REACT_APP_API_ENDPOINT='http://localhost:3009' REACT_APP_ALLOWED_IFRAME_ORIGINS='http://localhost:3011,http://localhost:3012,http://classroom.localhost:3013' - diff --git a/.eslintrc.json b/.eslintrc.json index 378bb0beb..4faba3fc2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -51,6 +51,14 @@ "cy": "readonly", "Cypress": "readonly" } + }, + { + "files": ["apps/scratch-frame/**/*.test.js", "apps/scratch-frame/**/*.test.jsx"], + "globals": { + "vi": "readonly", + "test": "readonly", + "describe": "readonly" + } } ] } diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index ed480c045..e155f2b9d 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -80,10 +80,13 @@ jobs: env: JEST_JUNIT_OUTPUT_DIR: ./coverage/ REACT_APP_API_ENDPOINT: http://localhost:3009 + REACT_APP_SCRATCH_FRAME_URL: "http://scratch-frame.example.com" - name: Record coverage run: ./.github/workflows/record_coverage env: GITHUB_TOKEN: ${{ github.token }} + - name: Run Scratch Frame tests + run: yarn run test:scratch-frame test-cypress: runs-on: ubuntu-latest @@ -111,8 +114,8 @@ jobs: with: install: false start: | - yarn start - wait-on: "http://localhost:3011" + yarn start:all + wait-on: "http://localhost:3011, http://localhost:3014/scratch.html" quiet: true config-file: cypress.config.mjs browser: chrome @@ -121,6 +124,8 @@ jobs: PUBLIC_URL: "http://localhost:3011" ASSETS_URL: "http://localhost:3011" REACT_APP_ALLOWED_IFRAME_ORIGINS: "http://localhost:3011" + CSP_API_MULTIPLE_ORIGINS: "http://localhost:3011,http://localhost:3009" + REACT_APP_SCRATCH_FRAME_URL: "http://localhost:3014" - name: Archive cypress artifacts uses: actions/upload-artifact@v4.6.0 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4fba2112e..d949efca1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -138,10 +138,11 @@ jobs: run: yarn install --immutable - name: Build WC and HTML renderer bundles - run: yarn build + run: yarn build:all env: PUBLIC_URL: ${{ needs.setup-environment.outputs.public_url }} ASSETS_URL: ${{ needs.setup-environment.outputs.assets_url }} + REACT_APP_SCRATCH_FRAME_URL: ${{ needs.setup-environment.outputs.assets_url }} HTML_RENDERER_URL: ${{ needs.setup-environment.outputs.html_renderer_url }} REACT_APP_API_ENDPOINT: ${{ inputs.react_app_api_endpoint }} REACT_APP_AUTHENTICATION_CLIENT_ID: ${{ inputs.react_app_authentication_client_id }} diff --git a/COPYRIGHT b/COPYRIGHT index 8637fa025..07bf4a00f 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -16,6 +16,11 @@ License: Apache-2.0 See the License for the specific language governing permissions and limitations under the License. + +Files: apps/scratch-frame +Copyright: 2025 Raspberry Pi Foundation +License: AGPL-3.0 + Files: public/p5-shim.js, public/py5-shim.js Copyright: 2021 Nick McIntyre License: MIT diff --git a/Dockerfile b/Dockerfile index 7d946f18a..c62bcf1ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,5 +14,6 @@ RUN corepack enable RUN chsh -s $(which zsh) ${USER} EXPOSE 3011 +EXPOSE 3014 -CMD ["yarn", "start"] +CMD ["yarn", "start:all"] diff --git a/LICENSE.txt b/LICENSE.txt index d64569567..cadb1f3d8 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,3 +1,6 @@ +This licence applies to files in this project unless otherwise specified. +See COPYRIGHT file for more. + Apache License Version 2.0, January 2004 @@ -187,7 +190,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2021-2026 Raspberry Pi Foundation Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app.json b/app.json deleted file mode 100644 index 27b4ee82b..000000000 --- a/app.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "editor-ui", - "formation": { - "web": { - "quantity": 1 - } - }, - "buildpacks": [ - { - "url": "heroku/nodejs" - }, - { - "url": "https://github.com/heroku/heroku-buildpack-nginx" - } - ] -} diff --git a/apps/scratch-frame/LICENSE b/apps/scratch-frame/LICENSE new file mode 100644 index 000000000..29ebfa545 --- /dev/null +++ b/apps/scratch-frame/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/apps/scratch-frame/README.md b/apps/scratch-frame/README.md new file mode 100644 index 000000000..ed463df65 --- /dev/null +++ b/apps/scratch-frame/README.md @@ -0,0 +1 @@ +# scratch-frame diff --git a/apps/scratch-frame/package.json b/apps/scratch-frame/package.json new file mode 100644 index 000000000..915a14cec --- /dev/null +++ b/apps/scratch-frame/package.json @@ -0,0 +1,27 @@ +{ + "name": "@raspberrypifoundation/scratch-frame", + "private": true, + "dependencies": { + "@RaspberryPiFoundation/scratch-gui": "13.7.3-code-classroom.20260522151700", + "@vitejs/plugin-react": "^6.0.2", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-redux": "^8.1.3", + "redux": "^4.2.1", + "vite": "^8.0.16", + "vite-plugin-static-copy": "^4.1.1" + }, + "scripts": { + "dev": "vite", + "start": "vite", + "build": "vite build", + "preview": "vite preview", + "test": "vitest" + }, + "devDependencies": { + "@testing-library/react": "14.3.1", + "redux-mock-store": "^1.5.4", + "vitest": "^4.1.9" + }, + "packageManager": "yarn@4.12.0" +} diff --git a/src/scratch.html b/apps/scratch-frame/scratch.html similarity index 66% rename from src/scratch.html rename to apps/scratch-frame/scratch.html index 091c4ea3b..f7db1ea06 100644 --- a/src/scratch.html +++ b/apps/scratch-frame/scratch.html @@ -13,10 +13,10 @@ style-src 'self' 'unsafe-inline'; worker-src 'self' blob:; child-src 'self' blob:; - connect-src 'self' <%= cspApiMultipleOrigins || cspApiOrigin %> <%= cspAssetOrigin %> <%= cspScratchLibraryAssetOrigin %> <%= isDev ? "ws: wss:" : "" %>; - img-src 'self' data: blob: <%= cspAssetOrigin %> <%= cspScratchLibraryAssetOrigin %>; - media-src 'self' blob: <%= cspAssetOrigin %> <%= cspScratchLibraryAssetOrigin %>; - font-src 'self' data: <%= cspAssetOrigin %>; + connect-src 'self' %cspApiMultipleOrigins% %cspAssetOrigin% %cspScratchLibraryAssetOrigin% %webSocketOrigin%; + img-src 'self' data: blob: %cspAssetOrigin% %cspScratchLibraryAssetOrigin%; + media-src 'self' blob: %cspAssetOrigin% %cspScratchLibraryAssetOrigin%; + font-src 'self' data: %cspAssetOrigin%; form-action 'self'; upgrade-insecure-requests; " @@ -73,16 +73,6 @@
loading
- - - - - - + diff --git a/src/components/ScratchEditor/ScratchEditor.jsx b/apps/scratch-frame/src/ScratchEditor.jsx similarity index 92% rename from src/components/ScratchEditor/ScratchEditor.jsx rename to apps/scratch-frame/src/ScratchEditor.jsx index 8ce149903..49262e116 100644 --- a/src/components/ScratchEditor/ScratchEditor.jsx +++ b/apps/scratch-frame/src/ScratchEditor.jsx @@ -1,8 +1,9 @@ -import scratchProjectSave from "../../utils/scratchProjectSave.js"; +import scratchProjectSave from "./utils/scratchProjectSave.js"; +import React from "react"; import { useCallback, useRef, useEffect, useState } from "react"; import WrapperdScratchGui from "./WrappedScratchGui.jsx"; -import { postScratchGuiEvent, allowedParentOrigin } from "./events.js"; +import { postScratchGuiEvent, allowedParentOrigin } from "./utils/events.js"; /** Scratch library picker assets (not project save/load — those use editor-api). */ export const SCRATCH_LIBRARY_ASSET_URL_TEMPLATE = @@ -84,7 +85,7 @@ const ScratchEditor = ({ projectHost={`${apiUrl}/api/scratch/projects`} assetHost={`${apiUrl}/api/scratch/assets`} libraryAssetUrlTemplate={SCRATCH_LIBRARY_ASSET_URL_TEMPLATE} - basePath={`${process.env.ASSETS_URL}/scratch-gui/`} + basePath={`${import.meta.env.REACT_APP_SCRATCH_FRAME_URL}/scratch-gui/`} onStorageInit={(storage) => { scratchFetchApiRef.current = storage.scratchFetch; if (accessToken) { diff --git a/src/components/ScratchEditor/ScratchEditor.test.jsx b/apps/scratch-frame/src/ScratchEditor.test.jsx similarity index 86% rename from src/components/ScratchEditor/ScratchEditor.test.jsx rename to apps/scratch-frame/src/ScratchEditor.test.jsx index 132572db2..6ce0e3aa4 100644 --- a/src/components/ScratchEditor/ScratchEditor.test.jsx +++ b/apps/scratch-frame/src/ScratchEditor.test.jsx @@ -1,20 +1,25 @@ +import React from "react"; import { render, cleanup, act } from "@testing-library/react"; import ScratchEditor, { SCRATCH_LIBRARY_ASSET_URL_TEMPLATE, } from "./ScratchEditor.jsx"; -const mockWrappedScratchGui = jest.fn(); -const mockScratchProjectSave = jest.fn(); +const { mockWrappedScratchGui, mockScratchProjectSave } = vi.hoisted(() => ({ + mockWrappedScratchGui: vi.fn(), + mockScratchProjectSave: vi.fn(), +})); -jest.mock("../../utils/scratchProjectSave.js", () => ({ +vi.mock("./utils/scratchProjectSave.js", () => ({ __esModule: true, default: (params) => mockScratchProjectSave(params), })); -jest.mock("./WrappedScratchGui.jsx", () => (props) => { - mockWrappedScratchGui(props); - return
; -}); +vi.mock("./WrappedScratchGui.jsx", () => ({ + default: (props) => { + mockWrappedScratchGui(props); + return
; + }, +})); describe("ScratchEditor", () => { afterEach(() => { @@ -56,7 +61,7 @@ describe("ScratchEditor", () => { const scratchGuiProps = mockWrappedScratchGui.mock.calls[0][0]; const scratchStorage = { scratchFetch: { - setMetadata: jest.fn(), + setMetadata: vi.fn(), }, }; @@ -93,7 +98,7 @@ describe("ScratchEditor", () => { const scratchGuiProps = mockWrappedScratchGui.mock.calls[0][0]; const scratchStorage = { scratchFetch: { - setMetadata: jest.fn(), + setMetadata: vi.fn(), }, }; diff --git a/src/components/ScratchEditor/ScratchIntegrationHOC.jsx b/apps/scratch-frame/src/ScratchIntegrationHOC.jsx similarity index 90% rename from src/components/ScratchEditor/ScratchIntegrationHOC.jsx rename to apps/scratch-frame/src/ScratchIntegrationHOC.jsx index f28a1dca4..e68099e34 100644 --- a/src/components/ScratchEditor/ScratchIntegrationHOC.jsx +++ b/apps/scratch-frame/src/ScratchIntegrationHOC.jsx @@ -2,13 +2,10 @@ import React from "react"; import PropTypes from "prop-types"; import { connect } from "react-redux"; import { saveAs } from "file-saver"; -import { - remixProject, - manualUpdateProject, - setStageSize, -} from "@RaspberryPiFoundation/scratch-gui"; -import { allowedIframeHost } from "../../utils/iframeUtils"; -import { postScratchGuiEvent } from "./events.js"; +import { allowedIframeHost } from "./utils/iframeUtils"; +import { postScratchGuiEvent } from "./utils/events.js"; + +const ScratchGui = window.GUI; const ScratchIntegrationHOC = function (WrappedComponent) { class ScratchIntegrationComponent extends React.Component { @@ -123,9 +120,9 @@ const ScratchIntegrationHOC = function (WrappedComponent) { }); const mapDispatchToProps = (dispatch) => ({ - onClickRemix: () => dispatch(remixProject()), - onClickSave: () => dispatch(manualUpdateProject()), - setStageSize: () => dispatch(setStageSize("small")), + onClickRemix: () => dispatch(ScratchGui.remixProject()), + onClickSave: () => dispatch(ScratchGui.manualUpdateProject()), + setStageSize: () => dispatch(ScratchGui.setStageSize("small")), }); ScratchIntegrationComponent.propTypes = { diff --git a/src/components/ScratchEditor/ScratchIntegrationHOC.test.jsx b/apps/scratch-frame/src/ScratchIntegrationHOC.test.jsx similarity index 76% rename from src/components/ScratchEditor/ScratchIntegrationHOC.test.jsx rename to apps/scratch-frame/src/ScratchIntegrationHOC.test.jsx index a2a292982..885774422 100644 --- a/src/components/ScratchEditor/ScratchIntegrationHOC.test.jsx +++ b/apps/scratch-frame/src/ScratchIntegrationHOC.test.jsx @@ -1,45 +1,43 @@ -const React = require("react"); -const { render, waitFor } = require("@testing-library/react"); -const { Provider } = require("react-redux"); -const configureStore = require("redux-mock-store").default; - -jest.mock("file-saver", () => ({ saveAs: jest.fn() })); -jest.mock("./events.js", () => ({ postScratchGuiEvent: jest.fn() })); -jest.mock("@RaspberryPiFoundation/scratch-gui", () => ({ - remixProject: () => ({ type: "remix" }), - manualUpdateProject: () => ({ type: "manualUpdate" }), - setStageSize: () => ({ type: "setStageSize" }), +import React from "react"; +import { render, waitFor } from "@testing-library/react"; +import { Provider } from "react-redux"; +import configureStore from "redux-mock-store"; +import { saveAs } from "file-saver"; +import { postScratchGuiEvent } from "./utils/events.js"; + +const { mockSaveAs, mockPostScratchGuiEvent } = vi.hoisted(() => ({ + mockSaveAs: vi.fn(), + mockPostScratchGuiEvent: vi.fn(), })); -const ScratchIntegrationHOC = require("./ScratchIntegrationHOC").default; -const { postScratchGuiEvent } = require("./events.js"); +vi.mock("file-saver", () => ({ saveAs: mockSaveAs })); +vi.mock("./utils/events.js", () => ({ + postScratchGuiEvent: mockPostScratchGuiEvent, +})); describe("ScratchIntegrationHOC", () => { - const mockSaveProjectSb3 = jest.fn(); - const mockLoadProject = jest.fn(); + const mockSaveProjectSb3 = vi.fn(); + const mockLoadProject = vi.fn(); const mockVm = { saveProjectSb3: mockSaveProjectSb3, loadProject: mockLoadProject, - on: jest.fn(), - removeListener: jest.fn(), + on: vi.fn(), + removeListener: vi.fn(), }; - const allowedOrigin = "https://editor.example.com"; + const allowedOrigin = + import.meta.env.REACT_APP_ALLOWED_IFRAME_ORIGINS?.split(",")[0] || + "http://localhost:3011"; let store; let Wrapped; - let saveAs; - - beforeEach(() => { - const fileSaver = require("file-saver"); - saveAs = fileSaver.saveAs || fileSaver; - if (typeof saveAs.mockClear === "function") { - saveAs.mockClear(); - } + + beforeEach(async () => { + vi.resetModules(); + saveAs.mockClear(); mockSaveProjectSb3.mockClear(); mockLoadProject.mockClear(); mockVm.on.mockClear(); mockVm.removeListener.mockClear(); postScratchGuiEvent.mockClear(); - process.env.REACT_APP_ALLOWED_IFRAME_ORIGINS = allowedOrigin; const mockStore = configureStore([]); store = mockStore({ scratchGui: { @@ -48,11 +46,19 @@ describe("ScratchIntegrationHOC", () => { }); const Dummy = () => React.createElement("div", { "data-testid": "wrapped" }); + window.GUI = { + remixProject: () => ({ type: "remix" }), + manualUpdateProject: () => ({ type: "manualUpdate" }), + setStageSize: () => ({ type: "setStageSize" }), + }; + + const { default: ScratchIntegrationHOC } = + await import("./ScratchIntegrationHOC.jsx"); Wrapped = ScratchIntegrationHOC(Dummy); }); afterEach(() => { - delete process.env.REACT_APP_ALLOWED_IFRAME_ORIGINS; + delete window.GUI; }); const getVmHandler = (eventName) => @@ -92,7 +98,7 @@ describe("ScratchIntegrationHOC", () => { it("calls loadProject with arrayBuffer from event.data.file", async () => { const arrayBuffer = new ArrayBuffer(8); const file = { - arrayBuffer: jest.fn().mockResolvedValue(arrayBuffer), + arrayBuffer: vi.fn().mockResolvedValue(arrayBuffer), }; mockLoadProject.mockResolvedValue(); diff --git a/apps/scratch-frame/src/WrappedScratchGui.jsx b/apps/scratch-frame/src/WrappedScratchGui.jsx new file mode 100644 index 000000000..e6e263500 --- /dev/null +++ b/apps/scratch-frame/src/WrappedScratchGui.jsx @@ -0,0 +1,14 @@ +import ScratchIntegrationHOC from "./ScratchIntegrationHOC.jsx"; +import { compose } from "redux"; + +const scratchGui = window.GUI; +const ScratchComponent = scratchGui.default; + +const appTarget = document.getElementById("app"); +scratchGui.setAppElement(appTarget); +const WrappedScratchGui = compose( + scratchGui.AppStateHOC, + ScratchIntegrationHOC, +)(ScratchComponent); + +export default WrappedScratchGui; diff --git a/apps/scratch-frame/src/bootstrap.jsx b/apps/scratch-frame/src/bootstrap.jsx new file mode 100644 index 000000000..f412a2a86 --- /dev/null +++ b/apps/scratch-frame/src/bootstrap.jsx @@ -0,0 +1,34 @@ +import React from "react"; +import * as ReactDOM from "react-dom"; +import { createRoot } from "react-dom/client"; +import * as ReactRedux from "react-redux"; +import * as Redux from "redux"; + +window.react = React; +window["react-dom"] = { ...ReactDOM, createRoot }; +window.redux = Redux; +window["react-redux"] = ReactRedux; + +const scratchFrameUrl = import.meta.env.REACT_APP_SCRATCH_FRAME_URL; +const scratchGuiScriptUrl = `${scratchFrameUrl}/scratch-gui/scratch-gui.js`; + +const loadScratchGuiScript = () => { + if (window.GUI) { + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + const script = document.createElement("script"); + script.src = scratchGuiScriptUrl; + script.onload = resolve; + script.onerror = () => { + reject( + new Error(`Unable to load Scratch GUI from ${scratchGuiScriptUrl}`), + ); + }; + document.head.appendChild(script); + }); +}; + +await loadScratchGuiScript(); +await import("./scratch.jsx"); diff --git a/src/scratch.jsx b/apps/scratch-frame/src/scratch.jsx similarity index 91% rename from src/scratch.jsx rename to apps/scratch-frame/src/scratch.jsx index f93b9c8d0..b1a69b098 100644 --- a/src/scratch.jsx +++ b/apps/scratch-frame/src/scratch.jsx @@ -1,20 +1,17 @@ import React from "react"; + import { createRoot } from "react-dom/client"; -import process from "process"; import dedupeScratchWarnings from "./utils/dedupeScratchWarnings.js"; -import ScratchStyles from "./assets/stylesheets/Scratch.scss"; -import ScratchEditor from "./components/ScratchEditor/ScratchEditor.jsx"; -import { - postScratchGuiEvent, - allowedParentOrigin, -} from "./components/ScratchEditor/events.js"; +import ScratchStyles from "./stylesheets/Scratch.scss?inline"; +import ScratchEditor from "./ScratchEditor.jsx"; +import { postScratchGuiEvent, allowedParentOrigin } from "./utils/events.js"; dedupeScratchWarnings(); const appTarget = document.getElementById("app"); const scratchLoading = document.getElementById("scratch-loading"); -if (process.env.NODE_ENV === "production" && typeof window === "object") { +if (import.meta.env.PROD && typeof window === "object") { // Warn before navigating away window.onbeforeunload = () => true; } diff --git a/src/scratch.test.js b/apps/scratch-frame/src/scratch.test.js similarity index 70% rename from src/scratch.test.js rename to apps/scratch-frame/src/scratch.test.js index 0a7f601d3..55b000d35 100644 --- a/src/scratch.test.js +++ b/apps/scratch-frame/src/scratch.test.js @@ -1,12 +1,19 @@ -jest.mock("./utils/dedupeScratchWarnings.js", () => jest.fn()); -jest.mock("./assets/stylesheets/Scratch.scss", () => ""); -jest.mock("./components/ScratchEditor/WrappedScratchGui.jsx", () => (props) => { - return null; -}); +import { createRoot } from "react-dom/client"; + +vi.mock("./utils/dedupeScratchWarnings.js", () => ({ + default: vi.fn(), +})); +vi.mock("./stylesheets/Scratch.scss", () => ""); +const { mockRenderRoot } = vi.hoisted(() => ({ + mockRenderRoot: vi.fn(), +})); -const mockRenderRoot = jest.fn(); -jest.mock("react-dom/client", () => ({ - createRoot: jest.fn(() => ({ +vi.mock("./WrappedScratchGui.jsx", () => ({ + default: () => null, +})); + +vi.mock("react-dom/client", () => ({ + createRoot: vi.fn(() => ({ render: mockRenderRoot, })), })); @@ -17,11 +24,7 @@ describe("scratch handshake retries", () => { let consoleErrorSpy; let removeEventListenerSpy; - const loadScratchModule = () => { - jest.isolateModules(() => { - require("./scratch.jsx"); - }); - }; + const loadScratchModule = () => import("./scratch.jsx"); const getHandshakeNonce = () => postMessageSpy.mock.calls[0][0].nonce; @@ -45,11 +48,11 @@ describe("scratch handshake retries", () => { }; const advanceToTimeout = () => { - jest.advanceTimersByTime(15000); + vi.advanceTimersByTime(15000); }; const expectRetriesStopped = (callsAfterHandshake) => { - jest.advanceTimersByTime(20000); + vi.advanceTimersByTime(20000); expect(postMessageSpy).toHaveBeenCalledTimes(callsAfterHandshake); expect(consoleErrorSpy).not.toHaveBeenCalled(); expect(removeEventListenerSpy).toHaveBeenCalledWith( @@ -59,12 +62,13 @@ describe("scratch handshake retries", () => { }; beforeEach(() => { - jest.useFakeTimers(); - jest.resetModules(); + vi.useFakeTimers(); + vi.resetModules(); + createRoot.mockClear(); mockRenderRoot.mockClear(); process.env = { ...originalEnv, - ASSETS_URL: "https://assets.example.com", + REACT_APP_SCRATCH_FRAME_URL: "https://scratch-frame.example.com", }; document.body.innerHTML = @@ -75,23 +79,23 @@ describe("scratch handshake retries", () => { "/scratch.html?project_id=project-123&api_url=https://api.example.com", ); - postMessageSpy = jest.spyOn(window.parent, "postMessage"); - consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {}); + postMessageSpy = vi.spyOn(window.parent, "postMessage"); + consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - removeEventListenerSpy = jest.spyOn(window, "removeEventListener"); + removeEventListenerSpy = vi.spyOn(window, "removeEventListener"); }); afterEach(() => { - jest.runOnlyPendingTimers(); - jest.useRealTimers(); + vi.runOnlyPendingTimers(); + vi.useRealTimers(); process.env = originalEnv; postMessageSpy.mockRestore(); consoleErrorSpy.mockRestore(); removeEventListenerSpy.mockRestore(); }); - test("retries ready handshake until timeout then stops", () => { - loadScratchModule(); + test("retries ready handshake until timeout then stops", async () => { + await loadScratchModule(); expect(postMessageSpy).toHaveBeenCalledTimes(1); @@ -103,12 +107,12 @@ describe("scratch handshake retries", () => { ); const callsAtTimeout = postMessageSpy.mock.calls.length; - jest.advanceTimersByTime(5000); + vi.advanceTimersByTime(5000); expect(postMessageSpy).toHaveBeenCalledTimes(callsAtTimeout); }); - test("stops retries and mounts after valid token message", () => { - loadScratchModule(); + test("stops retries and mounts after valid token message", async () => { + await loadScratchModule(); const nonce = getHandshakeNonce(); dispatchSetTokenMessage({ nonce, accessToken: "token-123" }); @@ -118,7 +122,7 @@ describe("scratch handshake retries", () => { }); test("passes accessToken as a prop", async () => { - loadScratchModule(); + await loadScratchModule(); const nonce = getHandshakeNonce(); dispatchSetTokenMessage({ nonce, accessToken: "token-123" }); @@ -129,14 +133,14 @@ describe("scratch handshake retries", () => { expect(scratchEditorComponent.props.accessToken).toBe("token-123"); }); - test("keeps retrying when auth is required but token is missing", () => { - loadScratchModule(); + test("keeps retrying when auth is required but token is missing", async () => { + await loadScratchModule(); const nonce = getHandshakeNonce(); dispatchSetTokenMessage({ nonce, accessToken: null }); const callsAfterNullToken = postMessageSpy.mock.calls.length; - jest.advanceTimersByTime(1000); + vi.advanceTimersByTime(1000); expect(postMessageSpy.mock.calls.length).toBeGreaterThan( callsAfterNullToken, ); @@ -147,8 +151,8 @@ describe("scratch handshake retries", () => { expectRetriesStopped(callsAfterOneRetry); }); - test("logs auth-specific timeout error when auth is required but token never arrives", () => { - loadScratchModule(); + test("logs auth-specific timeout error when auth is required but token never arrives", async () => { + await loadScratchModule(); const nonce = getHandshakeNonce(); dispatchSetTokenMessage({ nonce, accessToken: null }); @@ -159,8 +163,8 @@ describe("scratch handshake retries", () => { ); }); - test("removes message listener when handshake times out", () => { - loadScratchModule(); + test("removes message listener when handshake times out", async () => { + await loadScratchModule(); advanceToTimeout(); expect(removeEventListenerSpy).toHaveBeenCalledWith( @@ -169,10 +173,9 @@ describe("scratch handshake retries", () => { ); }); - test("ignores late token messages after timeout", () => { - loadScratchModule(); + test("ignores late token messages after timeout", async () => { + await loadScratchModule(); - const { createRoot } = require("react-dom/client"); const nonce = getHandshakeNonce(); advanceToTimeout(); diff --git a/src/assets/stylesheets/Scratch.scss b/apps/scratch-frame/src/stylesheets/Scratch.scss similarity index 100% rename from src/assets/stylesheets/Scratch.scss rename to apps/scratch-frame/src/stylesheets/Scratch.scss diff --git a/src/utils/dedupeScratchWarnings.js b/apps/scratch-frame/src/utils/dedupeScratchWarnings.js similarity index 97% rename from src/utils/dedupeScratchWarnings.js rename to apps/scratch-frame/src/utils/dedupeScratchWarnings.js index 5a372ef1b..798d3c920 100644 --- a/src/utils/dedupeScratchWarnings.js +++ b/apps/scratch-frame/src/utils/dedupeScratchWarnings.js @@ -33,7 +33,7 @@ const scratchWarningMatchers = { const dedupeScratchWarnings = () => { if ( - process.env.NODE_ENV !== "development" || + import.meta.env.PROD || typeof window !== "object" || window[scratchWarningsKey] ) { diff --git a/src/components/ScratchEditor/events.js b/apps/scratch-frame/src/utils/events.js similarity index 100% rename from src/components/ScratchEditor/events.js rename to apps/scratch-frame/src/utils/events.js diff --git a/apps/scratch-frame/src/utils/iframeUtils.js b/apps/scratch-frame/src/utils/iframeUtils.js new file mode 100644 index 000000000..56ee7b0e2 --- /dev/null +++ b/apps/scratch-frame/src/utils/iframeUtils.js @@ -0,0 +1,6 @@ +export function allowedIframeHost(origin) { + const allowedHosts = import.meta.env.REACT_APP_ALLOWED_IFRAME_ORIGINS + ? import.meta.env.REACT_APP_ALLOWED_IFRAME_ORIGINS.split(",") + : []; + return import.meta.env.MODE === "test" || allowedHosts.includes(origin); +} diff --git a/src/utils/scratchProjectSave.js b/apps/scratch-frame/src/utils/scratchProjectSave.js similarity index 100% rename from src/utils/scratchProjectSave.js rename to apps/scratch-frame/src/utils/scratchProjectSave.js diff --git a/src/utils/scratchProjectSave.test.js b/apps/scratch-frame/src/utils/scratchProjectSave.test.js similarity index 92% rename from src/utils/scratchProjectSave.test.js rename to apps/scratch-frame/src/utils/scratchProjectSave.test.js index d0b3fb42f..b1a4e8e38 100644 --- a/src/utils/scratchProjectSave.test.js +++ b/apps/scratch-frame/src/utils/scratchProjectSave.test.js @@ -2,14 +2,14 @@ import scratchProjectSave from "./scratchProjectSave"; describe("scratchProjectSave", () => { const buildScratchFetchApi = () => ({ - scratchFetch: jest.fn(), + scratchFetch: vi.fn(), }); test("updates an existing project through scratchFetch", async () => { const scratchFetchApi = buildScratchFetchApi(); scratchFetchApi.scratchFetch.mockResolvedValue({ status: 200, - json: jest.fn().mockResolvedValue({ ok: true }), + json: vi.fn().mockResolvedValue({ ok: true }), }); const response = await scratchProjectSave({ @@ -38,9 +38,7 @@ describe("scratchProjectSave", () => { const scratchFetchApi = buildScratchFetchApi(); scratchFetchApi.scratchFetch.mockResolvedValue({ status: 200, - json: jest - .fn() - .mockResolvedValue({ "content-name": "created-project-id" }), + json: vi.fn().mockResolvedValue({ "content-name": "created-project-id" }), }); const response = await scratchProjectSave({ @@ -76,7 +74,7 @@ describe("scratchProjectSave", () => { const scratchFetchApi = buildScratchFetchApi(); scratchFetchApi.scratchFetch.mockResolvedValue({ status: 401, - json: jest.fn(), + json: vi.fn(), }); await expect( diff --git a/apps/scratch-frame/vite.config.js b/apps/scratch-frame/vite.config.js new file mode 100644 index 000000000..19a876a10 --- /dev/null +++ b/apps/scratch-frame/vite.config.js @@ -0,0 +1,140 @@ +import { defineConfig, loadEnv } from "vite"; +import { viteStaticCopy } from "vite-plugin-static-copy"; +import react from "@vitejs/plugin-react"; + +const path = require("path"); + +const toOrigin = (envVarName, value) => { + const normalizedValue = String(value || "") + .trim() + .replace(/^['"]|['"]$/g, ""); + + if (!normalizedValue) return ""; + + try { + return new URL(normalizedValue).origin; + } catch (_) { + throw new Error( + `Invalid URL in ${envVarName}: "${value}". ` + + `Expected an absolute URL, for example "https://example.com".`, + ); + } +}; + +export default defineConfig(({ mode }) => { + const envDir = path.resolve(__dirname, "../../"); + const env = loadEnv(mode, envDir, ""); + const isDev = mode === "development"; + + const cspApiOrigin = toOrigin( + "REACT_APP_API_ENDPOINT", + env.REACT_APP_API_ENDPOINT, + ); + const cspAssetOrigin = toOrigin("ASSETS_URL", env.ASSETS_URL); + + // Keep in sync with SCRATCH_LIBRARY_ASSET_URL_TEMPLATE in ScratchEditor.jsx + const cspScratchLibraryAssetOrigin = "https://editor-assets.raspberrypi.org"; + + // When present these override cspApiOrigin for CSP API/connect-src origins. + // This supports staging setups that need to allow multiple API origins, + // such as also reaching the test API. + const cspApiMultipleOrigins = String(env.CSP_API_MULTIPLE_ORIGINS || "") + .split(/[\s,]+/) + .map((originValue, index) => + toOrigin(`CSP_API_MULTIPLE_ORIGINS[${index}]`, originValue), + ) + .filter(Boolean) + .join(" "); + + const htmlTransform = () => { + return { + name: "html-transform", + transformIndexHtml(html) { + html = html.replaceAll( + "%cspApiMultipleOrigins%", + cspApiMultipleOrigins || cspApiOrigin, + ); + html = html.replaceAll("%cspAssetOrigin%", cspAssetOrigin); + html = html.replaceAll( + "%cspScratchLibraryAssetOrigin%", + cspScratchLibraryAssetOrigin, + ); + html = html.replaceAll("%webSocketOrigin%", isDev ? "ws: wss:" : ""); + return html; + }, + }; + }; + + const resolveFromApp = (request) => + require.resolve(request, { paths: [__dirname] }); + + const scratchDistDir = path.dirname( + resolveFromApp("@RaspberryPiFoundation/scratch-gui"), + ); + + const scratchStaticDir = path.resolve(scratchDistDir, "static"); + const scratchChunkDir = path.resolve(scratchDistDir, "chunks"); + const scratchGuiBundlePath = path.resolve(scratchDistDir, "scratch-gui.js"); + const scratchGuiLicensePath = path.resolve( + scratchDistDir, + "scratch-gui.js.LICENSE.txt", + ); + + const staticCopy = viteStaticCopy({ + targets: [ + { + src: scratchGuiBundlePath, + dest: "scratch-gui", + rename: { stripBase: true, name: "scratch-gui.js" }, + }, + { + src: scratchGuiLicensePath, + dest: "scratch-gui", + rename: { stripBase: true, name: "scratch-gui.js.LICENSE.txt" }, + }, + { + src: `${scratchStaticDir}/**/*`, + dest: "scratch-gui/static", + rename: { stripBase: 5 }, + }, + { + src: `${scratchChunkDir}/**/*`, + dest: "chunks", + rename: { stripBase: 5 }, + }, + ], + }); + + return { + plugins: [react({ jsxRuntime: "classic" }), htmlTransform(), staticCopy], + envDir: path.resolve(__dirname, "../../"), + envPrefix: "REACT_APP_", + base: env.REACT_APP_SCRATCH_FRAME_URL, + build: { + outDir: path.resolve(__dirname, "../../build"), + rolldownOptions: { + input: { + scratch: "scratch.html", + }, + }, + }, + server: { + host: true, + port: 3014, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": + "GET, POST, PUT, DELETE, PATCH, OPTIONS", + "Access-Control-Allow-Headers": + "X-Requested-With, content-type, Authorization", + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Embedder-Policy": "require-corp", + "Cross-Origin-Resource-Policy": "cross-origin", + }, + }, + test: { + globals: true, + environment: "jsdom", + }, + }; +}); diff --git a/cypress.config.mjs b/cypress.config.mjs index f554a5669..a098bb090 100644 --- a/cypress.config.mjs +++ b/cypress.config.mjs @@ -111,5 +111,6 @@ export default defineConfig({ }, env: { REACT_APP_API_ENDPOINT: process.env.REACT_APP_API_ENDPOINT, + REACT_APP_SCRATCH_FRAME_URL: process.env.REACT_APP_SCRATCH_FRAME_URL, }, }); diff --git a/cypress/e2e/spec-scratch.cy.js b/cypress/e2e/spec-scratch.cy.js index b1b2fdf59..e8698368c 100644 --- a/cypress/e2e/spec-scratch.cy.js +++ b/cypress/e2e/spec-scratch.cy.js @@ -8,6 +8,7 @@ import { } from "../helpers/scratch.js"; const origin = "http://localhost:3011/web-component.html"; +const scratchFrameOrigin = Cypress.env("REACT_APP_SCRATCH_FRAME_URL"); const authKey = "oidc.user:https://auth-v1.raspberrypi.org:editor-api"; const user = { access_token: "dummy-access-token", @@ -124,7 +125,7 @@ describe("Scratch save integration", () => { cy.window().then((win) => { win.dispatchEvent( new win.MessageEvent("message", { - origin: win.location.origin, + origin: scratchFrameOrigin, data: { type: "scratch-gui-project-id-updated", projectId: "student-remix", @@ -133,7 +134,7 @@ describe("Scratch save integration", () => { ); win.dispatchEvent( new win.MessageEvent("message", { - origin: win.location.origin, + origin: scratchFrameOrigin, data: { type: "scratch-gui-remixing-succeeded", }, @@ -166,7 +167,7 @@ describe("Scratch save integration", () => { cy.window().then((win) => { win.dispatchEvent( new win.MessageEvent("message", { - origin: win.location.origin, + origin: scratchFrameOrigin, data: { type: "scratch-gui-project-changed", }, diff --git a/docker-compose.yml b/docker-compose.yml index ce87e03d4..2ac75a5f2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,8 +11,9 @@ x-app: &x-app services: app: <<: *x-app - command: sh -c "yarn install && yarn start" + command: sh -c "yarn install && yarn start:all" ports: - "3011:3011" + - "3014:3014" volumes: node_modules: null diff --git a/package.json b/package.json index 45ad1392e..040e35bae 100644 --- a/package.json +++ b/package.json @@ -76,14 +76,16 @@ }, "scripts": { "start": "NODE_ENV=development BABEL_ENV=development webpack serve -c ./webpack.config.js", + "start:all": "yarn workspaces foreach -pi --all run start", "build": "NODE_ENV=production BABEL_ENV=production webpack build -c ./webpack.config.js", + "build:all": "yarn workspaces foreach --all run build", "analyze": "ANALYZE_WEBPACK_BUNDLE=true yarn build", - "lint": "eslint 'src/**/*.{js,jsx}' cypress/**/*.js", - "lint:fix": "eslint --fix 'src/**/*.{js,jsx}' cypress/**/*.js", - "stylelint": "stylelint src/**/*.scss", + "lint": "eslint 'src/**/*.{js,jsx}' cypress/**/*.js 'apps/**/*.{js,jsx}'", + "lint:fix": "eslint --fix 'src/**/*.{js,jsx}' cypress/**/*.js 'apps/**/*.{js,jsx}'", + "stylelint": "stylelint 'src/**/*.scss' 'apps/**/*.scss'", "test": "node scripts/test.js", - "watch-css": "sass --load-path=./ -q --watch src:src", - "heroku-postbuild": "export PUBLIC_URL='' && yarn build" + "test:scratch-frame": "yarn workspace @raspberrypifoundation/scratch-frame test", + "watch-css": "sass --load-path=./ -q --watch src:src" }, "browserslist": { "production": [ @@ -219,5 +221,8 @@ ], "resetMocks": true }, + "workspaces": [ + "apps/*" + ], "packageManager": "yarn@4.12.0" } diff --git a/src/components/Editor/Project/ScratchContainer.jsx b/src/components/Editor/Project/ScratchContainer.jsx index db4a434f7..05d3e8843 100644 --- a/src/components/Editor/Project/ScratchContainer.jsx +++ b/src/components/Editor/Project/ScratchContainer.jsx @@ -142,9 +142,7 @@ export default function ScratchContainer() { queryParams.set("scratchMetadata", "1"); queryParams.set("parent_origin", window.location.origin); - const iframeSrcUrl = `${ - process.env.ASSETS_URL - }/scratch.html?${queryParams.toString()}`; + const iframeSrcUrl = `${process.env.REACT_APP_SCRATCH_FRAME_URL}/scratch.html?${queryParams.toString()}`; return (
diff --git a/src/components/Editor/Project/ScratchContainer.test.js b/src/components/Editor/Project/ScratchContainer.test.js index 5563c7647..0e4372e04 100644 --- a/src/components/Editor/Project/ScratchContainer.test.js +++ b/src/components/Editor/Project/ScratchContainer.test.js @@ -80,7 +80,10 @@ describe("ScratchContainer", () => { }; }; - const dispatchMessage = (data, origin = "https://example.com") => { + const dispatchMessage = ( + data, + origin = "https://scratch-frame.example.com", + ) => { window.dispatchEvent( new MessageEvent("message", { origin, @@ -117,11 +120,12 @@ describe("ScratchContainer", () => { }); }; - let originalAssetsUrl; + let originalScratchFrameUrl; beforeEach(() => { - originalAssetsUrl = process.env.ASSETS_URL; - process.env.ASSETS_URL = "https://example.com"; + originalScratchFrameUrl = process.env.REACT_APP_SCRATCH_FRAME_URL; + process.env.REACT_APP_SCRATCH_FRAME_URL = + "https://scratch-frame.example.com"; localStorage.clear(); mockOverlayScrollbarsComponent.mockImplementation( renderMockOverlayScrollbarsComponent, @@ -129,7 +133,7 @@ describe("ScratchContainer", () => { }); afterEach(() => { - process.env.ASSETS_URL = originalAssetsUrl; + process.env.REACT_APP_SCRATCH_FRAME_URL = originalScratchFrameUrl; jest.clearAllMocks(); }); @@ -414,8 +418,9 @@ describe("ScratchContainer", () => { }); }); - test("accepts scratch-gui-ready from origin when ASSETS_URL includes path", () => { - process.env.ASSETS_URL = "https://example.com/branches/main"; + test("accepts scratch-gui-ready from origin when REACT_APP_SCRATCH_FRAME_URL includes path", () => { + process.env.REACT_APP_SCRATCH_FRAME_URL = + "https://example.com/branches/main"; const store = buildStore({ authReducer: (state = { user: { access_token: "token-123" } }) => state, }); diff --git a/src/components/ProjectBar/ScratchProjectBar.test.js b/src/components/ProjectBar/ScratchProjectBar.test.js index a368cadf0..f054cf26d 100644 --- a/src/components/ProjectBar/ScratchProjectBar.test.js +++ b/src/components/ProjectBar/ScratchProjectBar.test.js @@ -85,7 +85,7 @@ const renderSignedInScratchProjectBar = ({ }, }); -const getScratchOrigin = () => process.env.ASSETS_URL || window.location.origin; +const getScratchOrigin = () => process.env.REACT_APP_SCRATCH_FRAME_URL; const dispatchScratchMessage = (type, origin = getScratchOrigin()) => { act(() => { diff --git a/src/components/ScratchEditor/WrappedScratchGui.jsx b/src/components/ScratchEditor/WrappedScratchGui.jsx deleted file mode 100644 index f8e647766..000000000 --- a/src/components/ScratchEditor/WrappedScratchGui.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import GUI, { AppStateHOC } from "@RaspberryPiFoundation/scratch-gui"; -import ScratchIntegrationHOC from "./ScratchIntegrationHOC.jsx"; -import { compose } from "redux"; - -const appTarget = document.getElementById("app"); -GUI.setAppElement(appTarget); -const WrappedScratchGui = compose(AppStateHOC, ScratchIntegrationHOC)(GUI); - -export default WrappedScratchGui; diff --git a/src/hooks/useScratchSaveState.test.js b/src/hooks/useScratchSaveState.test.js index 13b560e55..a0690a496 100644 --- a/src/hooks/useScratchSaveState.test.js +++ b/src/hooks/useScratchSaveState.test.js @@ -17,7 +17,7 @@ jest.mock("../utils/scratchIframe", () => ({ jest.useFakeTimers(); -const scratchOrigin = "https://assets.example.com"; +const scratchOrigin = "https://scratch-frame.example.com"; const scratchProject = { identifier: "scratch-project", project_type: "code_editor_scratch", @@ -60,17 +60,17 @@ const dispatchScratchMessage = (type, origin = scratchOrigin) => { }; describe("useScratchSaveState", () => { - const originalAssetsUrl = process.env.ASSETS_URL; + const originalScratchFrameUrl = process.env.REACT_APP_SCRATCH_FRAME_URL; beforeEach(() => { jest.clearAllMocks(); - process.env.ASSETS_URL = scratchOrigin; + process.env.REACT_APP_SCRATCH_FRAME_URL = scratchOrigin; getScratchAllowedOrigin.mockReturnValue(scratchOrigin); }); afterEach(() => { jest.clearAllTimers(); - process.env.ASSETS_URL = originalAssetsUrl; + process.env.REACT_APP_SCRATCH_FRAME_URL = originalScratchFrameUrl; }); test("posts the scratch save command", () => { @@ -282,8 +282,8 @@ describe("useScratchSaveState", () => { expect(store.getState().editor.saving).toBe("idle"); }); - test("accepts messages when ASSETS_URL contains a path", () => { - process.env.ASSETS_URL = `${scratchOrigin}/branches/main`; + test("accepts messages when REACT_APP_SCRATCH_FRAME_URL contains a path", () => { + process.env.REACT_APP_SCRATCH_FRAME_URL = `${scratchOrigin}/branches/main`; getScratchAllowedOrigin.mockReturnValue(scratchOrigin); const { store } = renderScratchSaveState({ enabled: true }); diff --git a/src/utils/scratchIframe.js b/src/utils/scratchIframe.js index 912256f5a..f3072b0ab 100644 --- a/src/utils/scratchIframe.js +++ b/src/utils/scratchIframe.js @@ -5,18 +5,8 @@ export const getScratchIframeContentWindow = () => { }; export const getScratchAllowedOrigin = () => { - const fallbackOrigin = window.location.origin; - const configuredAssetsUrl = process.env.ASSETS_URL; - - if (!configuredAssetsUrl) { - return fallbackOrigin; - } - - try { - return new URL(configuredAssetsUrl).origin; - } catch (error) { - return fallbackOrigin; - } + const url = process.env.REACT_APP_SCRATCH_FRAME_URL; + return new URL(url).origin; }; export const postMessageToScratchIframe = (message) => { diff --git a/src/utils/scratchIframe.test.js b/src/utils/scratchIframe.test.js index ba77821dc..e373a56b4 100644 --- a/src/utils/scratchIframe.test.js +++ b/src/utils/scratchIframe.test.js @@ -44,7 +44,7 @@ describe("scratchIframe", () => { beforeEach(() => { process.env = { ...originalEnv, - ASSETS_URL: "https://assets.example.com", + REACT_APP_SCRATCH_FRAME_URL: "https://scratch-frame.example.com", }; }); @@ -58,34 +58,36 @@ describe("scratchIframe", () => { expect(mockPostMessage).toHaveBeenCalledTimes(1); expect(mockPostMessage).toHaveBeenCalledWith( message, - "https://assets.example.com", + "https://scratch-frame.example.com", ); }); - it("uses only the origin when ASSETS_URL includes a path", () => { + it("uses only the origin when REACT_APP_SCRATCH_FRAME_URL includes a path", () => { process.env = { ...originalEnv, - ASSETS_URL: "https://assets.example.com/branches/main", + REACT_APP_SCRATCH_FRAME_URL: + "https://scratch-frame.example.com/branches/main", }; const message = { type: "scratch-gui-download", filename: "cool.sb3" }; postMessageToScratchIframe(message); expect(mockPostMessage).toHaveBeenCalledTimes(1); expect(mockPostMessage).toHaveBeenCalledWith( message, - "https://assets.example.com", + "https://scratch-frame.example.com", ); }); }); describe("subscribeToScratchProjectIdentifierUpdates", () => { - const originalAssetsUrl = process.env.ASSETS_URL; + const originalScratchFrameUrl = process.env.REACT_APP_SCRATCH_FRAME_URL; beforeEach(() => { - process.env.ASSETS_URL = "https://assets.example.com"; + process.env.REACT_APP_SCRATCH_FRAME_URL = + "https://scratch-frame.example.com"; }); afterEach(() => { - process.env.ASSETS_URL = originalAssetsUrl; + process.env.REACT_APP_SCRATCH_FRAME_URL = originalScratchFrameUrl; }); it("calls the handler with the updated project id", () => { @@ -94,7 +96,7 @@ describe("scratchIframe", () => { window.dispatchEvent( new MessageEvent("message", { - origin: "https://assets.example.com", + origin: "https://scratch-frame.example.com", data: { type: "scratch-gui-project-id-updated", projectId: "project-456", @@ -106,14 +108,15 @@ describe("scratchIframe", () => { unsubscribe(); }); - it("accepts updates when ASSETS_URL contains a path", () => { - process.env.ASSETS_URL = "https://assets.example.com/branches/main"; + it("accepts updates when REACT_APP_SCRATCH_FRAME_URL contains a path", () => { + process.env.REACT_APP_SCRATCH_FRAME_URL = + "https://scratch-frame.example.com/branches/main"; const handler = jest.fn(); const unsubscribe = subscribeToScratchProjectIdentifierUpdates(handler); window.dispatchEvent( new MessageEvent("message", { - origin: "https://assets.example.com", + origin: "https://scratch-frame.example.com", data: { type: "scratch-gui-project-id-updated", projectId: "project-789", @@ -141,7 +144,7 @@ describe("scratchIframe", () => { window.dispatchEvent( new MessageEvent("message", { - origin: "https://assets.example.com", + origin: "https://scratch-frame.example.com", data: { type: "scratch-gui-saving-succeeded", }, @@ -160,12 +163,15 @@ describe("scratchIframe", () => { process.env = originalEnv; }); - it("returns origin when ASSETS_URL contains a path", () => { + it("returns origin when REACT_APP_SCRATCH_FRAME_URL contains a path", () => { process.env = { ...originalEnv, - ASSETS_URL: "https://assets.example.com/branches/main", + REACT_APP_SCRATCH_FRAME_URL: + "https://scratch-frame.example.com/branches/main", }; - expect(getScratchAllowedOrigin()).toBe("https://assets.example.com"); + expect(getScratchAllowedOrigin()).toBe( + "https://scratch-frame.example.com", + ); }); }); diff --git a/webpack.config.js b/webpack.config.js index 3c090b227..9bdd100a7 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -12,54 +12,6 @@ let publicUrl = process.env.PUBLIC_URL || "/"; if (!publicUrl.endsWith("/")) { publicUrl += "/"; } -const isDev = process.env.NODE_ENV !== "production"; - -const toOrigin = (envVarName, value) => { - const normalizedValue = String(value || "") - .trim() - .replace(/^['"]|['"]$/g, ""); - - if (!normalizedValue) return ""; - - try { - return new URL(normalizedValue).origin; - } catch (_) { - throw new Error( - `Invalid URL in ${envVarName}: "${value}". ` + - `Expected an absolute URL, for example "https://example.com".`, - ); - } -}; - -const cspApiOrigin = toOrigin( - "REACT_APP_API_ENDPOINT", - process.env.REACT_APP_API_ENDPOINT, -); -const cspAssetOrigin = toOrigin("ASSETS_URL", process.env.ASSETS_URL); - -// Keep in sync with SCRATCH_LIBRARY_ASSET_URL_TEMPLATE in ScratchEditor.jsx -const cspScratchLibraryAssetOrigin = "https://editor-assets.raspberrypi.org"; - -// When present these override cspApiOrigin for CSP API/connect-src origins. -// This supports staging setups that need to allow multiple API origins, -// such as also reaching the test API. -const cspApiMultipleOrigins = String(process.env.CSP_API_MULTIPLE_ORIGINS || "") - .split(/[\s,]+/) - .map((originValue, index) => - toOrigin(`CSP_API_MULTIPLE_ORIGINS[${index}]`, originValue), - ) - .filter(Boolean) - .join(" "); - -const scratchStaticDir = path.resolve( - __dirname, - "node_modules/@RaspberryPiFoundation/scratch-gui/dist/static", -); - -const scratchChunkDir = path.resolve( - __dirname, - "node_modules/@RaspberryPiFoundation/scratch-gui/dist/chunks", -); const moduleRules = [ { @@ -184,20 +136,12 @@ const mainConfig = { directory: path.join(__dirname, "src", "projects"), publicPath: `${publicUrl}projects`, }, - { - directory: scratchStaticDir, - publicPath: `${publicUrl}scratch-gui/static`, - }, - { - directory: scratchChunkDir, - publicPath: `${publicUrl}scratch-gui/chunks`, - }, ], headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS", "Access-Control-Allow-Headers": - "X-Requested-With, content-type, Authorization", + "X-Requested-With, content-type, Authorization, x-run-id, x-project-id", // Pyodide - required for input and code interruption - needed on the host app "Cross-Origin-Opener-Policy": "same-origin", "Cross-Origin-Embedder-Policy": "require-corp", @@ -209,8 +153,8 @@ const mainConfig = { "/pyodide/shims/_internal_sense_hat.js", "/pyodide/shims/pygal.js", "/PyodideWorker.js", + "/api/scratch/projects/cool-scratch.json", ].includes(req.url) || - req.url.startsWith("/scratch.html") || req.url.startsWith("/html-renderer.html") ) { res.setHeader("Cross-Origin-Resource-Policy", "cross-origin"); @@ -253,83 +197,4 @@ const mainConfig = { stats: "minimal", }; -const scratchConfig = { - entry: { - scratch: path.resolve(__dirname, "./src/scratch.jsx"), - }, - module: { rules: moduleRules }, - resolve: { - extensions: [".*", ".js", ".jsx", ".css"], - }, - output: { - path: path.resolve(__dirname, "./build"), - filename: "[name].js", - publicPath: publicUrl, - }, - externals: [ - function ({ request }, callback) { - if (request === "@RaspberryPiFoundation/scratch-gui") - return callback(null, "GUI"); - if (request === "react") return callback(null, "React"); - if (request === "react-dom" || request.startsWith("react-dom/")) - return callback(null, "ReactDOM"); - if (request === "redux") return callback(null, "Redux"); - if (request === "react-redux") return callback(null, "ReactRedux"); - callback(); - }, - ], - plugins: [ - new Dotenv({ - path: "./.env", - systemvars: true, - }), - new HtmlWebpackPlugin({ - inject: "body", - template: "src/scratch.html", - filename: "scratch.html", - chunks: ["scratch"], - templateParameters: { - publicUrl: publicUrl, - cspApiOrigin, - cspApiMultipleOrigins, - cspAssetOrigin, - cspScratchLibraryAssetOrigin, - isDev, - }, - }), - new CopyWebpackPlugin({ - patterns: [ - { from: scratchStaticDir, to: "scratch-gui/static" }, - { from: `${scratchStaticDir}/assets`, to: "vendor/static/assets" }, - { from: scratchChunkDir, to: "chunks" }, - { - from: "node_modules/scratchReactVendor/umd/react.production.min.js", - to: "vendor/react.production.min.js", - }, - { - from: "node_modules/scratchReactDomVendor/umd/react-dom.production.min.js", - to: "vendor/react-dom.production.min.js", - }, - { - from: "node_modules/redux/dist/redux.min.js", - to: "vendor/redux.min.js", - }, - { - from: "node_modules/react-redux/dist/react-redux.min.js", - to: "vendor/react-redux.min.js", - }, - { - from: "node_modules/@RaspberryPiFoundation/scratch-gui/dist/scratch-gui.js", - to: "vendor/scratch-gui.js", - }, - { - from: "node_modules/@RaspberryPiFoundation/scratch-gui/dist/scratch-gui.js.LICENSE.txt", - to: "vendor/scratch-gui.js.LICENSE.txt", - }, - ], - }), - ], - stats: "minimal", -}; - -module.exports = [mainConfig, scratchConfig]; +module.exports = [mainConfig]; diff --git a/yarn.lock b/yarn.lock index 1d8bbbc3e..e7f586a5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2306,6 +2306,34 @@ __metadata: languageName: node linkType: hard +"@emnapi/core@npm:1.11.1": + version: 1.11.1 + resolution: "@emnapi/core@npm:1.11.1" + dependencies: + "@emnapi/wasi-threads": "npm:1.2.2" + tslib: "npm:^2.4.0" + checksum: 10/9aba37e0c11a75ef8372fd0a9c6e5396f4e8c1ebdd6fee737414787610a9dc1cd9bf188f525153561ca9363896e1135dd240f1ce28f3470dba3ad7e683e6db1a + languageName: node + linkType: hard + +"@emnapi/runtime@npm:1.11.1": + version: 1.11.1 + resolution: "@emnapi/runtime@npm:1.11.1" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10/8f7c622a49314df4d07952110e108e83b0fe129a8ddb9ef1e0ae372d754616169d5b0dd47a0d354a0fea2612abe42cedb582d15916936d1320c6c468acc804cc + languageName: node + linkType: hard + +"@emnapi/wasi-threads@npm:1.2.2": + version: 1.2.2 + resolution: "@emnapi/wasi-threads@npm:1.2.2" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10/297fb6b1d89744bd0b41d5fec32bade05dc8dcf1f70eba86527226609fb3f6ad3fa96b3b2377b7449709715b3bd1569654c9def1dbbc85fb6b9cb0cff5bc5ebf + languageName: node + linkType: hard + "@esbuild/aix-ppc64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/aix-ppc64@npm:0.27.2" @@ -3025,7 +3053,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.5.0": +"@jridgewell/sourcemap-codec@npm:^1.5.0, @jridgewell/sourcemap-codec@npm:^1.5.5": version: 1.5.5 resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" checksum: 10/5d9d207b462c11e322d71911e55e21a4e2772f71ffe8d6f1221b8eb5ae6774458c1d242f897fb0814e8714ca9a6b498abfa74dfe4f434493342902b1a48b33a5 @@ -3325,6 +3353,18 @@ __metadata: languageName: node linkType: hard +"@napi-rs/wasm-runtime@npm:^1.1.6": + version: 1.1.6 + resolution: "@napi-rs/wasm-runtime@npm:1.1.6" + dependencies: + "@tybys/wasm-util": "npm:^0.10.3" + peerDependencies: + "@emnapi/core": ^1.7.1 + "@emnapi/runtime": ^1.7.1 + checksum: 10/3e43425df17547d9d58ab69cce8e6cef38a062eccec4d2def5fc9e10e81cd19ae228b3ab9be4b149b57078d33913687511312d2414089877759b7db1f43625ba + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -3394,6 +3434,13 @@ __metadata: languageName: node linkType: hard +"@oxc-project/types@npm:=0.137.0": + version: 0.137.0 + resolution: "@oxc-project/types@npm:0.137.0" + checksum: 10/50a961188b0fec059a709445290dff201f010c7dee69a15b35251d43ecd7c1f2801b165c6f744a2a8a37a81924d56dc8f35f05d8308bdb704ee6bf0a0275ad36 + languageName: node + linkType: hard + "@parcel/watcher-android-arm64@npm:2.4.1": version: 2.4.1 resolution: "@parcel/watcher-android-arm64@npm:2.4.1" @@ -4267,6 +4314,24 @@ __metadata: languageName: node linkType: hard +"@raspberrypifoundation/scratch-frame@workspace:apps/scratch-frame": + version: 0.0.0-use.local + resolution: "@raspberrypifoundation/scratch-frame@workspace:apps/scratch-frame" + dependencies: + "@RaspberryPiFoundation/scratch-gui": "npm:13.7.3-code-classroom.20260522151700" + "@testing-library/react": "npm:14.3.1" + "@vitejs/plugin-react": "npm:^6.0.2" + react: "npm:18.3.1" + react-dom: "npm:18.3.1" + react-redux: "npm:^8.1.3" + redux: "npm:^4.2.1" + redux-mock-store: "npm:^1.5.4" + vite: "npm:^8.0.16" + vite-plugin-static-copy: "npm:^4.1.1" + vitest: "npm:^4.1.9" + languageName: unknown + linkType: soft + "@react-three/drei@npm:^10.0.0": version: 10.7.7 resolution: "@react-three/drei@npm:10.7.7" @@ -4393,6 +4458,122 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-android-arm64@npm:1.1.3": + version: 1.1.3 + resolution: "@rolldown/binding-android-arm64@npm:1.1.3" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-darwin-arm64@npm:1.1.3": + version: 1.1.3 + resolution: "@rolldown/binding-darwin-arm64@npm:1.1.3" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-darwin-x64@npm:1.1.3": + version: 1.1.3 + resolution: "@rolldown/binding-darwin-x64@npm:1.1.3" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@rolldown/binding-freebsd-x64@npm:1.1.3": + version: 1.1.3 + resolution: "@rolldown/binding-freebsd-x64@npm:1.1.3" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@rolldown/binding-linux-arm-gnueabihf@npm:1.1.3": + version: 1.1.3 + resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.1.3" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@rolldown/binding-linux-arm64-gnu@npm:1.1.3": + version: 1.1.3 + resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.1.3" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-arm64-musl@npm:1.1.3": + version: 1.1.3 + resolution: "@rolldown/binding-linux-arm64-musl@npm:1.1.3" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rolldown/binding-linux-ppc64-gnu@npm:1.1.3": + version: 1.1.3 + resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.1.3" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-s390x-gnu@npm:1.1.3": + version: 1.1.3 + resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.1.3" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-x64-gnu@npm:1.1.3": + version: 1.1.3 + resolution: "@rolldown/binding-linux-x64-gnu@npm:1.1.3" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-x64-musl@npm:1.1.3": + version: 1.1.3 + resolution: "@rolldown/binding-linux-x64-musl@npm:1.1.3" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rolldown/binding-openharmony-arm64@npm:1.1.3": + version: 1.1.3 + resolution: "@rolldown/binding-openharmony-arm64@npm:1.1.3" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-wasm32-wasi@npm:1.1.3": + version: 1.1.3 + resolution: "@rolldown/binding-wasm32-wasi@npm:1.1.3" + dependencies: + "@emnapi/core": "npm:1.11.1" + "@emnapi/runtime": "npm:1.11.1" + "@napi-rs/wasm-runtime": "npm:^1.1.6" + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@rolldown/binding-win32-arm64-msvc@npm:1.1.3": + version: 1.1.3 + resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.1.3" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-win32-x64-msvc@npm:1.1.3": + version: 1.1.3 + resolution: "@rolldown/binding-win32-x64-msvc@npm:1.1.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@rolldown/pluginutils@npm:^1.0.0, @rolldown/pluginutils@npm:^1.0.1": + version: 1.0.1 + resolution: "@rolldown/pluginutils@npm:1.0.1" + checksum: 10/4e95cf9ce23d75e5aa03ea0249cd86f7d1e21f83fbf6f8520e4edd8a251ba1b82c4ba9bc13cd24b6c4661daec6225b06e6d35c64c604e731b230b2a49af47d05 + languageName: node + linkType: hard + "@rtsao/scc@npm:^1.1.0": version: 1.1.0 resolution: "@rtsao/scc@npm:1.1.0" @@ -4584,6 +4765,13 @@ __metadata: languageName: node linkType: hard +"@standard-schema/spec@npm:^1.1.0": + version: 1.1.0 + resolution: "@standard-schema/spec@npm:1.1.0" + checksum: 10/a209615c9e8b2ea535d7db0a5f6aa0f962fd4ab73ee86a46c100fb78116964af1f55a27c1794d4801e534a196794223daa25ff5135021e03c7828aa3d95e1763 + languageName: node + linkType: hard + "@svgr/babel-plugin-add-jsx-attribute@npm:^5.4.0": version: 5.4.0 resolution: "@svgr/babel-plugin-add-jsx-attribute@npm:5.4.0" @@ -5011,6 +5199,15 @@ __metadata: languageName: node linkType: hard +"@tybys/wasm-util@npm:^0.10.3": + version: 0.10.3 + resolution: "@tybys/wasm-util@npm:0.10.3" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10/6cf39f7a2926b1c8bc6fe3f9f03318a33dd6dae81bdbd059983f9c6ee22d10a827f12564d648c05a2d4926e03c86cbe2799fb20609ee65e9efc39603039b4765 + languageName: node + linkType: hard + "@types/aria-query@npm:^5.0.1": version: 5.0.4 resolution: "@types/aria-query@npm:5.0.4" @@ -5078,6 +5275,16 @@ __metadata: languageName: node linkType: hard +"@types/chai@npm:^5.2.2": + version: 5.2.3 + resolution: "@types/chai@npm:5.2.3" + dependencies: + "@types/deep-eql": "npm:*" + assertion-error: "npm:^2.0.1" + checksum: 10/e79947307dc235953622e65f83d2683835212357ca261389116ab90bed369ac862ba28b146b4fed08b503ae1e1a12cb93ce783f24bb8d562950469f4320e1c7c + languageName: node + linkType: hard + "@types/connect-history-api-fallback@npm:^1.5.4": version: 1.5.4 resolution: "@types/connect-history-api-fallback@npm:1.5.4" @@ -5104,6 +5311,13 @@ __metadata: languageName: node linkType: hard +"@types/deep-eql@npm:*": + version: 4.0.2 + resolution: "@types/deep-eql@npm:4.0.2" + checksum: 10/249a27b0bb22f6aa28461db56afa21ec044fa0e303221a62dff81831b20c8530502175f1a49060f7099e7be06181078548ac47c668de79ff9880241968d43d0c + languageName: node + linkType: hard + "@types/draco3d@npm:^1.4.0": version: 1.4.10 resolution: "@types/draco3d@npm:1.4.10" @@ -5138,6 +5352,13 @@ __metadata: languageName: node linkType: hard +"@types/estree@npm:^1.0.0": + version: 1.0.9 + resolution: "@types/estree@npm:1.0.9" + checksum: 10/16aabfa703b5bdac83f719b07ce92a11b2d3c9b8628eacc92889d8af46cab2d78fc45c7b5378de383d0500585cea5c2f79125eeddfe5fbc6bd6a27eb0c8ccee5 + languageName: node + linkType: hard + "@types/estree@npm:^1.0.8": version: 1.0.8 resolution: "@types/estree@npm:1.0.8" @@ -5858,6 +6079,106 @@ __metadata: languageName: node linkType: hard +"@vitejs/plugin-react@npm:^6.0.2": + version: 6.0.3 + resolution: "@vitejs/plugin-react@npm:6.0.3" + dependencies: + "@rolldown/pluginutils": "npm:^1.0.1" + peerDependencies: + "@rolldown/plugin-babel": ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + "@rolldown/plugin-babel": + optional: true + babel-plugin-react-compiler: + optional: true + checksum: 10/135a63a27592108ea0fcbd8a5d71753f9e8b484568113e15975f59d48bd796ca361de501e4acec6f04ae1e5dcb2d266b82ed158c759c4177b2504755d481e35c + languageName: node + linkType: hard + +"@vitest/expect@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/expect@npm:4.1.9" + dependencies: + "@standard-schema/spec": "npm:^1.1.0" + "@types/chai": "npm:^5.2.2" + "@vitest/spy": "npm:4.1.9" + "@vitest/utils": "npm:4.1.9" + chai: "npm:^6.2.2" + tinyrainbow: "npm:^3.1.0" + checksum: 10/aba1a06cd28199f9c861d97797b014c0584fa6f6197e78345da0db5f74914d47f18958bb848658e889ca44452aa61e07ae851c16ea7b2175afd50d649dd4ed8c + languageName: node + linkType: hard + +"@vitest/mocker@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/mocker@npm:4.1.9" + dependencies: + "@vitest/spy": "npm:4.1.9" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.21" + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10/3e35ff3e2ecbdfbcae598e9c5c83978dd5f0cf3b16df37cf947c80faabce797ab275ca2075c3bb8ca85f595f3070267f93cb6798bbe415f1af2698f51833974c + languageName: node + linkType: hard + +"@vitest/pretty-format@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/pretty-format@npm:4.1.9" + dependencies: + tinyrainbow: "npm:^3.1.0" + checksum: 10/52512b300c000594c54bebbbfe31fab39e416a35d3686e2c46bc8e48ef8476d32306605f7736139608c3962943e0d22790dc15a3e6b1ffa436143d31f743a7c8 + languageName: node + linkType: hard + +"@vitest/runner@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/runner@npm:4.1.9" + dependencies: + "@vitest/utils": "npm:4.1.9" + pathe: "npm:^2.0.3" + checksum: 10/52e4e16e627faa62676f17683e570f505d58d2ce0ef421a3ae60e70c0ec5606d4af090fa6c7d5717d6e949f4401d6357b1f69cf06e52a5455a0ad9c9040268c0 + languageName: node + linkType: hard + +"@vitest/snapshot@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/snapshot@npm:4.1.9" + dependencies: + "@vitest/pretty-format": "npm:4.1.9" + "@vitest/utils": "npm:4.1.9" + magic-string: "npm:^0.30.21" + pathe: "npm:^2.0.3" + checksum: 10/c83349b1ad08d48284c1d3393168a7b7faffd24ace1ef337751a568dad322d83b0f9bc29378a4a60379cf2a13a268092b1d802936d6adb1ca28859f02dad8b87 + languageName: node + linkType: hard + +"@vitest/spy@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/spy@npm:4.1.9" + checksum: 10/8b8e42cc8e4b20d29bd8b312f34b9dbf2e20d4b4cdc24e3bcf6fd4d3b1f49e8924636d2730cca3946fbb45de893dfb531c77b832eb853c2624fdc2b800444e75 + languageName: node + linkType: hard + +"@vitest/utils@npm:4.1.9": + version: 4.1.9 + resolution: "@vitest/utils@npm:4.1.9" + dependencies: + "@vitest/pretty-format": "npm:4.1.9" + convert-source-map: "npm:^2.0.0" + tinyrainbow: "npm:^3.1.0" + checksum: 10/78f5969fc09b1a95fda9dadd37e84a3a6ead35f66af15ad3b792eef35f80407047803e7afd53df86a8d794f59bf25ffbdc4146099140a3d5f9b51ea061bf2308 + languageName: node + linkType: hard + "@webassemblyjs/ast@npm:1.14.1, @webassemblyjs/ast@npm:^1.14.1": version: 1.14.1 resolution: "@webassemblyjs/ast@npm:1.14.1" @@ -6678,6 +6999,13 @@ __metadata: languageName: node linkType: hard +"assertion-error@npm:^2.0.1": + version: 2.0.1 + resolution: "assertion-error@npm:2.0.1" + checksum: 10/a0789dd882211b87116e81e2648ccb7f60340b34f19877dd020b39ebb4714e475eb943e14ba3e22201c221ef6645b7bfe10297e76b6ac95b48a9898c1211ce66 + languageName: node + linkType: hard + "assign-symbols@npm:^1.0.0": version: 1.0.0 resolution: "assign-symbols@npm:1.0.0" @@ -7622,6 +7950,13 @@ __metadata: languageName: node linkType: hard +"chai@npm:^6.2.2": + version: 6.2.2 + resolution: "chai@npm:6.2.2" + checksum: 10/13cda42cc40aa46da04a41cf7e5c61df6b6ae0b4e8a8c8b40e04d6947e4d7951377ea8c14f9fa7fe5aaa9e8bd9ba414f11288dc958d4cee6f5221b9436f2778f + languageName: node + linkType: hard + "chalk@npm:2.4.2, chalk@npm:^2.4.1, chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -9447,6 +9782,13 @@ __metadata: languageName: node linkType: hard +"detect-libc@npm:^2.0.3": + version: 2.1.2 + resolution: "detect-libc@npm:2.1.2" + checksum: 10/b736c8d97d5d46164c0d1bed53eb4e6a3b1d8530d460211e2d52f1c552875e706c58a5376854e4e54f8b828c9cada58c855288c968522eb93ac7696d65970766 + languageName: node + linkType: hard + "detect-newline@npm:^3.0.0": version: 3.1.0 resolution: "detect-newline@npm:3.1.0" @@ -10837,6 +11179,15 @@ __metadata: languageName: node linkType: hard +"estree-walker@npm:^3.0.3": + version: 3.0.3 + resolution: "estree-walker@npm:3.0.3" + dependencies: + "@types/estree": "npm:^1.0.0" + checksum: 10/a65728d5727b71de172c5df323385755a16c0fdab8234dc756c3854cfee343261ddfbb72a809a5660fac8c75d960bb3e21aa898c2d7e9b19bb298482ca58a3af + languageName: node + linkType: hard + "esutils@npm:^2.0.2": version: 2.0.3 resolution: "esutils@npm:2.0.3" @@ -10954,6 +11305,13 @@ __metadata: languageName: node linkType: hard +"expect-type@npm:^1.3.0": + version: 1.4.0 + resolution: "expect-type@npm:1.4.0" + checksum: 10/bad91f4b7eb807248695ee840a935d12818fe531ad523bc7ac7dcc540a4d86dc566a3ef829f4da6e8b5a458ac84092771739865114c91e7e8a9317302f401f09 + languageName: node + linkType: hard + "expect@npm:^29.0.0, expect@npm:^29.7.0": version: 29.7.0 resolution: "expect@npm:29.7.0" @@ -11225,6 +11583,18 @@ __metadata: languageName: node linkType: hard +"fdir@npm:^6.5.0": + version: 6.5.0 + resolution: "fdir@npm:6.5.0" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10/14ca1c9f0a0e8f4f2e9bf4e8551065a164a09545dae548c12a18d238b72e51e5a7b39bd8e5494b56463a0877672d0a6c1ef62c6fa0677db1b0c847773be939b1 + languageName: node + linkType: hard + "fflate@npm:^0.6.9": version: 0.6.10 resolution: "fflate@npm:0.6.10" @@ -15110,6 +15480,126 @@ __metadata: languageName: node linkType: hard +"lightningcss-android-arm64@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-android-arm64@npm:1.32.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"lightningcss-darwin-arm64@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-darwin-arm64@npm:1.32.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"lightningcss-darwin-x64@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-darwin-x64@npm:1.32.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"lightningcss-freebsd-x64@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-freebsd-x64@npm:1.32.0" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"lightningcss-linux-arm-gnueabihf@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-linux-arm-gnueabihf@npm:1.32.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"lightningcss-linux-arm64-gnu@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-linux-arm64-gnu@npm:1.32.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"lightningcss-linux-arm64-musl@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-linux-arm64-musl@npm:1.32.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"lightningcss-linux-x64-gnu@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-linux-x64-gnu@npm:1.32.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"lightningcss-linux-x64-musl@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-linux-x64-musl@npm:1.32.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"lightningcss-win32-arm64-msvc@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-win32-arm64-msvc@npm:1.32.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"lightningcss-win32-x64-msvc@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-win32-x64-msvc@npm:1.32.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"lightningcss@npm:^1.32.0": + version: 1.32.0 + resolution: "lightningcss@npm:1.32.0" + dependencies: + detect-libc: "npm:^2.0.3" + lightningcss-android-arm64: "npm:1.32.0" + lightningcss-darwin-arm64: "npm:1.32.0" + lightningcss-darwin-x64: "npm:1.32.0" + lightningcss-freebsd-x64: "npm:1.32.0" + lightningcss-linux-arm-gnueabihf: "npm:1.32.0" + lightningcss-linux-arm64-gnu: "npm:1.32.0" + lightningcss-linux-arm64-musl: "npm:1.32.0" + lightningcss-linux-x64-gnu: "npm:1.32.0" + lightningcss-linux-x64-musl: "npm:1.32.0" + lightningcss-win32-arm64-msvc: "npm:1.32.0" + lightningcss-win32-x64-msvc: "npm:1.32.0" + dependenciesMeta: + lightningcss-android-arm64: + optional: true + lightningcss-darwin-arm64: + optional: true + lightningcss-darwin-x64: + optional: true + lightningcss-freebsd-x64: + optional: true + lightningcss-linux-arm-gnueabihf: + optional: true + lightningcss-linux-arm64-gnu: + optional: true + lightningcss-linux-arm64-musl: + optional: true + lightningcss-linux-x64-gnu: + optional: true + lightningcss-linux-x64-musl: + optional: true + lightningcss-win32-arm64-msvc: + optional: true + lightningcss-win32-x64-msvc: + optional: true + checksum: 10/098e61007f0d0ec8b5c50884e33b543b551d1ff21bc7b062434b6638fd0b8596858f823b60dfc2a4aa756f3cb120ad79f2b7f4a55b1bda2c0269ab8cf476f114 + languageName: node + linkType: hard + "linebreak@npm:0.3.0": version: 0.3.0 resolution: "linebreak@npm:0.3.0" @@ -15444,6 +15934,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.30.21": + version: 0.30.21 + resolution: "magic-string@npm:0.30.21" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.5" + checksum: 10/57d5691f41ed40d962d8bd300148114f53db67fadbff336207db10a99f2bdf4a1be9cac3a68ee85dba575912ee1d4402e4396408196ec2d3afd043b076156221 + languageName: node + linkType: hard + "make-dir@npm:^3.0.2, make-dir@npm:^3.1.0": version: 3.1.0 resolution: "make-dir@npm:3.1.0" @@ -16141,6 +16640,15 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:^3.3.12": + version: 3.3.15 + resolution: "nanoid@npm:3.3.15" + bin: + nanoid: bin/nanoid.cjs + checksum: 10/13c74a5208d455286f7af46f42ac9f3d7b821b8a719aff8dbd5ad3fb80399c0c63cdd1e92d046ea576e403bec05262fbb7e116beb4cfcd5b5483550372bd94b1 + languageName: node + linkType: hard + "nanoid@npm:^3.3.7": version: 3.3.7 resolution: "nanoid@npm:3.3.7" @@ -16606,6 +17114,13 @@ __metadata: languageName: node linkType: hard +"obug@npm:^2.1.1": + version: 2.1.3 + resolution: "obug@npm:2.1.3" + checksum: 10/574e5c3d3def75440c54c29ce8a6d82a5698116962d2f9f28fb13f6fd5dbcc01b7df1b8e02a95f816c8dba482dcf315d9c5c97d9e718516cfea8bd6c896dcf67 + languageName: node + linkType: hard + "oidc-client@npm:^1.11.5": version: 1.11.5 resolution: "oidc-client@npm:1.11.5" @@ -16797,6 +17312,13 @@ __metadata: languageName: node linkType: hard +"p-map@npm:^7.0.4": + version: 7.0.4 + resolution: "p-map@npm:7.0.4" + checksum: 10/ef48c3b2e488f31c693c9fcc0df0ef76518cf6426a495cf9486ebbb0fd7f31aef7f90e96f72e0070c0ff6e3177c9318f644b512e2c29e3feee8d7153fcb6782e + languageName: node + linkType: hard + "p-retry@npm:^6.2.0": version: 6.2.0 resolution: "p-retry@npm:6.2.0" @@ -17044,6 +17566,13 @@ __metadata: languageName: node linkType: hard +"pathe@npm:^2.0.3": + version: 2.0.3 + resolution: "pathe@npm:2.0.3" + checksum: 10/01e9a69928f39087d96e1751ce7d6d50da8c39abf9a12e0ac2389c42c83bc76f78c45a475bd9026a02e6a6f79be63acc75667df855862fe567d99a00a540d23d + languageName: node + linkType: hard + "pbf@npm:^3.2.1, pbf@npm:^3.3.0": version: 3.3.0 resolution: "pbf@npm:3.3.0" @@ -17098,6 +17627,13 @@ __metadata: languageName: node linkType: hard +"picomatch@npm:^4.0.3, picomatch@npm:^4.0.4": + version: 4.0.4 + resolution: "picomatch@npm:4.0.4" + checksum: 10/f6ef80a3590827ce20378ae110ac78209cc4f74d39236370f1780f957b7ee41c12acde0e4651b90f39983506fd2f5e449994716f516db2e9752924aff8de93ce + languageName: node + linkType: hard + "pify@npm:^2.2.0, pify@npm:^2.3.0": version: 2.3.0 resolution: "pify@npm:2.3.0" @@ -17476,6 +18012,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.5.15": + version: 8.5.15 + resolution: "postcss@npm:8.5.15" + dependencies: + nanoid: "npm:^3.3.12" + picocolors: "npm:^1.1.1" + source-map-js: "npm:^1.2.1" + checksum: 10/d02ad19eb1e0fa53a1229ee6d53807eb88f903f2b9a8cac66993367f3ac7dd3b97238c783a54ccbf4145f82f6ca9a5cbd58f089846285d759c8a3259fbea8318 + languageName: node + linkType: hard + "postcss@npm:^8.5.6": version: 8.5.8 resolution: "postcss@npm:8.5.8" @@ -17989,6 +18536,18 @@ __metadata: languageName: node linkType: hard +"react-dom@npm:18.3.1, scratchReactDomVendor@npm:react-dom@18.3.1": + version: 18.3.1 + resolution: "react-dom@npm:18.3.1" + dependencies: + loose-envify: "npm:^1.1.0" + scheduler: "npm:^0.23.2" + peerDependencies: + react: ^18.3.1 + checksum: 10/3f4b73a3aa083091173b29812b10394dd06f4ac06aff410b74702cfb3aa29d7b0ced208aab92d5272919b612e5cda21aeb1d54191848cf6e46e9e354f3541f81 + languageName: node + linkType: hard + "react-dom@npm:^19.2.7": version: 19.2.7 resolution: "react-dom@npm:19.2.7" @@ -18386,6 +18945,15 @@ __metadata: languageName: node linkType: hard +"react@npm:18.3.1, scratchReactVendor@npm:react@18.3.1": + version: 18.3.1 + resolution: "react@npm:18.3.1" + dependencies: + loose-envify: "npm:^1.1.0" + checksum: 10/261137d3f3993eaa2368a83110466fc0e558bc2c7f7ae7ca52d94f03aac945f45146bd85e5f481044db1758a1dbb57879e2fcdd33924e2dde1bdc550ce73f7bf + languageName: node + linkType: hard + "react@npm:^19.2.7": version: 19.2.7 resolution: "react@npm:19.2.7" @@ -19113,6 +19681,64 @@ __metadata: languageName: node linkType: hard +"rolldown@npm:~1.1.2": + version: 1.1.3 + resolution: "rolldown@npm:1.1.3" + dependencies: + "@oxc-project/types": "npm:=0.137.0" + "@rolldown/binding-android-arm64": "npm:1.1.3" + "@rolldown/binding-darwin-arm64": "npm:1.1.3" + "@rolldown/binding-darwin-x64": "npm:1.1.3" + "@rolldown/binding-freebsd-x64": "npm:1.1.3" + "@rolldown/binding-linux-arm-gnueabihf": "npm:1.1.3" + "@rolldown/binding-linux-arm64-gnu": "npm:1.1.3" + "@rolldown/binding-linux-arm64-musl": "npm:1.1.3" + "@rolldown/binding-linux-ppc64-gnu": "npm:1.1.3" + "@rolldown/binding-linux-s390x-gnu": "npm:1.1.3" + "@rolldown/binding-linux-x64-gnu": "npm:1.1.3" + "@rolldown/binding-linux-x64-musl": "npm:1.1.3" + "@rolldown/binding-openharmony-arm64": "npm:1.1.3" + "@rolldown/binding-wasm32-wasi": "npm:1.1.3" + "@rolldown/binding-win32-arm64-msvc": "npm:1.1.3" + "@rolldown/binding-win32-x64-msvc": "npm:1.1.3" + "@rolldown/pluginutils": "npm:^1.0.0" + dependenciesMeta: + "@rolldown/binding-android-arm64": + optional: true + "@rolldown/binding-darwin-arm64": + optional: true + "@rolldown/binding-darwin-x64": + optional: true + "@rolldown/binding-freebsd-x64": + optional: true + "@rolldown/binding-linux-arm-gnueabihf": + optional: true + "@rolldown/binding-linux-arm64-gnu": + optional: true + "@rolldown/binding-linux-arm64-musl": + optional: true + "@rolldown/binding-linux-ppc64-gnu": + optional: true + "@rolldown/binding-linux-s390x-gnu": + optional: true + "@rolldown/binding-linux-x64-gnu": + optional: true + "@rolldown/binding-linux-x64-musl": + optional: true + "@rolldown/binding-openharmony-arm64": + optional: true + "@rolldown/binding-wasm32-wasi": + optional: true + "@rolldown/binding-win32-arm64-msvc": + optional: true + "@rolldown/binding-win32-x64-msvc": + optional: true + bin: + rolldown: ./bin/cli.mjs + checksum: 10/9095a02363721dfd925dd8069822b6a726d85020430b4a310622fb551ca3e8d492cf1a612beabfc582fb10f3afe5f639f72fe1f1d067f8e4d192edb75091a91f + languageName: node + linkType: hard + "rrweb-cssom@npm:^0.8.0": version: 0.8.0 resolution: "rrweb-cssom@npm:0.8.0" @@ -19462,27 +20088,6 @@ __metadata: languageName: node linkType: hard -"scratchReactDomVendor@npm:react-dom@18.3.1": - version: 18.3.1 - resolution: "react-dom@npm:18.3.1" - dependencies: - loose-envify: "npm:^1.1.0" - scheduler: "npm:^0.23.2" - peerDependencies: - react: ^18.3.1 - checksum: 10/3f4b73a3aa083091173b29812b10394dd06f4ac06aff410b74702cfb3aa29d7b0ced208aab92d5272919b612e5cda21aeb1d54191848cf6e46e9e354f3541f81 - languageName: node - linkType: hard - -"scratchReactVendor@npm:react@18.3.1": - version: 18.3.1 - resolution: "react@npm:18.3.1" - dependencies: - loose-envify: "npm:^1.1.0" - checksum: 10/261137d3f3993eaa2368a83110466fc0e558bc2c7f7ae7ca52d94f03aac945f45146bd85e5f481044db1758a1dbb57879e2fcdd33924e2dde1bdc550ce73f7bf - languageName: node - linkType: hard - "scratchblocks@npm:^3.7.0": version: 3.7.0 resolution: "scratchblocks@npm:3.7.0" @@ -19853,6 +20458,13 @@ __metadata: languageName: node linkType: hard +"siginfo@npm:^2.0.0": + version: 2.0.0 + resolution: "siginfo@npm:2.0.0" + checksum: 10/e93ff66c6531a079af8fb217240df01f980155b5dc408d2d7bebc398dd284e383eb318153bf8acd4db3c4fe799aa5b9a641e38b0ba3b1975700b1c89547ea4e7 + languageName: node + linkType: hard + "signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" @@ -20226,6 +20838,13 @@ __metadata: languageName: node linkType: hard +"stackback@npm:0.0.2": + version: 0.0.2 + resolution: "stackback@npm:0.0.2" + checksum: 10/2d4dc4e64e2db796de4a3c856d5943daccdfa3dd092e452a1ce059c81e9a9c29e0b9badba91b43ef0d5ff5c04ee62feb3bcc559a804e16faf447bac2d883aa99 + languageName: node + linkType: hard + "startaudiocontext@npm:1.2.1, startaudiocontext@npm:^1.2.1": version: 1.2.1 resolution: "startaudiocontext@npm:1.2.1" @@ -20308,6 +20927,13 @@ __metadata: languageName: node linkType: hard +"std-env@npm:^4.0.0-rc.1": + version: 4.1.0 + resolution: "std-env@npm:4.1.0" + checksum: 10/008146cdb834010383138d356e0dd3e3b0ac127a8229f711b8c518bb22940813cc0dcd654fc76b17f0b18179f56089f8b8e52bd6a7ffa0041a966581e7a44dbe + languageName: node + linkType: hard + "stop-iteration-iterator@npm:^1.0.0": version: 1.0.0 resolution: "stop-iteration-iterator@npm:1.0.0" @@ -21150,6 +21776,13 @@ __metadata: languageName: node linkType: hard +"tinybench@npm:^2.9.0": + version: 2.9.0 + resolution: "tinybench@npm:2.9.0" + checksum: 10/cfa1e1418e91289219501703c4693c70708c91ffb7f040fd318d24aef419fb5a43e0c0160df9471499191968b2451d8da7f8087b08c3133c251c40d24aced06c + languageName: node + linkType: hard + "tinycolor2@npm:^1.4.2": version: 1.6.0 resolution: "tinycolor2@npm:1.6.0" @@ -21157,6 +21790,23 @@ __metadata: languageName: node linkType: hard +"tinyexec@npm:^1.0.2": + version: 1.2.4 + resolution: "tinyexec@npm:1.2.4" + checksum: 10/f20b3e6f56f24c3ebe0129d0b6e657e561d225df2cf93c1a10362996232dd6ad4b8af8c9e81d258a64d09020e723772baf6fe0be26512dba7c61bb366d67b1f9 + languageName: node + linkType: hard + +"tinyglobby@npm:^0.2.15, tinyglobby@npm:^0.2.17": + version: 0.2.17 + resolution: "tinyglobby@npm:0.2.17" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.4" + checksum: 10/f85e8a217d675c3f78d5f0ad25ea4557e7e023ed13ddc2b014da10bd0312eea53a34cd52356af07ccdff777f1243012547656282a4ca70936f68bf5065fbaa71 + languageName: node + linkType: hard + "tinyqueue@npm:^2.0.3": version: 2.0.3 resolution: "tinyqueue@npm:2.0.3" @@ -21171,6 +21821,13 @@ __metadata: languageName: node linkType: hard +"tinyrainbow@npm:^3.1.0": + version: 3.1.0 + resolution: "tinyrainbow@npm:3.1.0" + checksum: 10/4c2c01dde1e5bb9a74973daaae141d4d733d246280b2f9a7f6a9e7dd8e940d48b2580a6086125278777897bc44635d6ccec5f9f563c2179dd2129f4542d0ec05 + languageName: node + linkType: hard + "tldts-core@npm:^6.1.86": version: 6.1.86 resolution: "tldts-core@npm:6.1.86" @@ -21484,7 +22141,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2, tslib@npm:^2.8.1": +"tslib@npm:2, tslib@npm:^2.4.0, tslib@npm:^2.8.1": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 @@ -22189,6 +22846,145 @@ __metadata: languageName: node linkType: hard +"vite-plugin-static-copy@npm:^4.1.1": + version: 4.1.1 + resolution: "vite-plugin-static-copy@npm:4.1.1" + dependencies: + chokidar: "npm:^3.6.0" + p-map: "npm:^7.0.4" + picocolors: "npm:^1.1.1" + tinyglobby: "npm:^0.2.17" + peerDependencies: + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 10/49c227f1d5b68a70feb0ab59d888cf9780b0aaa639255f4e630818f11a6f9a5abfcfded8b4b77445fd6f68f13468b0b458e927bc551a02f0e6995cf8e0803fc4 + languageName: node + linkType: hard + +"vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0, vite@npm:^8.0.16": + version: 8.1.0 + resolution: "vite@npm:8.1.0" + dependencies: + fsevents: "npm:~2.3.3" + lightningcss: "npm:^1.32.0" + picomatch: "npm:^4.0.4" + postcss: "npm:^8.5.15" + rolldown: "npm:~1.1.2" + tinyglobby: "npm:^0.2.17" + peerDependencies: + "@types/node": ^20.19.0 || >=22.12.0 + "@vitejs/devtools": ^0.3.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: ">=1.21.0" + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: ">=0.54.8" + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + "@vitejs/devtools": + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + bin: + vite: bin/vite.js + checksum: 10/da5b7dfb4045c2936e0afe03afa0bcbb417526d804b80412a2efc23896dccb457f92aaa7d1e44a9c3ff45cea39139e2c45fc6d1be979dca0ef2c1eab38b7cebe + languageName: node + linkType: hard + +"vitest@npm:^4.1.9": + version: 4.1.9 + resolution: "vitest@npm:4.1.9" + dependencies: + "@vitest/expect": "npm:4.1.9" + "@vitest/mocker": "npm:4.1.9" + "@vitest/pretty-format": "npm:4.1.9" + "@vitest/runner": "npm:4.1.9" + "@vitest/snapshot": "npm:4.1.9" + "@vitest/spy": "npm:4.1.9" + "@vitest/utils": "npm:4.1.9" + es-module-lexer: "npm:^2.0.0" + expect-type: "npm:^1.3.0" + magic-string: "npm:^0.30.21" + obug: "npm:^2.1.1" + pathe: "npm:^2.0.3" + picomatch: "npm:^4.0.3" + std-env: "npm:^4.0.0-rc.1" + tinybench: "npm:^2.9.0" + tinyexec: "npm:^1.0.2" + tinyglobby: "npm:^0.2.15" + tinyrainbow: "npm:^3.1.0" + vite: "npm:^6.0.0 || ^7.0.0 || ^8.0.0" + why-is-node-running: "npm:^2.3.0" + peerDependencies: + "@edge-runtime/vm": "*" + "@opentelemetry/api": ^1.9.0 + "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 + "@vitest/browser-playwright": 4.1.9 + "@vitest/browser-preview": 4.1.9 + "@vitest/browser-webdriverio": 4.1.9 + "@vitest/coverage-istanbul": 4.1.9 + "@vitest/coverage-v8": 4.1.9 + "@vitest/ui": 4.1.9 + happy-dom: "*" + jsdom: "*" + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@opentelemetry/api": + optional: true + "@types/node": + optional: true + "@vitest/browser-playwright": + optional: true + "@vitest/browser-preview": + optional: true + "@vitest/browser-webdriverio": + optional: true + "@vitest/coverage-istanbul": + optional: true + "@vitest/coverage-v8": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vite: + optional: false + bin: + vitest: ./vitest.mjs + checksum: 10/64f9d1a0aae92c493c39822ecae8ec5b5a336fc27166f776d08c01ae79ef1ec5485a1826ef1451bb05df2edaae109894125c2ecceadaa56c17be2690f66f9758 + languageName: node + linkType: hard + "vlq@npm:^0.2.2": version: 0.2.3 resolution: "vlq@npm:0.2.3" @@ -22728,6 +23524,18 @@ __metadata: languageName: node linkType: hard +"why-is-node-running@npm:^2.3.0": + version: 2.3.0 + resolution: "why-is-node-running@npm:2.3.0" + dependencies: + siginfo: "npm:^2.0.0" + stackback: "npm:0.0.2" + bin: + why-is-node-running: cli.js + checksum: 10/0de6e6cd8f2f94a8b5ca44e84cf1751eadcac3ebedcdc6e5fbbe6c8011904afcbc1a2777c53496ec02ced7b81f2e7eda61e76bf8262a8bc3ceaa1f6040508051 + languageName: node + linkType: hard + "wildcard@npm:^2.0.0": version: 2.0.1 resolution: "wildcard@npm:2.0.1"