diff --git a/.github/actions/setup-specialized/action.yml b/.github/actions/setup-specialized/action.yml new file mode 100644 index 0000000..2a28357 --- /dev/null +++ b/.github/actions/setup-specialized/action.yml @@ -0,0 +1,37 @@ +name: Set up specialized build sources +description: Install a pinned sibling VM and the packager from their lockfiles +inputs: + vm-ref: + description: Full commit SHA of the matching modified scratch-vm + required: true +runs: + using: composite + steps: + - name: Require a pinned VM revision + shell: bash + env: + SPECIALIZED_VM_REF: ${{ inputs.vm-ref }} + run: | + if [[ ! "$SPECIALIZED_VM_REF" =~ ^[0-9a-f]{40}$ ]]; then + echo 'Set the SPECIALIZED_VM_REF repository variable to the matching scratch-vm commit SHA.' >&2 + exit 1 + fi + - uses: actions/checkout@v6 + with: + repository: ScratchCN/Scratch-vm + ref: ${{ inputs.vm-ref }} + path: scratch-vm + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: 24 + cache: npm + cache-dependency-path: | + scratch-vm/package-lock.json + tw-packager/package-lock.json + - name: Install locked sources + shell: bash + working-directory: tw-packager + run: | + npm ci --prefix ../scratch-vm + npm ci diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0679219..92294d5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,32 +2,43 @@ name: Build on: push: - branches: [ master ] + branches: [ main ] pull_request: - branches: [ master ] + branches: [ main ] jobs: build: runs-on: ubuntu-latest + timeout-minutes: 45 + defaults: + run: + working-directory: tw-packager steps: - uses: actions/checkout@v6 with: persist-credentials: false - - name: Install Node.js - uses: actions/setup-node@v6 + path: tw-packager + - uses: ./tw-packager/.github/actions/setup-specialized with: - node-version: 24 - cache: npm - - name: Install dependencies - run: npm ci + vm-ref: ${{ vars.SPECIALIZED_VM_REF }} - name: Build - run: npm run build + run: npm run build-node-prod - name: Test - run: npm test + run: | + npm test -- --runInBand + npm run test:specialized + node test/specialized-differential.mjs --linked + npm run test:specialized-build + node test/specialized-worker-input.mjs + npx playwright install --with-deps chromium firefox webkit + npm run test:specialized-browser + npm run test:specialized-performance - name: Build standalone run: npm run build-standalone-prod + - name: Test offline standalone + run: node test/specialized-standalone.mjs - name: Upload standalone artifact uses: actions/upload-artifact@v6 with: name: standalone - path: dist/standalone.html + path: tw-packager/dist/standalone.html diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2b311be..ed3d9e2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,7 +3,7 @@ name: Deploy to GitHub Pages on: workflow_dispatch: push: - branches: [master] + branches: [main] concurrency: group: "deploy" @@ -17,21 +17,30 @@ permissions: jobs: build: runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 45 + defaults: + run: + working-directory: tw-packager steps: - name: Checkout uses: actions/checkout@v6 with: persist-credentials: false - - name: Install Node.js - uses: actions/setup-node@v6 + path: tw-packager + - uses: ./tw-packager/.github/actions/setup-specialized with: - node-version: 24 - cache: npm - - name: Install dependencies - run: npm ci + vm-ref: ${{ vars.SPECIALIZED_VM_REF }} - name: Test - run: npm test + run: | + npm test -- --runInBand + npm run build-node-prod + npm run test:specialized + node test/specialized-differential.mjs --linked + npm run test:specialized-build + node test/specialized-worker-input.mjs + npx playwright install --with-deps chromium firefox webkit + npm run test:specialized-browser + npm run test:specialized-performance - name: Build website run: | mkdir -p web @@ -42,13 +51,14 @@ jobs: - name: Build standalone run: | npm run build-standalone-prod + node test/specialized-standalone.mjs cp dist/standalone.html web - name: Configure GitHub Pages uses: actions/configure-pages@v5 - name: Upload artifact uses: actions/upload-pages-artifact@v4 with: - path: ./web/ + path: ./tw-packager/web/ deploy: environment: diff --git a/.github/workflows/publish-to-npm.yml b/.github/workflows/publish-to-npm.yml index 550f167..b9954d0 100644 --- a/.github/workflows/publish-to-npm.yml +++ b/.github/workflows/publish-to-npm.yml @@ -12,11 +12,15 @@ permissions: jobs: publish: runs-on: ubuntu-latest + defaults: + run: + working-directory: tw-packager steps: - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 with: - node-version: 24 - - run: npm ci - - run: npm test + path: tw-packager + persist-credentials: false + - uses: ./tw-packager/.github/actions/setup-specialized + with: + vm-ref: ${{ vars.SPECIALIZED_VM_REF }} - run: npm publish diff --git a/.gitignore b/.gitignore index 6b1044b..0ea0e85 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules dist +generated/ *.zip # Related to Node.js API and related demo scripts diff --git a/.npmignore b/.npmignore index e6004bf..64b6488 100644 --- a/.npmignore +++ b/.npmignore @@ -1,4 +1,6 @@ src +scripts +generated static test webpack.config.js diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..9dee295 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@sccn:registry=https://git.fmcraft.us.kg/api/packages/ScCN/npm/ diff --git a/README.md b/README.md index cdd63e4..fe04f29 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SB3C Packager -An independent modified version of [TurboWarp Packager](https://github.com/TurboWarp/packager), focused on ahead-of-time compiled `.sb3c` projects, VM-access protection, reproducible JavaScript obfuscation, and package integrity verification. +An independent modified version of [TurboWarp Packager](https://github.com/TurboWarp/packager), focused on offline, program-specific players that jointly compile Scratch scripts and the runtime. See [the build architecture](docs/specialized-builds.md). Source: https://github.com/LuYifei2011/tw-packager @@ -12,9 +12,10 @@ Compilation and obfuscation are not encryption or DRM. They can make casual insp ## Development -Install dependencies: +Keep the matching modified `scratch-vm` checkout beside this repository. Install dependencies in both checkouts (Node 22.15 or newer): ``` +npm ci --prefix ../scratch-vm npm ci ``` @@ -50,12 +51,7 @@ We strive to make the packager easy to fork, even for mods that aren't based on ### Packages -If you want to change the scratch-vm/scratch-render/scratch-audio/scratch-storage/etc. used, this is simple: - - - `npm install` or `npm link` your package. The package name does not matter. - - Update src/scaffolding/scratch-libraries.js to import the packages with the name you have. (some of our packages are prefixed with `@turbowarp/` while others are still just `scratch-vm` -- just make sure they match yours) - -Then just rebuild. You can even install a vanilla scratch-vm and all core functionality will still work (but optional features such as interpolation, high quality pen, stage size, etc. may not work) +The compiler frontend, source preparation and player must use the same sibling `scratch-vm` checkout. A generic upstream VM does not provide the specialized IR API. Dependency changes require regenerating the source pack, reviewing initialization/property boundaries and running the differential, artifact and browser checks described in [the build documentation](docs/specialized-builds.md). The lockfile and audited dependency source hashes form part of the build baseline. Note that npm is a very buggy piece of software and our dependency tree is very large. Occasionally you might get errors about missing dependencies, which should go away if you run `npm install`. @@ -63,14 +59,14 @@ Note that npm is a very buggy piece of software and our dependency tree is very The packager is deployed as a simple static website. You can host it anywhere by just copying the `dist` folder after a build. -This fork includes `.github/workflows/deploy.yml` for GitHub Pages. In the repository's **Settings → Pages**, set **Source** to **GitHub Actions**. A push to `master`, or a manual run of “Deploy to GitHub Pages”, will test the project, build the production website and standalone version, upload the Pages artifact, and deploy it to `https://luyifei2011.github.io/tw-packager/`. +This fork includes `.github/workflows/deploy.yml` for GitHub Pages. In the repository's **Settings → Pages**, set **Source** to **GitHub Actions**. A push to `main`, or a manual run of “Deploy to GitHub Pages”, will test the project, build the production website and standalone version, upload the Pages artifact, and deploy it to `https://luyifei2011.github.io/tw-packager/`. We use GitHub Actions and GitHub Pages to manage our deployment. If you want to do this too: - Fork the repository on GitHub and push your changes. - Go to your fork's settings on GitHub and enable GitHub Pages with the source set to GitHub Actions. - Go to the "Actions" tab and enable GitHub Actions if it isn't already enabled. - - Push commits to the "master" branch. + - Push commits to the "main" branch. - In a few minutes, your site will automatically be built and deployed to GitHub Pages. ### Branding @@ -87,7 +83,7 @@ Set the environment variable `ENABLE_SERVICE_WORKER` to `1` to enable service wo ## Standalone builds -The packager supports generating "standalone builds" that are single HTML files containing the entire packager. Large files such as Electron binaries will still be downloaded from a remote server as needed. You can download prebuilt standalone builds from [our GitHub releases](https://github.com/TurboWarp/packager/releases). These can be useful if our website is blocked or you don't have a reliable internet connection. Note that standalone builds do not contain an update checker, so do check on your own occasionally. +The packager supports generating "standalone builds" that are single HTML files containing the entire packager, including its compiler, resolved runtime sources and linker. HTML builds work offline; desktop wrapper binaries and extensions not supplied locally are fetched during construction. Standalone builds do not contain an update checker. To make a production standalone build locally: diff --git a/docs/specialized-builds.md b/docs/specialized-builds.md new file mode 100644 index 0000000..50ee386 --- /dev/null +++ b/docs/specialized-builds.md @@ -0,0 +1,100 @@ +# Program-specific player builds + +`compileProject` now returns `{code, assets, buildId, diagnostics}`. There is no compiled package loader in the production player. HTML, ZIP and desktop wrappers consume the same linked VM/program source; resources contain initial state and assets, not executable scripts. The ordinary VM compiler remains available for development and differential tests. + +## Build implementation + +1. `scratch-vm.exportSpecializedProjectBuildInput()` exports optimized structured IR, event descriptors, initial project state and extension requirements. Export also extracts gamepad hints before deleting source blocks and comments. +2. `scratch-vm/src/compiler/continuation.js` lowers the IR directly. It reuses the existing typed expression and atomic operation emitters. Calls, returns and waits save per-thread continuations; compatible warp loops stay structured, including bounded inlining of synchronous, acyclic warp calls. Terminal void warp calls reuse frames; arguments that can suspend still spill in evaluation order. Straight-line yielding loops use one resume region. It never parses old factory strings. +3. `scripts/prepare-runtime-sources.mjs` prepares resolved ESM modules from the locked sibling VM and dependencies. First-party CommonJS is converted before linking. Runtime, Thread and Sequencer have production definitions with the editor/compiler entry paths removed. The generated source pack is a build-tool asset, never a player asset. +4. `src/specialized/link.mjs` replaces Sequencer's step method with shared dispatch methods and emits static hat/primitive entries. Each group contains at most 256 regions with a 12,000-character input-code budget; an indivisible native loop may exceed that budget. Groups are actual separate VM methods. Encoded group/local PCs let both dispatch levels use jump tables. Iteration crosses groups without introducing a yield. Slot permutations and declared internal property names use the same build seed. Pure synchronous regions cache proven VM variable references/values and commit before runtime exits and on exceptions; calls that can observe VM state prevent this optimization. +5. Rollup links the player from memory, then joint property transformation and Terser compression run on the combined result. Both browser Worker and Node use this implementation. The standalone builder embeds the source pack, Rollup and parser WASM; it also works from a local HTML file without network access. + +The implementation renames the explicitly owned continuation, lifecycle, worker and target-identity fields in both VM and generated code. The scheduling manifest also covers three Sequencer methods (`stepThread`, `stepThreads`, `retireThread`) and eleven Thread stack/identity methods. Build-only annotations bind the actual classes and monitor-execution parameters across Rollup; they are removed before the player is emitted. The audit follows immutable aliases, destructuring and optional member accesses. Literal reflection, dynamic access through known scheduling receivers, reassigned aliases, unrelated declarations and unproven receivers reserve the affected names. `diagnostics.schedulingProperties` reports each scheduling method's mapping or reservation; `diagnostics.reservedPropertyReasons` explains reservations. Missing owner methods or changed parameter/static/async/generator signatures fail the build until the manifest is reviewed. + +This is a bounded scheduling audit, not whole-program ownership inference. Ambiguous legacy fields such as `status`, `target`, `runtime` and `value`, host APIs and extension RPC fields keep stable names. The boundary is recorded in `diagnostics.propertyBoundary`; future expansion requires additional ownership/escape analysis. With `sourceMap: true`, `compileProject` returns a separate `sourceMap` object. Property/slot maps and analysis are separate return data. None are embedded in the player; enabling maps does not change its code. + +Capabilities are selected from emitted operations, assets, hats, monitors and extensions. Unused sound, video and pen modules are removed. Builtin block classes retain only referenced methods, transitive helpers and initialization. Builtin extensions with static registration metadata are pruned by required opcodes and direct IR calls. Dynamic registration metadata conservatively retains its extension as a unit. `diagnostics` lists retained and removed modules/members, capability reasons, shared dispatch groups, fixed-extension dependencies and surviving third-party CommonJS wrappers. + +The wrapper allowlist contains only JSZip's `utils.js`/`base64.js` cycle, whose `require` bindings capture partially initialized mutable export objects. Their source hashes, dependency cycle and initialization reason are recorded in the build report; source changes require a fresh review. Player-only ZIP preparation excludes Node stream adapters and obsolete Promise polyfills. Text utility module closures are converted to ESM. TWGL 4.4.0's pinned 15-module acyclic webpack bundle is unpacked into ESM at build-tool preparation time, removing its factory table and loader. Both transformations check their source shape, and the TWGL transform checks its exact source hash. CommonJS uses the explicit allowlist, rather than automatic conditional-require wrapping. Any surviving unaudited wrapper fails the build. + +## Playback API + +The only global integration object is frozen `window.player`: + +```js +player.on('loaded', () => player.start()); +player.on('error', ({message}) => console.error(message)); +player.postInput('keyboard', {key: 'a', isDown: true}); +player.pause(); +player.resume(); +player.stop(); +player.dispose(); +``` + +The complete method set is `start`, `stop`, `pause`, `resume`, `postInput`, `on`, `off`, `dispose`. Event types are `loaded`, `state`, `problem`, `error`. Device inputs accept scalar keyboard, mouse, mouse-wheel and user-data fields. Event payloads do not expose VM/Thread references. Disposal terminates extension Workers, closes audio/video/cloud resources and removes player/addon input listeners. + +Promise continuations capture the thread's lifetime object. Stopping, retiring or restarting a thread invalidates it. Promise completion during pause does not bypass the pause scheduler. The implementation never falls back to the old compiled-package protection chain. + +## Extensions and compatibility + +Builtin extensions are selected from project requirements. Third-party code is frozen from build-time URLs or supplied `{url, source, dependencies}` entries. Dependency keys are absolute URLs. Static ESM imports and literal `importScripts` calls are recursively resolved during the build. No extension code is resolved from a CDN by the player. Data-fetching blocks and normal cloud variables can still use the network as part of project behavior. + +Third-party extensions register synchronously in an isolated Worker; their block operations may return Promises and branch indices. Metadata is collected separately during compilation and checked again during playback. Node metadata collection uses a separate permission-restricted process and a context with dynamic code generation disabled. Runtime extension Workers receive no VM or compiler objects. Dynamic registration/loading, compiler mutation, unsandboxed VM access and runtime code evaluation are rejected. + +Old `.sb3c`/`.sb3c2` packages require the original SB3 for rebuilding. Old `compiler.aot`, `compiledProjectHooks`, signature/network-protection settings, `bakeExtensions`, custom JavaScript and unsafe cloud evaluation no longer have implementation paths. Loading legacy settings reports migration and resets incompatible fields. Custom CSS and normal cloud providers remain supported. + +## Validation + +```sh +npm ci +npm run build-node +npm test -- --runInBand +npm run test:specialized +node --test test/specialized-structure.mjs +node --test test/specialized-properties.mjs +node test/specialized-build.mjs +node test/specialized-worker-input.mjs +node test/specialized-browser.mjs +node test/specialized-performance.mjs +npm run build-standalone +node test/specialized-standalone.mjs +``` + +Playwright's Chromium, Firefox and WebKit binaries must be installed for browser tests. Build/performance artifacts go to the sibling `work/acceptance` directory and are not published. Use Node 22.15 or newer. The lockfile pins the Node and browser Rollup versions; the WASM bootstrap patch deliberately fails if that pinned entry changes. + +CI checks out both repositories as siblings. Set the `SPECIALIZED_VM_REF` repository variable to the full commit SHA containing the matching VM changes; workflows reject an absent or floating revision. No commit is invented or automatically pushed by local builds. `node test/specialized-differential.mjs --linked` runs the same fixture comparison against the complete linked/pruned/renamed production VM, in addition to the faster continuation-only harness. + +The execution harness covers all 112 existing VM fixtures and compares variables, lists and SAY event order with JS AOT. The linked variant also exercises serialization, pruning and joint transformation; it caught headless target-layer loss and inline legacy primitive registration, both corrected in the implementation. Separate lifecycle tests cover stale Promise completions after stop/restart and completion during pause. Artifact tests reject direct/aliased/computed dynamic evaluation, runtime imports and generators, and inspect source removal, capability removal, seed determinism and dispatch structure. Browser tests prohibit JavaScript dynamic evaluation and exercise playback, API isolation, async extensions, actual WebGL pen pixels and Node/Worker byte equality. + +The performance harness measures the actual jointly linked VM in the same JS realm as JS AOT, after three warmups and over five measured rounds. It records medians, nearest-rank p95, linked-player startup, build time and process heap. Its 10% median CPU regression gate fails the test on regression. These CPU fixtures cover arithmetic, warp procedure calls among cold scripts, lists, broadcasts, clones and pen state updates. **Passing these small fixtures does not establish real-project performance parity. GPU drawing, per-player peak memory and native desktop startup on every supported OS remain separate release gates.** An additional renderer trace test compares movement, effects and pen operations; an actual dispatcher substitution test rejects a dispatcher from another build and verifies that both original builds preserve behavior. Broader renderer scenarios remain part of project-level regression testing. + +The actual macOS WebView archive has been built and its executable launched. GUI capture was unavailable in the test host (ScreenCaptureKit error -3811), so native visual playback has not been claimed. Windows/Linux native launch remains to be checked on those platforms. The offline standalone builder was exercised from `file://` with networking disabled, including generating and playing its downloaded HTML. + +### Initial local acceptance recorded on 2026-09-03 (limited fixture scope) + +- Continuation differential: 112/112 existing fixtures passed. Final complete linked-player differential: 112/112 passed across three isolated process shards (38 + 37 + 37). `--shard=0/3` through `--shard=2/3` partition the sorted fixture list without sharing VM globals. +- Lifecycle, structure and renderer trace: 7/7 tests passed. Existing package tests: 54 passed, one pre-existing skip. Modified VM files passed ESLint; both repositories passed `git diff --check`. +- Chromium, Firefox and WebKit each passed bare player, embedded HTML, web ZIP, fixed asynchronous extension/branch, actual pen pixels and Node/Worker byte parity checks. Offline standalone UI construction and playback passed. +- Final five-round median CPU changes relative to JS AOT: arithmetic -9.4%, lists +1.6%, broadcasts -56.1%, clones -47.1%, pen state updates -13.3%. All met the +10% gate. Startup timing excludes writing the test artifact to disk. The scope exclusions above still apply. +- Production Node build and package dry-run passed; the package contains the prepared linker and excludes obsolete Scaffolding API documentation and duplicate intermediate source packs. CI configuration was syntax-checked locally; remote CI/deployment was not run. + +### Real-project regression: Scratch Benchmark 441947766 + +The reported computation score was 2060 versus 36233 in TurboWarp. The original tiny-loop gate missed this regression: it did not exercise a large program with frequently called warp procedures. The old backend split non-suspending calls into frames and put all switch groups in one oversized function. On the original workload procedures, the initial same-realm reproduction measured roughly 10x execution time for terrain generation and 9x for raycasting. + +The corrections above retain structured computation, remove unused inlined procedure regions, split actual dispatcher methods and reduce frame/variable lookup overhead. This is still **not a completed +10% real-project release gate**. In the final five-round Node 26.7.0 check with seed `scratch-benchmark`, median execution-time changes were PRNG -14.9%, terrain -49.6%, raycasting +32.7%, quicksort -1.5%, list search +4.8% and string parsing -8.7%. All workload states matched, but raycasting failed the gate. Node 24 and Node 26 differ, so results must include the engine and seed. Earlier fixture-only performance numbers must not be generalized to all projects. Browser automation policy blocked opening the preview during this investigation; updated full-browser scores have not been verified. + +After these corrections, the complete linked-player differential passed 112/112 fixtures, and lifecycle/structure/renderer checks passed 7/7 tests. The six smaller performance fixtures also passed, including the added million-iteration warp-call loop among 80 cold scripts. Prepared browser Worker/linker resources and the Node bundle were rebuilt. Existing exported HTML files must be rebuilt to receive the changes. + +The subsequent scheduling-renaming expansion passed the complete linked differential again (112/112), 18 lifecycle/structure/rendering/property tests, and 61 package tests (one existing skip). The property suite includes linked pause/resume/stop integration and rejection of changed owner signatures. Artifact checks confirmed all 14 scheduling names are mapped in the plain and fixed-extension fixtures, with no ownership markers in the player. Node and browser Worker/linker resources were rebuilt; this follow-up was validated through Node, not a new browser run. + +In a sequential before/after run on Node 26.7.0 with seed `scratch-benchmark`, three warmups and five measured rounds, the renamed player's CPU time relative to JS AOT was PRNG -14.9%, terrain -46.4%, raycasting +29.9%, quicksort +2.1%, list search +3.9% and string parsing -7.0%. All six workload states matched. Relative to the signed checkpoint, its own workload medians changed between -4.4% and +8.5%; this is a bounded regression check, not evidence of a stable speedup. In particular, raycasting's absolute median changed from 0.6254 ms to 0.6412 ms even though its ratio to AOT improved from +38.6% to +29.9%. The real-project gate remains failed; the six small performance fixtures passed. Scheduling renaming does not resolve the remaining dispatch overhead. + +Run the additional gate using the original SB3 downloaded from [Scratch Benchmark](https://scratch.mit.edu/projects/441947766/): + +```sh +npm run test:specialized-scratch-benchmark -- /path/to/original.sb3 ../work/acceptance/scratch-benchmark-report.json scratch-benchmark +``` + +The runner retains the full program in the final linked/minified code, invokes the original CPU workload procedures, compares resulting variables/lists, alternates AOT/specialized measurement order, and records source/code hashes, seed, engine, five sample sets and p95. It excludes the project's wall-clock score calculation, graphics, assets and cloud I/O. It exits unsuccessfully when any workload exceeds +10%; there is no fallback to the old protection chain. `BENCH_PROFILE=1` additionally writes an independent CPU profile, code and property map next to the report for development. diff --git a/node-api-docs/README.md b/node-api-docs/README.md index ccb86e0..27e9153 100644 --- a/node-api-docs/README.md +++ b/node-api-docs/README.md @@ -112,59 +112,38 @@ Some options expect an image as an argument. In the Node.js module, there is a s packager.options.app.icon = new Packager.Image('image/png', fs.readFileSync('icon.png')); ``` -### Ahead-of-time compilation and protection +### Program-specific compilation -The packager can compile a project into a `.sb3c` package before embedding it. VM member-access protection is enabled by default when this mode is selected; `javascript-obfuscator` can be added as a second stage. +Every export jointly links the optimized project and its specialized VM. Node 22.15 or newer is required. ```js -packager.options.compiler.aot.enabled = true; -packager.options.compiler.aot.obfuscate = true; -packager.options.compiler.aot.javascriptObfuscator = true; -packager.options.compiler.aot.shortIds = true; -packager.options.compiler.aot.seed = 'public-reproducible-seed'; +packager.options.compiler.seed = 'reproducible-build'; +packager.options.compiler.warpTimer = false; +``` -Object.assign(packager.options.compiler.aot.javascriptObfuscatorOptions, { - identifierNamesGenerator: 'mangled-shuffled', - stringArray: true, - stringArrayEncoding: 'rc4', - stringArrayThreshold: 0.75, - controlFlowFlattening: true, - controlFlowFlatteningThreshold: 0.5, - deadCodeInjection: false, - numbersToExpressions: true, - splitStrings: true, - splitStringsChunkLength: 8, - selfDefending: true, - debugProtection: true, - debugProtectionInterval: 3000 +The seed is optional; an empty seed selects fresh cryptographic random build material. A seed is not a secret or an encryption key. + +To obtain the linked JavaScript and resource archive without a page wrapper: + +```js +const {compileProject} = require('@sccn/tw-packager'); +const player = await compileProject(fs.readFileSync('project.sb3'), { + seed: 'reproducible-build', + autoplay: false }); +fs.writeFileSync('player.js', player.code); +for (const asset of player.assets) fs.writeFileSync(asset.name, asset.data); ``` -`renameGlobals`, `renameProperties`, and source maps are intentionally unavailable for compiled factories because they can break VM bindings or the artifact format. Self-defending mode applies to each independently evaluated compiled factory and must be the final factory-source transformation. Debug protection is different: the packager generates one global player sidecar, so entry scripts and procedures do not each install their own guard or interval. An interval of `0` runs the guard once; repeating values must be at least 1000 ms, and 2000–4000 ms is the practical range suggested by JavaScript Obfuscator. +Load `player.js` with a normal script tag alongside `project.zip`. `player.buildId` identifies the final JavaScript and resource archive; `player.diagnostics` contains capability reasons, retained/removed modules, internal property mappings and continuation groups. `diagnostics.schedulingProperties` lists the audited Thread/Sequencer methods and their final names; `diagnostics.reservedPropertyReasons` explains names retained for dynamic access, reflection or uncertain ownership. Keep diagnostics as development material, outside the player. -These transformations are deterrence, not encryption or a security boundary. They raise the cost of casual inspection and automated analysis, but executable JavaScript and the accessor still reach the user's machine. Never include private keys, long-term secrets, authorization logic, or other client-side trust decisions in a project. +Pass `sourceMap: true` to receive a separate `player.sourceMap` object for local debugging. It contains source information and must not be distributed with a protected player. The default result omits it, and neither mode inserts a source-map URL into the code. -For release builds, keep providers and secrets outside the serializable options object. `compiledProjectHooks` supports project-field seed selection, local or remote seed derivation, custom stages, `javascript-obfuscator` options, and integrity signing: +The browser exposes a frozen `window.player` with `start`, `stop`, `pause`, `resume`, `postInput(device, data)`, `on(event, callback)`, `off(event, callback)` and `dispose`. Events are `loaded`, `state`, `problem`, and `error`; event payloads contain no VM references. Wait for `loaded` before starting a player with autoplay disabled. -```js -packager.compiledProjectHooks = { - seedMaterial: ({project}) => ({ - extensions: project.extensions, - targetNames: project.targets.map(target => target.name) - }), - deriveSeed: async ({materialSha256}) => { - const {seed, keyId} = await seedService.derive({materialSha256}); - return {seed, keyId, derivationId: 'release-hmac-v1'}; - }, - javascriptObfuscatorOptions: {compact: true}, - signManifest, - // Source for the verifier embedded in the generated player. It must be a - // self-contained async function accepting {payload, manifest, signature}. - signatureVerifierSource: verifyManifest.toString() -}; -``` +Third-party extensions run in isolated Workers. Supply URLs in `options.extensions`, or `{url, source, dependencies}` objects to `compileProject`; `dependencies` maps absolute code URLs to source strings. Static imports and literal `importScripts` dependencies are frozen during the build. Registration must finish synchronously; block operations may be asynchronous. Extensions requiring full VM access, compiler mutation, dynamic code imports or new runtime extensions fail compilation. -The generated package includes a SHA-256 integrity manifest. When VM member-access protection is enabled, the matching accessor sidecar is embedded separately in the player and injected through the compiled artifact's declared binding. The artifact and accessor must always be published together; the packager handles this automatically. +Old `.sb3c`/`.sb3c2` packages, `compiler.aot`, `compiledProjectHooks`, signing/network protection, `bakeExtensions`, custom JavaScript, and unsafe cloud evaluation have been removed. Rebuild from the original SB3 and reset legacy settings. Ordinary cloud variables remain available through the packager's cloud options. Note that a Packager is a single-use object; you must make a new Packager each time you want to package a project. diff --git a/package-lock.json b/package-lock.json index 707abbe..9bba585 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@luyifei2011/tw-packager", + "name": "@sccn/tw-packager", "version": "3.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@luyifei2011/tw-packager", + "name": "@sccn/tw-packager", "version": "3.13.0", "license": "MPL-2.0", "dependencies": { @@ -13,12 +13,24 @@ "@turbowarp/json": "^0.1.1", "@turbowarp/jszip": "^3.11.0", "@turbowarp/sbdl": "^7.0.0", + "@xmldom/xmldom": "^0.9.12", "cross-fetch": "^4.1.0", + "pngjs": "^6.0.0", + "rollup": "4.63.1", "sha.js": "^2.4.11" }, "devDependencies": { "@babel/core": "^7.16.5", + "@babel/generator": "^7.28.0", + "@babel/parser": "^7.28.0", "@babel/preset-env": "^7.16.5", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "@playwright/test": "^1.62.1", + "@rollup/browser": "4.63.1", + "@rollup/plugin-commonjs": "^28.0.6", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.1", "@turbowarp/scratch-storage": "^2.0.0", "@turbowarp/scratch-svg-renderer": "^1.1.0", "babel-jest": "^27.4.5", @@ -29,22 +41,88 @@ "fastestsmallesttextencoderdecoder": "^1.0.22", "file-loader": "^6.2.0", "html-webpack-plugin": "^4.5.2", - "javascript-obfuscator": "^5.5.0", "jest": "^27.4.5", "rimraf": "^2.7.1", - "sb3c-vm-obfuscator": "git+https://github.com/LuYifei2011/sb3c-vm-obfuscator.git#main", "scratch-audio": "git+https://github.com/TurboWarp/scratch-audio.git#develop", "scratch-render": "git+https://github.com/TurboWarp/scratch-render.git#develop", - "scratch-vm": "git+https://github.com/LuYifei2011/scratch-vm.git#compiled-project-artifacts", + "scratch-vm": "file:../scratch-vm", "style-loader": "^2.0.0", "svelte": "^3.44.3", "svelte-loader": "^3.1.2", + "terser": "^5.43.1", "url-loader": "^4.1.1", "webpack": "4.47.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.9.1", "webpack-dev-server": "^3.11.3", "worker-loader": "^2.0.0" + }, + "engines": { + "node": ">=22.15.0" + } + }, + "../scratch-vm": { + "name": "@sccn/scratch-vm", + "version": "1.0.0", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@turbowarp/json": "^0.1.2", + "@turbowarp/jszip": "^3.11.0", + "@turbowarp/nanolog": "^0.2.0", + "@vernier/godirect": "1.5.0", + "arraybuffer-loader": "^1.0.6", + "atob": "2.1.2", + "btoa": "1.2.1", + "canvas-toBlob": "1.0.0", + "decode-html": "2.0.0", + "diff-match-patch": "1.0.4", + "format-message": "6.2.1", + "htmlparser2": "3.10.0", + "scratch-parser": "github:TurboWarp/scratch-parser#master", + "scratch-sb1-converter": "0.2.7", + "scratch-translate-extension-languages": "^1.0.7", + "text-encoding": "0.7.0", + "uuid": "8.3.2", + "worker-loader": "^1.1.1" + }, + "devDependencies": { + "@babel/core": "7.13.10", + "@babel/preset-env": "7.14.8", + "adm-zip": "0.4.11", + "babel-eslint": "10.1.0", + "babel-loader": "8.2.2", + "callsite": "1.0.0", + "copy-webpack-plugin": "4.5.4", + "docdash": "1.2.0", + "eslint": "8.55.0", + "eslint-config-scratch": "9.0.3", + "expose-loader": "0.7.5", + "file-loader": "2.0.0", + "format-message-cli": "6.2.0", + "in-publish": "2.0.1", + "js-md5": "0.7.3", + "jsdoc": "3.6.6", + "json": "^9.0.4", + "lodash.defaultsdeep": "4.6.1", + "pngjs": "3.3.3", + "scratch-audio": "0.1.0-prerelease.20231221012053", + "scratch-blocks": "0.1.0-prerelease.20230527085947", + "scratch-l10n": "3.16.20231222031921", + "scratch-render": "0.1.0-prerelease.20231220210403", + "scratch-render-fonts": "github:TurboWarp/scratch-render-fonts#master", + "scratch-storage": "2.3.1", + "script-loader": "0.7.2", + "stats.js": "0.17.0", + "tap": "16.2.0", + "tiny-worker": "2.3.0", + "uglifyjs-webpack-plugin": "1.2.7", + "webpack": "4.47.0", + "webpack-cli": "3.1.0", + "webpack-dev-server": "3.11.2" + }, + "peerDependencies": { + "@turbowarp/scratch-svg-renderer": "^1.1.0" } }, "node_modules/@ampproject/remapping": { @@ -1850,68 +1928,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@inversifyjs/common": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@inversifyjs/common/-/common-1.5.2.tgz", - "integrity": "sha512-WlzR9xGadABS9gtgZQ+luoZ8V6qm4Ii6RQfcfC9Ho2SOlE6ZuemFo7PKJvKI0ikm8cmKbU8hw5UK6E4qovH21w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@inversifyjs/container": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@inversifyjs/container/-/container-1.15.0.tgz", - "integrity": "sha512-U2xYsPrJTz5za2TExi5lg8qOWf8TEVBpN+pQM7B8BVA2rajtbRE9A66SLRHk8c1eGXmg+0K4Hdki6tWAsSQBUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inversifyjs/common": "1.5.2", - "@inversifyjs/core": "9.2.0", - "@inversifyjs/plugin": "0.2.0", - "@inversifyjs/reflect-metadata-utils": "1.4.1" - }, - "peerDependencies": { - "reflect-metadata": "~0.2.2" - } - }, - "node_modules/@inversifyjs/core": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@inversifyjs/core/-/core-9.2.0.tgz", - "integrity": "sha512-Nm7BR6KmpgshIHpVQWuEDehqRVb6GBm8LFEuhc2s4kSZWrArZ15RmXQzROLk4m+hkj4kMXgvMm5Qbopot/D6Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inversifyjs/common": "1.5.2", - "@inversifyjs/prototype-utils": "0.1.3", - "@inversifyjs/reflect-metadata-utils": "1.4.1" - } - }, - "node_modules/@inversifyjs/plugin": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@inversifyjs/plugin/-/plugin-0.2.0.tgz", - "integrity": "sha512-R/JAdkTSD819pV1zi0HP54mWHyX+H2m8SxldXRgPQarS3ySV4KPyRdosWcfB8Se0JJZWZLHYiUNiS6JvMWSPjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@inversifyjs/prototype-utils": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@inversifyjs/prototype-utils/-/prototype-utils-0.1.3.tgz", - "integrity": "sha512-EzRamZzNgE9Sn3QtZ8NncNa2lpPMZfspqbK6BWFguWnOpK8ymp2TUuH46ruFHZhrHKnknPd7fG22ZV7iF517TQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inversifyjs/common": "1.5.2" - } - }, - "node_modules/@inversifyjs/reflect-metadata-utils": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@inversifyjs/reflect-metadata-utils/-/reflect-metadata-utils-1.4.1.tgz", - "integrity": "sha512-Cp77C4d2wLaHXiUB7iH6Cxb7i1lD/YDuTIHLTDzKINqGSz0DCSoL/Dg2wVkW/6Qx03r/yQMLJ+32Agl32N2X8g==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "reflect-metadata": "~0.2.2" - } - }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1939,35 +1955,6 @@ "node": ">=8" } }, - "node_modules/@javascript-obfuscator/escodegen": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@javascript-obfuscator/escodegen/-/escodegen-2.4.2.tgz", - "integrity": "sha512-3N7EUdYxuldwvtOtZIlAxNwPW1qTUFwWt3PxNkik6hB1uSSXLXj9JMlSyUbTCXK3Osjn0Nu0s/aa4mbvkGouXQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@javascript-obfuscator/estraverse": "^5.3.0", - "esprima": "^4.0.1", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/@javascript-obfuscator/estraverse": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@javascript-obfuscator/estraverse/-/estraverse-5.4.0.tgz", - "integrity": "sha512-CZFX7UZVN9VopGbjTx4UXaXsi9ewoM1buL0kY7j1ftYdSs7p2spv9opxFjHlQ/QGTgh4UqufYqJJ0WKLml7b6w==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, "node_modules/@jest/console": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", @@ -2290,9 +2277,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "dev": true, "license": "MIT" }, @@ -2307,6 +2294,25 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2401,6 +2407,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@playwright/test": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", + "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.28", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", @@ -2408,6 +2430,530 @@ "dev": true, "license": "MIT" }, + "node_modules/@rollup/browser": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/browser/-/browser-4.63.1.tgz", + "integrity": "sha512-qsJijWUL79BPPzuVazW3E/tVQAkSMmjHVk/NQfRbr3BmviFqwxPb7twF0RfneJqebBikfAL2RxXGvzC6O1JEqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.9", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.9.tgz", + "integrity": "sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@sinonjs/commons": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", @@ -2470,16 +3016,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@turbowarp/sb3fix": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@turbowarp/sb3fix/-/sb3fix-0.5.0.tgz", - "integrity": "sha512-X5EOo+2nO3jIjEAzDY/Fmyn0wj6uAqOYXrRB9HAwTX4xWrmwvhuN/l8trVg/p+718/ySB9vQ7BGP2P0wklBCzw==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "@turbowarp/jszip": "^3.12.0" - } - }, "node_modules/@turbowarp/sbdl": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@turbowarp/sbdl/-/sbdl-7.0.0.tgz", @@ -2641,6 +3177,12 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, "node_modules/@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -2727,6 +3269,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/source-list-map": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.6.tgz", @@ -2766,13 +3315,6 @@ "source-map": "^0.6.1" } }, - "node_modules/@types/validator": { - "version": "13.15.10", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", - "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/webpack": { "version": "4.41.40", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.40.tgz", @@ -2827,94 +3369,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@vercel/blob": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@vercel/blob/-/blob-2.6.1.tgz", - "integrity": "sha512-KTJytw85j1XQBxjN5d6UXI7fIWNQe1jotn4nWN+0hePqLs+Qi1B3jHdQcSKFGF0m2rsy9uhPT6GOXMtHe3qNzg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@vercel/oidc": "^3.6.1", - "async-retry": "^1.3.3", - "is-buffer": "^2.0.5", - "is-node-process": "^1.2.0", - "throttleit": "^2.1.0", - "undici": "^6.23.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@vercel/blob/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@vercel/cli-config": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@vercel/cli-config/-/cli-config-0.2.2.tgz", - "integrity": "sha512-kAy35eymNzRBfmcqEViVQge0KJ59FYEKsPqGNCSJQ7M9HTuFGWd3qPcZos1A5cZpAWRBdiYe82Bcz9P7pIZvUQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "xdg-app-paths": "5", - "zod": "4.1.11" - } - }, - "node_modules/@vercel/cli-exec": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@vercel/cli-exec/-/cli-exec-1.0.1.tgz", - "integrity": "sha512-g9XerViJ/paZujufXYcu5XYI2vU2rtB4sgdpjUHde5RnOkdmpu0ngH46LCFGHoPXO/C+qDPSczIHIRN+8Q2YKQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "execa": "5.1.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@vercel/oidc": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.8.2.tgz", - "integrity": "sha512-nmVSeQ7tewCkqYBNB/MNL8aPB9sTvtjvqIE6+U17U4IuTyehuWVERDlWrSn0DVfiFAstRlRkGLHBlKHB2FyzbQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@vercel/cli-config": "0.2.2", - "@vercel/cli-exec": "1.0.1", - "jose": "^5.9.6" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@vernier/godirect": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@vernier/godirect/-/godirect-1.5.0.tgz", - "integrity": "sha512-vMS0fQubI3WSSLg1Ry3aey/qWCl9XoCsFzgwOWYkeJs45YxjPel+42pLh5pO7uP6oF47sjZUKx8kGOoTiiiirA==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -3147,6 +3601,15 @@ } } }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.12", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.12.tgz", + "integrity": "sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A==", + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -3230,16 +3693,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/acorn-walk": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", @@ -3450,16 +3903,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-differ": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", - "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/array-flatten": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", @@ -3554,16 +3997,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/asn1.js": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", @@ -3661,26 +4094,6 @@ "dev": true, "license": "MIT" }, - "node_modules/async-retry": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "retry": "0.13.1" - } - }, - "node_modules/async-retry/node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4312,19 +4725,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/btoa": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", - "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", - "dev": true, - "license": "(MIT OR Apache-2.0)", - "bin": { - "btoa": "bin/btoa.js" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/buffer": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", @@ -4572,13 +4972,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/canvas-toBlob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/canvas-toBlob/-/canvas-toBlob-1.0.0.tgz", - "integrity": "sha512-oU5bawygt/Nef9F+C49eTFmzXzz6yKdGqn6J1wn/LZQF5ulnnZVm0KIZzik85I6tjCbZFH6aa47j4bU2tkHxRw==", - "dev": true, - "license": "MIT" - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4596,13 +4989,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chance": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/chance/-/chance-1.1.13.tgz", - "integrity": "sha512-V6lQCljcLznE7tUYUM9EOAnnKXbctE6j/rdQkYOHIWbfGQbrzTsAXNW9CdU5XCo4ArXQCj/rb6HgxPlmGJcaUg==", - "dev": true, - "license": "MIT" - }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -4613,16 +4999,6 @@ "node": ">=10" } }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -4770,18 +5146,6 @@ "node": ">= 0.4" } }, - "node_modules/class-validator": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", - "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/validator": "^13.15.3", - "libphonenumber-js": "^1.11.1", - "validator": "^13.15.20" - } - }, "node_modules/clean-css": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", @@ -5307,16 +5671,6 @@ "node": ">= 8" } }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, "node_modules/crypto-browserify": { "version": "3.12.1", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", @@ -5601,13 +5955,6 @@ "dev": true, "license": "MIT" }, - "node_modules/decode-html": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/decode-html/-/decode-html-2.0.0.tgz", - "integrity": "sha512-lVJ+EBozhAXA2nSQG+xAgcD0P5K3uejnIIvM09uoQfS8AALkQ+HhHcEUvKovXi0EIpIZWjm0y8X7ULjaJpgY9w==", - "dev": true, - "license": "MIT" - }, "node_modules/decode-uri-component": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", @@ -5982,13 +6329,6 @@ "dev": true, "license": "MIT" }, - "node_modules/diff-match-patch": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.4.tgz", - "integrity": "sha512-Uv3SW8bmH9nAtHKaKSanOQmj2DnlH65fUpcrMdfdaOxUG02QQ4YGZ8AE7kKOMisF7UqvOlGKVYWRvezdncW9lg==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/diff-sequences": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", @@ -6340,22 +6680,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/env-paths": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-4.0.0.tgz", - "integrity": "sha512-pxP8eL2SwwaTRi/KHYwLYXinDs7gL3jxFcBYmEdYfZmZXbaVDvdppd0XBU8qVz03rDfKZMXg1omHCbsJjZrMsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-safe-filename": "^0.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/envinfo": { "version": "7.14.0", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", @@ -6615,19 +6939,6 @@ "node": ">=4.0" } }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -6665,6 +6976,13 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -7399,44 +7717,6 @@ "node": ">= 6" } }, - "node_modules/format-message": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/format-message/-/format-message-6.2.1.tgz", - "integrity": "sha512-6gBXI+MOE9pu9QNeZf95V87GKYiLCdUvhisjts75xonaPAshtBu20NTZt2l8kbYcoMtxNyH9E9af+aieIRfVmw==", - "dev": true, - "license": "MIT", - "dependencies": { - "format-message-formats": "^6.2.0", - "format-message-interpret": "^6.2.0", - "format-message-parse": "^6.2.0", - "lookup-closest-locale": "^6.2.0" - } - }, - "node_modules/format-message-formats": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/format-message-formats/-/format-message-formats-6.2.4.tgz", - "integrity": "sha512-smT/fAqBLqusWfWCKRAx6QBDAAbmYznWsIyTyk66COmvwt2Byiqd7SJe2ma9a5oV0kwRaOJpN/F4lr4YK/n6qQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/format-message-interpret": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/format-message-interpret/-/format-message-interpret-6.2.4.tgz", - "integrity": "sha512-dRvz9mXhITApyOtfuFEb/XqvCe1u6RMkQW49UJHXS8w2S8cAHCqq5LNDFK+QK6XVzcofROycLb/k1uybTAKt2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "format-message-formats": "^6.2.4", - "lookup-closest-locale": "^6.2.0" - } - }, - "node_modules/format-message-parse": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/format-message-parse/-/format-message-parse-6.2.4.tgz", - "integrity": "sha512-k7WqXkEzgXkW4wkHdS6Cv2Ou0rIFtiDelZjgoe1saW4p7FT7zS8OeAUpAekhormqzpeecR97e4vBft1zMsfFOQ==", - "dev": true, - "license": "MIT" - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -7515,7 +7795,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -8115,6 +8394,31 @@ "node": ">=6" } }, + "node_modules/html-minifier-terser/node_modules/terser": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", + "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/html-minifier-terser/node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/html-webpack-plugin": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.5.2.tgz", @@ -8589,18 +8893,6 @@ "node": ">= 0.10" } }, - "node_modules/inversify": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/inversify/-/inversify-7.11.0.tgz", - "integrity": "sha512-yZDprSSr8TyVeMGI/AOV4ws6gwjX22hj9Z8/oHAVpJORY6WRFTcUzhnZtibBUHEw2U8ArvHcR+i863DplQ3Cwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inversifyjs/common": "1.5.2", - "@inversifyjs/container": "1.15.0", - "@inversifyjs/core": "9.2.0" - } - }, "node_modules/ip": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", @@ -8961,27 +9253,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-nan": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-node-process": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", - "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", "dev": true, "license": "MIT" }, @@ -9068,6 +9343,16 @@ "dev": true, "license": "MIT" }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -9087,19 +9372,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-safe-filename": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-safe-filename/-/is-safe-filename-0.1.1.tgz", - "integrity": "sha512-4SrR7AdnY11LHfDKTZY1u6Ga3RuxZdl3YKWWShO5iyuG5h8QS4GD2tOb04peBJ5I7pXbR+CGBNEhTcwK+FzN3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-set": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", @@ -9388,99 +9660,6 @@ "node": ">=8" } }, - "node_modules/javascript-obfuscator": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/javascript-obfuscator/-/javascript-obfuscator-5.5.0.tgz", - "integrity": "sha512-YBkZLadMb0ynLQKKMj8urk+NPsglWa4Upwm/kd6x65gyY6YkNkCJi40OOdGDQa1bVGixgRAmVINwODkLnmiJNw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@javascript-obfuscator/escodegen": "2.4.2", - "@javascript-obfuscator/estraverse": "5.4.0", - "@vercel/blob": ">=0.23.0", - "acorn": "8.15.0", - "acorn-import-attributes": "^1.9.5", - "assert": "2.1.0", - "chalk": "4.1.2", - "chance": "1.1.13", - "class-validator": "0.14.3", - "commander": "12.1.0", - "env-paths": "4.0.0", - "eslint-scope": "8.4.0", - "eslint-visitor-keys": "4.2.1", - "fast-deep-equal": "3.1.3", - "inversify": "7.11.0", - "js-string-escape": "1.0.1", - "md5": "2.3.0", - "multimatch": "5.0.0", - "process": "0.11.10", - "reflect-metadata": "0.2.2", - "string-template": "1.0.0", - "stringz": "2.1.0", - "tslib": "2.8.1" - }, - "bin": { - "javascript-obfuscator": "bin/javascript-obfuscator" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/javascript-obfuscator/node_modules/assert": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", - "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "is-nan": "^1.3.2", - "object-is": "^1.1.5", - "object.assign": "^4.1.4", - "util": "^0.12.5" - } - }, - "node_modules/javascript-obfuscator/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/javascript-obfuscator/node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/javascript-obfuscator/node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, "node_modules/jest": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", @@ -10138,16 +10317,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/jose": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", - "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/js-md5": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz", @@ -10155,16 +10324,6 @@ "dev": true, "license": "MIT" }, - "node_modules/js-string-escape": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", - "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -10331,13 +10490,6 @@ "node": ">= 0.8.0" } }, - "node_modules/libphonenumber-js": { - "version": "1.13.10", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.10.tgz", - "integrity": "sha512-xJxrdqvbl2rtn2MaUJrUejz8J7/uZNC0V77oks2LxYrO/+ZtVpRmz+fEQMuu6VusnEB1fmpByiLS1WXecOnAnw==", - "dev": true, - "license": "MIT" - }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -10455,13 +10607,6 @@ "url": "https://tidelift.com/funding/github/npm/loglevel" } }, - "node_modules/lookup-closest-locale": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/lookup-closest-locale/-/lookup-closest-locale-6.2.0.tgz", - "integrity": "sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -10570,18 +10715,6 @@ "node": ">= 0.4" } }, - "node_modules/md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } - }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -10952,33 +11085,6 @@ "dev": true, "license": "MIT" }, - "node_modules/multimatch": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", - "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/minimatch": "^3.0.3", - "array-differ": "^3.0.0", - "array-union": "^2.1.0", - "arrify": "^2.0.1", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/multimatch/node_modules/@types/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", - "dev": true, - "license": "MIT" - }, "node_modules/multipipe": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.3.1.tgz", @@ -11531,16 +11637,6 @@ "dev": true, "license": "MIT" }, - "node_modules/os-paths": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/os-paths/-/os-paths-4.4.0.tgz", - "integrity": "sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6.0" - } - }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -11934,6 +12030,53 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/pngjs": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", @@ -12459,13 +12602,6 @@ "node": ">= 0.10" } }, - "node_modules/reflect-metadata": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -12828,6 +12964,51 @@ "inherits": "^2.0.1" } }, + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -12957,32 +13138,6 @@ "node": ">=10" } }, - "node_modules/sb3c-vm-obfuscator": { - "version": "0.1.0", - "resolved": "git+https://github.com/LuYifei2011/sb3c-vm-obfuscator.git#1fae276ae88c01c181fad481e99239ee5f8da1f5", - "dev": true, - "dependencies": { - "@babel/generator": "^7.28.0", - "@babel/parser": "^7.28.0", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0" - }, - "bin": { - "sb3c-map-stats": "src/stats-cli.mjs", - "sb3c-obfuscate": "src/cli.mjs" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "javascript-obfuscator": ">=5 <6" - }, - "peerDependenciesMeta": { - "javascript-obfuscator": { - "optional": true - } - } - }, "node_modules/schema-utils": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", @@ -13004,7 +13159,7 @@ }, "node_modules/scratch-audio": { "version": "0.1.0", - "resolved": "git+https://github.com/TurboWarp/scratch-audio.git#aba00cd02e36d95407effafa03f3678e4c669b30", + "resolved": "git+ssh://git@github.com/TurboWarp/scratch-audio.git#aba00cd02e36d95407effafa03f3678e4c669b30", "integrity": "sha512-SW920/XWeSC9u9DSnYjQT3LVE/yRVxGOk1Oml4ShTKysq3f0KuNmqWn02nurzk+LP9gEEx/unGYjUvVZyBo9ww==", "dev": true, "license": "BSD-3-Clause", @@ -13013,51 +13168,9 @@ "@turbowarp/startaudiocontext": "^1.0.0" } }, - "node_modules/scratch-parser": { - "version": "0.0.0-development", - "resolved": "git+https://github.com/TurboWarp/scratch-parser.git#fb65ba09d78ab6a0d19ce908d64b72d1a122312f", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "@turbowarp/json": "^0.1.1", - "@turbowarp/jszip": "^3.11.0", - "@turbowarp/sb3fix": "^0.5.0", - "ajv": "6.3.0", - "pify": "4.0.1" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/scratch-parser/node_modules/ajv": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.3.0.tgz", - "integrity": "sha512-6TQywaGYtRub2fqHkSXfVANlhfja2nbF33wCCHnt3aQstOrtd9jsQGiRUTIOlkEqcxpzRd2akfnqvBBPmLxs8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - } - }, - "node_modules/scratch-parser/node_modules/fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha512-fueX787WZKCV0Is4/T2cyAdM4+x1S3MXXOAhavE1ys/W42SHAPacLTQhucja22QBYrfGw50M2sRiXPtTGv9Ymw==", - "dev": true, - "license": "MIT" - }, - "node_modules/scratch-parser/node_modules/json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha512-4JD/Ivzg7PoW8NzdrBSr3UFwC9mHgvI7Z6z3QGBsSHgKaRTUDmyZAAKJo2UbG1kUVfS9WS8bi36N49U1xw43DA==", - "dev": true, - "license": "MIT" - }, "node_modules/scratch-render": { "version": "0.1.0", - "resolved": "git+https://github.com/TurboWarp/scratch-render.git#a67f7c9c07d459582c227d4fd3fae8f59d8fc9ce", + "resolved": "git+ssh://git@github.com/TurboWarp/scratch-render.git#a67f7c9c07d459582c227d4fd3fae8f59d8fc9ce", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -13074,25 +13187,13 @@ }, "node_modules/scratch-render-fonts": { "version": "1.0.0", - "resolved": "git+https://github.com/TurboWarp/scratch-render-fonts.git#7b6768fc6dfef6b343a06f992587b74807043961", + "resolved": "git+ssh://git@github.com/TurboWarp/scratch-render-fonts.git#7b6768fc6dfef6b343a06f992587b74807043961", "dev": true, "peer": true, "dependencies": { "base64-loader": "1.0.0" } }, - "node_modules/scratch-sb1-converter": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/scratch-sb1-converter/-/scratch-sb1-converter-0.2.7.tgz", - "integrity": "sha512-MF1NzmRLbgCiq7wiSTuk2C9rQC8uM5Wj7SaYsn1SEMWNK9rtK0uc3LSm8FnEt14QS/1Icx2YgMsUPKR0HaPDdQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "js-md5": "0.7.3", - "minilog": "3.1.0", - "text-encoding": "^0.7.0" - } - }, "node_modules/scratch-storage": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/scratch-storage/-/scratch-storage-1.3.6.tgz", @@ -13108,171 +13209,9 @@ "worker-loader": "^2.0.0" } }, - "node_modules/scratch-translate-extension-languages": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/scratch-translate-extension-languages/-/scratch-translate-extension-languages-1.0.7.tgz", - "integrity": "sha512-6+bQU9iVYv23T8J0SjpV6MTugm0y8myh/4DPgu1BGfccysdkaWzu3MkNGQyQRUlbqAiW9wM7ctfv3USPEkzTgg==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/scratch-vm": { - "version": "2.1.46", - "resolved": "git+https://github.com/LuYifei2011/scratch-vm.git#061f4bd5ee3592aaa53db09e715c8249fcee9d9b", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "@turbowarp/json": "^0.1.2", - "@turbowarp/jszip": "^3.11.0", - "@turbowarp/nanolog": "^0.2.0", - "@vernier/godirect": "1.5.0", - "arraybuffer-loader": "^1.0.6", - "atob": "2.1.2", - "btoa": "1.2.1", - "canvas-toBlob": "1.0.0", - "decode-html": "2.0.0", - "diff-match-patch": "1.0.4", - "format-message": "6.2.1", - "htmlparser2": "3.10.0", - "scratch-parser": "github:TurboWarp/scratch-parser#master", - "scratch-sb1-converter": "0.2.7", - "scratch-translate-extension-languages": "^1.0.7", - "text-encoding": "0.7.0", - "uuid": "8.3.2", - "worker-loader": "^1.1.1" - }, - "peerDependencies": { - "@turbowarp/scratch-svg-renderer": "^1.1.0" - } - }, - "node_modules/scratch-vm/node_modules/dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - } - }, - "node_modules/scratch-vm/node_modules/dom-serializer/node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/scratch-vm/node_modules/dom-serializer/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/scratch-vm/node_modules/domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/scratch-vm/node_modules/domhandler": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "1" - } - }, - "node_modules/scratch-vm/node_modules/domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "node_modules/scratch-vm/node_modules/entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/scratch-vm/node_modules/htmlparser2": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.0.tgz", - "integrity": "sha512-J1nEUGv+MkXS0weHNWVKJJ+UrLfePxRWpN3C9bEi9fLxL2+ggW94DQvgYVXsaT30PGwYRIZKNZXuyMhp3Di4bQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^1.3.0", - "domhandler": "^2.3.0", - "domutils": "^1.5.1", - "entities": "^1.1.1", - "inherits": "^2.0.1", - "readable-stream": "^3.0.6" - } - }, - "node_modules/scratch-vm/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/scratch-vm/node_modules/schema-utils": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", - "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.1.0", - "ajv-keywords": "^3.1.0" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/scratch-vm/node_modules/worker-loader": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-1.1.1.tgz", - "integrity": "sha512-qJZLVS/jMCBITDzPo/RuweYSIG8VJP5P67mP/71alGyTZRe1LYJFdwLjLalY3T5ifx0bMDRD3OB6P2p1escvlg==", - "dev": true, - "license": "MIT", - "dependencies": { - "loader-utils": "^1.0.0", - "schema-utils": "^0.4.0" - }, - "engines": { - "node": ">= 4.8 < 5.0.0 || >= 5.10" - }, - "peerDependencies": { - "webpack": "^2.0.0 || ^3.0.0 || ^4.0.0" - } + "resolved": "../scratch-vm", + "link": true }, "node_modules/select-hose": { "version": "2.0.0", @@ -14389,13 +14328,6 @@ "node": ">=10" } }, - "node_modules/string-template": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string-template/-/string-template-1.0.0.tgz", - "integrity": "sha512-SLqR3GBUXuoPP5MmYtD7ompvXiG87QjT6lzOszyXjTM86Uu7At7vNnt2xgyTLq5o9T4IxTYFyGxcULqpsmsfdg==", - "dev": true, - "license": "MIT" - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -14470,16 +14402,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/stringz": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/stringz/-/stringz-2.1.0.tgz", - "integrity": "sha512-KlywLT+MZ+v0IRepfMxRtnSvDCMc3nR1qqCs3m/qIbSOWkNZYT8XHQA31rS3TnKp0c5xjZu3M4GY/2aRKSi/6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -14761,21 +14683,22 @@ } }, "node_modules/terser": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", - "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", + "version": "5.51.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.2.tgz", + "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==", "dev": true, "license": "BSD-2-Clause", "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" + "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" }, "engines": { - "node": ">=6.0.0" + "node": ">=10" } }, "node_modules/terser-webpack-plugin": { @@ -14888,14 +14811,6 @@ "node": ">=8" } }, - "node_modules/text-encoding": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz", - "integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==", - "deprecated": "no longer maintained", - "dev": true, - "license": "(Unlicense OR Apache-2.0)" - }, "node_modules/throat": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz", @@ -14903,19 +14818,6 @@ "dev": true, "license": "MIT" }, - "node_modules/throttleit": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", - "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -15300,16 +15202,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/undici": { - "version": "6.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", - "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", @@ -15765,16 +15657,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/validator": { - "version": "13.15.35", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", - "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -17603,33 +17485,6 @@ } } }, - "node_modules/xdg-app-paths": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/xdg-app-paths/-/xdg-app-paths-5.5.1.tgz", - "integrity": "sha512-hI3flOB4PLZIy5prbtTpirobtPE2ZtZ52szO+2mM9Efp6ErM398La+C1lIpNWDfNoQk+6Lsi6nMcCwVB7pxeMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "os-paths": "^4.0.1", - "xdg-portable": "^7.2.0" - }, - "engines": { - "node": ">= 6.0" - } - }, - "node_modules/xdg-portable": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/xdg-portable/-/xdg-portable-7.3.0.tgz", - "integrity": "sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "os-paths": "^4.0.1" - }, - "engines": { - "node": ">= 6.0" - } - }, "node_modules/xml-name-validator": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", @@ -17712,16 +17567,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zod": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.11.tgz", - "integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } } } } diff --git a/package.json b/package.json index 792df46..730b528 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,28 @@ { - "name": "@luyifei2011/tw-packager", + "name": "@sccn/tw-packager", "version": "3.13.0", "description": "Compiles, protects, and packages Scratch projects as HTML, archives, or desktop applications.", "main": "dist/packager.js", "scripts": { - "start": "webpack serve", + "start": "npm run prepare:specialized && webpack serve", "clean": "rimraf dist", - "build": "npm run clean && webpack", + "build": "npm run clean && npm run prepare:specialized && webpack", "build-prod": "cross-env NODE_ENV=production npm run build", - "watch": "webpack --watch", + "watch": "npm run prepare:specialized && webpack --watch", "build-standalone": "cross-env STANDALONE=1 npm run build && node src/build/generate-standalone.js", "build-standalone-prod": "cross-env NODE_ENV=production npm run build-standalone", "build-node": "cross-env BUILD_MODE=node npm run build", "build-node-prod": "cross-env NODE_ENV=production npm run build-node", "test": "jest", - "prepublishOnly": "npm run test && npm run build-node-prod && node src/build/nodejs/update-readme.js node", - "postpublish": "node src/build/nodejs/update-readme.js restore" + "prepublishOnly": "npm run test && npm run build-node-prod && npm run test:specialized && npm run test:specialized-linked && npm run test:specialized-build && npm run test:specialized-performance && node src/build/nodejs/update-readme.js node", + "postpublish": "node src/build/nodejs/update-readme.js restore", + "prepare:specialized": "node scripts/prepare-runtime-sources.mjs && node scripts/build-linker.mjs", + "test:specialized": "node test/specialized-differential.mjs && node --test test/specialized-lifecycle.mjs test/specialized-structure.mjs test/specialized-rendering.mjs test/specialized-properties.mjs", + "test:specialized-linked": "node test/specialized-differential.mjs --linked", + "test:specialized-build": "node test/specialized-build.mjs", + "test:specialized-browser": "node test/specialized-browser.mjs", + "test:specialized-performance": "node test/specialized-performance.mjs", + "test:specialized-scratch-benchmark": "node test/specialized-scratch-benchmark.mjs" }, "author": "LuYifei2011", "homepage": "https://luyifei2011.github.io/tw-packager/", @@ -36,12 +43,24 @@ "@turbowarp/json": "^0.1.1", "@turbowarp/jszip": "^3.11.0", "@turbowarp/sbdl": "^7.0.0", + "@xmldom/xmldom": "^0.9.12", "cross-fetch": "^4.1.0", + "pngjs": "^6.0.0", + "rollup": "4.63.1", "sha.js": "^2.4.11" }, "devDependencies": { "@babel/core": "^7.16.5", + "@babel/generator": "^7.28.0", + "@babel/parser": "^7.28.0", "@babel/preset-env": "^7.16.5", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "@playwright/test": "^1.62.1", + "@rollup/browser": "4.63.1", + "@rollup/plugin-commonjs": "^28.0.6", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.1", "@turbowarp/scratch-storage": "^2.0.0", "@turbowarp/scratch-svg-renderer": "^1.1.0", "babel-jest": "^27.4.5", @@ -52,16 +71,15 @@ "fastestsmallesttextencoderdecoder": "^1.0.22", "file-loader": "^6.2.0", "html-webpack-plugin": "^4.5.2", - "javascript-obfuscator": "^5.5.0", "jest": "^27.4.5", "rimraf": "^2.7.1", "scratch-audio": "git+https://github.com/TurboWarp/scratch-audio.git#develop", "scratch-render": "git+https://github.com/TurboWarp/scratch-render.git#develop", - "scratch-vm": "git+https://github.com/LuYifei2011/scratch-vm.git#compiled-project-artifacts", - "sb3c-vm-obfuscator": "git+https://github.com/LuYifei2011/sb3c-vm-obfuscator.git#main", + "scratch-vm": "file:../scratch-vm", "style-loader": "^2.0.0", "svelte": "^3.44.3", "svelte-loader": "^3.1.2", + "terser": "^5.43.1", "url-loader": "^4.1.1", "webpack": "4.47.0", "webpack-bundle-analyzer": "^4.5.0", @@ -73,5 +91,8 @@ "webpack@4.47.0": { "terser-webpack-plugin": "^4.2.3" } + }, + "engines": { + "node": ">=22.15.0" } } diff --git a/scaffolding-api-docs/README.md b/scaffolding-api-docs/README.md deleted file mode 100644 index b25b81c..0000000 --- a/scaffolding-api-docs/README.md +++ /dev/null @@ -1,381 +0,0 @@ -# Scaffolding API - -## Scaffolding's API is in an ALPHA state. It WILL change in ways that are backwards incompatible. Please pin to exact versions to avoid pain later! - -## This documentation is very work in progress. Report any errors you find or any issues you encounter. - -Scaffolding is the project player used by the packager. - -Here's what Scaffolding does for you: - - - Loads the entire Scratch VM and connects everything together - - Forwards user input to Scratch and implements things like sprite dragging - - Removes unnecessary parts of Scratch that you don't need just for running projects - - Implements the "ask and wait" block - - Implements highly optimized (much faster than scratch-gui) variable and list monitors with context menus and file dropping - - Implements cloud variables using a Scratch-compatible WebSocket server or local storage - - Implements video sensing - -Here's what Scaffolding doesn't do for you: - - - Green flag screen - - Controls - - A progress bar - -## Versions - -There are two versions of Scaffolding: - - - scaffolding-full (4.6MB) - contains every part of Scratch - - scaffolding-min (2.5MB) - contains all of Scratch EXCEPT sound files used by the music extension. Significantly smaller than scaffolding-full. - -Scratch is a large application so either script is very large. If you don't need the music extension to function, use scaffolding-min. - -## Supported environments - -Scaffolding strives to support any web browser released in 2018 or later. - -Scaffolding only runs in web browsers. It will not run in Node.js. - -## Content-Security-Policy - -Meaningfully sandboxing Scaffolding with a Content-Security-Policy is hard. It needs to be able to fetch things from `data:` and `blob:` URLs, and the compiler (enabled by default) requires `unsafe-eval`. - -This section will be updated later with more guidance. - -## Installing - -Scaffolding is distributed as one big JavaScript file. Any supplemental CSS etc. is stored inside the JS file. - -We make no promise of having a stable API between even minor releases as Scaffolding is effectively just an implementation detail of the packager, which is why it's very important to lock your application to specific versions. - -You can only load one version of Scaffolding on a page, but you can create as many Scaffolding project players as you'd like. - -### From a CDN - -```html - - - - - -``` - -Replace `0.0.0` with the latest release from https://github.com/TurboWarp/packager/releases, for example `1.0.0`. - -DO NOT use versions like `@1` or `@latest` -- your website will break if (when) Scaffolding's API changes! - -If you don't want to use a CDN, you can download the JS file linked by the script tag to a server you control and simply load that script instead. - -### From npm - -```bash -npm install --save-exact @turbowarp/packager -``` - -```js -// for scaffolding-min: -require('@turbowarp/packager/dist/scaffolding/scaffolding-min.js'); -// for scaffolding-full: -require('@turbowarp/packager/dist/scaffolding/scaffolding-full.js'); - -// or, if you prefer ES6 imports, -// for scaffolding-min: -import '@turbowarp/packager/dist/scaffolding/scaffolding-min.js'; -// for scaffolding-full: -import '@turbowarp/packager/dist/scaffolding/scaffolding-full.js'; -``` - -Note that regardless of how you import Scaffolding, it is exported on `window.Scaffolding`, not on the module. This is strange and weird; it may change in the future. - -## Usage - -For an example, see [../static/example.html](../static/example.html). You can also examine the output of files generated by the packager. - -At this point you should have Scaffolding installed on `window.Scaffolding`. - -### Setup - -Create an instance of Scaffolding: - -```js -const scaffolding = new Scaffolding.Scaffolding(); -``` - -You should be able to have multiple of these on one page if you want. - -### Configuration - -Certain Scaffolding options must be configured before you can finish setting up the Scratch environment. These are optional. The options that must be configured at this point are listed below along with their defaults. - -```js -scaffolding.width = 480; // Custom stage width -scaffolding.height = 360; // Custom stage height -scaffolding.resizeMode = 'preserve-ratio'; // or 'dynamic-resize' or 'stretch' -scaffolding.editableLists = false; // Affects list monitors -``` - -### Finalize setup - -This will actually create the Scratch VM, Renderer, Audio Engine, etc. and link them all together. - -```js -scaffolding.setup(); -``` - -### Put it in the DOM somewhere - -You are responsible for making a location in the DOM for Scaffolding and using CSS to appropriately size it. - -If your HTML looks like this: - -```html - -
-``` - -You would append Scaffolding to the DOM like this: - -```js -scaffolding.appendTo(document.getElementById('project')); -``` - -Scaffolding will automatically resize the project player to fit inside the space you gave it. It listens for the window 'resize' event to do this. If you are resizing the element in a way that doesn't involve a window resize, you can manually call `scaffolding.relayout()` to force a resize. This can be slow, so don't do it when you don't need to. - -### Tell it where to fetch projects and assets from - -You have to manually configure scratch-storage to know where to fetch files from. If you want to load projects from scratch.mit.edu, you would do: - -```js -const storage = scaffolding.storage; -storage.addWebStore( - [storage.AssetType.ImageVector, storage.AssetType.ImageBitmap, storage.AssetType.Sound], - (asset) => `https://assets.scratch.mit.edu/internalapi/asset/${asset.assetId}.${asset.dataFormat}/get/` -); -``` - -Downloading shared Scratch projects can be done manually with something like this: - -```js -const id = '437419376'; -const projectMetadata = await (await fetch(`https://trampoline.turbowarp.org/api/projects/${id}`)).json(); -const token = projectMetadata.project_token; -const projectData = await (await fetch(`https://projects.scratch.mit.edu/${id}?token=${token}`)).arrayBuffer(); -``` - -### Configure cloud variables - -By default cloud variables are just normal variables. You must reconfigure this manually if you don't want that. - -To synchronize cloud variables globally: - -```js -const cloudVariableProvider = scaffolding.addCloudProvider(new Scaffolding.Cloud.WebSocketProvider('wss://clouddata.example.com', 'PROJECT ID HERE')); -``` - -To store cloud variables locally: - -```js -const localStorageProvider = scaffolding.addCloudProvider(new Scaffolding.Cloud.LocalStorageProvider('UNIQUE STORAGE KEY (such as project ID) HERE')); -``` - -To use a different provider for different variables, there is an override system. - -```js -scaffolding.addCloudProviderOverride("☁ my variable", cloudVariableProvider); -scaffolding.addCloudProviderOverride("☁ other variable", localStorageProvider); -``` - -### Set username - -To change the value of the username block from the default empty string: - -```js -scaffolding.setUsername("ExampleUser"); -``` - -### Set accent color - -To change the accent color used in prompts, context menus, etc.: - -```js -scaffolding.setAccentColor("#abcdef"); -``` - -The color MUST be a hex color code with 6 characters. 3 character codes will not work. The leading # is required. Color names will also not work. - -### Load a project - -At this point you can load a project. Scaffolding accepts an ArrayBuffer, Uint8Array, or string containing a full compressed project or a project.json. - -Full compressed projects are full sb/sb2/sb3 files including assets. - -project.json is just the JSON part of an sb2/sb3 file. In this case assets will be fetched separately. - -```js -scaffolding.loadProject(projectData) - .then(() => { - // ... - }) - .catch((error) => { - // ... - }); -``` - -This returns a Promise that resolves when the project has finished loading or rejects if the project could not be loaded. - -The project is not automatically started when loadProject completes. - -If you configured scratch-storage to load projects from scratch.mit.edu, you can use: - -```js -scaffolding.storage.load(Scaffolding.Storage.AssetType.Project, "PROJECT ID HERE eg. 104") - .then((projectDataAsset) => scaffolding.load(projectDataAsset.data)) - .then(() => { - // ... - }) - .catch((error) => { - // ... - }); -``` - -### Start the project - -This will start the project *and* start scripts that are supposed to run when you press the green flag. - -If you want to automatically start the project, you can do: - -```js -scaffolding.loadProject(/* ... */) // see previous step - .then(() => { - scaffolding.start(); - }); -``` - -If you want to display something like Scratch's green flag screen, you can create the screen yourself and call scaffolding.start() when the user clicks on it. - -### Advanced topics - -Scaffolding gives you direct access to various internal classes. - -|Package|Instance|Constructor| -|:-:|:-:|:-:| -|scratch-vm|`scaffolding.vm`|`Scaffolding.VM`| -|scratch-render|`scaffolding.renderer`|`Scaffolding.Renderer`| -|scratch-storage|`scaffolding.storage`|`Scaffolding.Storage`| -|scratch-audio|`scaffolding.audioEngine`|`Scaffolding.AudioEngine`| -|JSZip|N/A|`Scaffolding.JSZip`| - -Constructors are immediately available when scaffolding-[full|min].js is loaded. Instances are available after scaffolding.setup() is called. - -The scratch-audio instance might be null if the browser doesn't support the [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API). This is pretty rare to not be supported, but a few people do turn it off for one reason or another. - -The scratch-storage constructor is actually a wrapper used for implementing progress monitoring. - -### VM Settings - -```js -// Press the green flag (like Scratch's green flag button) -scaffolding.greenFlag(); - -// Stop all (like Scratch's stop sign button) -scaffolding.stopAll(); - -// Turbo Mode -scaffolding.vm.setTurboMode(true); - -// Framerate -scaffolding.vm.setFramerate(60); - -// Interpolation -scaffolding.vm.setInterpolation(true); - -// High quality pen -scaffolding.renderer.setUseHighQualityRender(true); - -// Turn off the compiler -scaffolding.vm.setRuntimeOptions({enabled: false}); - -// Infinite clones, Remove miscellaneous limits, Remove fencing -scaffolding.vm.setRuntimeOptions({fencing: false, miscLimits: false, maxClones: Infinity}); - -// Load custom extension -// Do this before calling loadProject() for best results. -scaffolding.vm.extensionManager.loadExtensionURL('https://extensions.turbowarp.org/fetch.js'); - -// Do something when the project stops -vm.runtime.on('PROJECT_RUN_STOP', () => { - // ... -}); -``` - -### Get or set variables and lists - -```js -scaffolding.getVariable("global variable name"); // -> "Hello, world!" -scaffolding.getList("global list name"); // -> [1, 2, 3] - -scaffolding.setVariable("global variable name", "New value"); -scaffolding.setList("global list name", [4, 5, 6]); -``` - -Variables must be a number, string, or boolean, and lists can only contain numbers, strings, and booleans. The VM was not designed to handle these invalid values and will not behave properly. Invalid values will throw an error. - -Lists returned by getList are returned by reference, so any changes you make to them will be reflected in the project. However, changes might not display on the any visible monitors unless you use setList. - -### Progress monitoring - -Scaffolding can't help you monitor the progress of the data you feed into loadProject() -- you have to do that on your own. That might mean using an XMLHttpRequest to fetch the data so you can listen to the progress event. - -Scaffolding can help you monitor asset download progress. - -```js -scaffolding.storage.onprogress = (totalAssets, loadedAssets) => { - // ... -}; -``` - -Again, you are responsible for displaying a progress bar on your own. - -### Fullscreen - -Scaffolding doesn't have an API for fullscreen. Instead, just use the DOM fullscreen API on an element that contains scaffolding. - -```js -document.getElementById('project').requestFullScreen(); -``` - -### Advanced styling - -You are free to override the styles for any class starting with `sc-`. - -### Addons - -In the same place as scaffolding-[full|min].js, there is a file called addons.js that contains some extra features. You can load it the same way you load the normal scaffolding script. It exports on window.ScaffoldingAddons instead. - -```js -ScaffoldingAddons.run(scaffolding, { - // Enable what you want. - gamepad: false, - pointerlock: false, - specialCloudBehaviors: false, - unsafeCloudBehaviors: false, - pause: false, // Adds API: scaffolding.vm.setPaused(true) -}); -``` - -### Exporting sb3 - -Here's something to get you started: - -```js -Object.assign(document.createElement("a"), { - href: URL.createObjectURL(await scaffolding.vm.saveProjectSb3()), - download: 'project.sb3' -}).click(); -``` diff --git a/scripts/build-linker.mjs b/scripts/build-linker.mjs new file mode 100644 index 0000000..f45f8fe --- /dev/null +++ b/scripts/build-linker.mjs @@ -0,0 +1,28 @@ +import fs from 'node:fs/promises'; +import {rollup} from 'rollup'; +import {nodeResolve} from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import json from '@rollup/plugin-json'; + +await fs.mkdir('generated', {recursive: true}); +for (const node of [true, false]) { + const bundle = await rollup({input: `src/specialized/${node ? 'node' : 'worker'}.mjs`, + external: node ? ['rollup', 'node:child_process'] : [], + onwarn(warning) { if (warning.code !== 'CIRCULAR_DEPENDENCY') console.warn(warning.message); }, + plugins: [{name: 'build-environment', transform(code, id) { + if (!/\.[cm]?js$/.test(id)) return; + return {code: code.replace(/process\.env\.BABEL_TYPES_8_BREAKING/g, 'false') + .replace(/process\.env\.BABEL_8_BREAKING/g, 'false').replace(/process\.env\.BABEL_TYPES_8_BREAKING/g, 'false') + .replace(/process\.env\.NODE_ENV/g, '"production"'), map: null}; + }}, nodeResolve({browser: !node, preferBuiltins: node}), commonjs(), json()]}); + await bundle.write({file: `generated/${node ? 'specialized-link.cjs' : 'specialized-worker.js'}`, + format: node ? 'cjs' : 'iife', inlineDynamicImports: true, + banner: node ? '' : 'var global = globalThis; var process = {env: {}, platform: "browser"};'}); + await bundle.close(); +} +const browserRollup = await fs.readFile('node_modules/@rollup/browser/dist/rollup.browser.js', 'utf8'); +const wasmBootstrap = /(\w+)=new URL\("bindings_wasm_bg.wasm",/; +if (!wasmBootstrap.test(browserRollup)) throw new Error('Pinned Rollup WASM bootstrap changed'); +await fs.writeFile('generated/rollup.browser.js', browserRollup.replace(wasmBootstrap, + '$1=globalThis.__specializedWasmBytes||new URL("bindings_wasm_bg.wasm",')); +await fs.copyFile('node_modules/@rollup/browser/dist/bindings_wasm_bg.wasm', 'generated/bindings_wasm_bg.wasm'); diff --git a/scripts/prepare-runtime-sources.mjs b/scripts/prepare-runtime-sources.mjs new file mode 100644 index 0000000..ade33e9 --- /dev/null +++ b/scripts/prepare-runtime-sources.mjs @@ -0,0 +1,420 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {createRequire} from 'node:module'; +import {rollup} from 'rollup'; +import commonjs from '@rollup/plugin-commonjs'; +import {nodeResolve} from '@rollup/plugin-node-resolve'; +import json from '@rollup/plugin-json'; +import {createHash} from 'node:crypto'; +import {unbundleTWGL} from './unbundle-twgl.mjs'; + +const require = createRequire(import.meta.url); +const {parse} = require('@babel/parser'); +const traverse = require('@babel/traverse').default; +const generate = require('@babel/generator').default; +const t = require('@babel/types'); +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const vm = path.resolve(root, '../scratch-vm'); +const sourceRoot = path.join(root, 'src'); +const vmSource = path.join(vm, 'src'); +const nativeDependencies = ['scratch-render', 'scratch-audio', '@turbowarp/scratch-storage', '@turbowarp/scratch-svg-renderer'] + .map(name => path.join(root, 'node_modules', name, 'src') + path.sep); +const output = path.join(root, 'generated/runtime-sources.json'); +const sources = {}; +const provenance = {}; +const ast = code => parse(code, {sourceType: 'unambiguous'}); +const method = (name, body) => ast(`class A { ${name} ${body} }`).program.body[0].body.body[0]; +const twglModules = unbundleTWGL(await fs.readFile(require.resolve('twgl.js'), 'utf8')); +const auditedWrappers = new Map(); +for (const [file, hash] of [ + ['utils.js', 'c58704c6a6ef17f3e78a1ef4dd6e7aa20712fe673a929eb4c08fc86fcc9341cd'], + ['base64.js', '5df6b73d9ed6c317c67835f3427d860c16fd93acf503eb471fd897167e747fdb'] +]) { + const filename = path.join(root, 'node_modules/@turbowarp/jszip/lib', file); + if (createHash('sha256').update(await fs.readFile(filename)).digest('hex') !== hash) { + throw new Error(`JSZip ${file} changed; review circular export initialization before updating the source pack`); + } + auditedWrappers.set(filename, {sha256: hash, + reason: 'JSZip utils -> base64 -> utils captures partially initialized mutable CommonJS exports; preserve circular export identity and first-use ordering', + cycle: ['@turbowarp/jszip/lib/utils.js', '@turbowarp/jszip/lib/base64.js', '@turbowarp/jszip/lib/utils.js']}); +} + +// Production source preparation happens once, in Node, before publishing the build tool. +// The browser receives resolved ESM, so it needs neither require() nor filesystem plugins. +function nativeModules(code, id) { + const tree = ast(code); + const imports = new Map(); + const getImport = name => { + if (!imports.has(name)) imports.set(name, t.identifier(`__dependency${imports.size}`)); + return t.cloneNode(imports.get(name)); + }; + let exported = false; + let namedExports = false; + traverse(tree, { + ReferencedIdentifier(p) { + if (p.node.name === 'exports' && !p.scope.hasBinding('exports')) { + namedExports = true; p.replaceWith(t.identifier('__exports')); + } + }, + CallExpression(p) { + if (t.isIdentifier(p.node.callee, {name: 'require'})) { + if (p.node.arguments.length !== 1 || !t.isStringLiteral(p.node.arguments[0])) { + throw new Error(`Non-static first-party require in ${id}`); + } + p.replaceWith(getImport(p.node.arguments[0].value)); + } + }, + ExpressionStatement(p) { + const expression = p.node.expression; + if (t.isAssignmentExpression(expression) && t.isMemberExpression(expression.left) && + t.isIdentifier(expression.left.object, {name: 'module'}) && + t.isIdentifier(expression.left.property, {name: 'exports'})) { + if (exported) throw new Error(`Multiple CommonJS exports in ${id}`); + exported = true; + p.replaceWith(t.exportDefaultDeclaration(expression.right)); + } + } + }); + if (namedExports) { + tree.program.body.unshift(...ast('const __exports = {};').program.body); + tree.program.body.push(...ast('export default __exports;').program.body); + } + tree.program.body.unshift(...[...imports].map(([name, local]) => + t.importDeclaration([['@turbowarp/json', 'uuid', '@vernier/godirect', 'transformation-matrix'].includes(name) ? + t.importNamespaceSpecifier(local) : t.importDefaultSpecifier(local)], t.stringLiteral(name)))); + return generate(tree, {comments: false}).code; +} + +function prepareVM(code, id) { + if (/\/engine\/blocks-(execute|runtime)-cache.js$/.test(id)) code = code.replace("require('./blocks');", ''); + const tree = ast(code); + if (id.endsWith('/virtual-machine.js')) { + // The playback facade and in-bundle addons are the only consumers of this class. + const roots = new Set(['constructor', 'start', 'greenFlag', 'stopAll', 'quit', 'clear', + 'attachStorage', 'attachRenderer', 'attachAudioEngine', 'attachV2BitmapAdapter', + 'setVideoProvider', 'setCloudProvider', 'postIOData', 'setTurboMode', 'setCompatibilityMode', + 'setFramerate', 'setInterpolation', 'setRuntimeOptions', 'setCompilerOptions', 'setStageSize', + 'setInEditor', 'convertToPackagedRuntime', 'loadProject', 'getTargetIdForDrawableId', + 'startDrag', 'stopDrag', 'postSpriteInfo', 'setLocale', 'setTranslate', 'getLocale', + 'setVariableValue', 'getVariableValue', 'setListValue', 'getListValue', 'setEditingTarget', + 'scanForPeripheral', 'connectPeripheral', 'disconnectPeripheral', 'getPeripheralIsConnected', + 'securityManager', 'runtime', 'renderer', 'editingTarget']); + const cls = tree.program.body.find(n => t.isClassDeclaration(n)); + traverse(tree, {ExpressionStatement(p) { + const node = p.node.expression; + if (t.isAssignmentExpression(node) && t.isMemberExpression(node.left) && + t.isThisExpression(node.left.object) && ['exports', 'blockListener', 'flyoutBlockListener', + 'monitorBlockListener', 'variableListener'].includes(node.left.property.name)) p.remove(); + }}); + const load = cls.body.body.find(n => n.key.name === 'loadProject'); + load.async = true; + load.body = ast(`async function load(input) { + const project = typeof input === 'string' ? JSON.parse(input) : + input instanceof ArrayBuffer || ArrayBuffer.isView(input) ? JSON.parse(new TextDecoder().decode(input)) : input; + this.clear(); + for (const id of project.extensions || []) await this.extensionManager.loadExtensionURL((project.extensionURLs || {})[id] || id); + const {targets} = await require('./serialization/sb3').deserialize(project, this.runtime); + for (let index = 0; index < targets.length; index++) { + const target = targets[index]; + target.sprite.__spTarget = index; + this.runtime.addTarget(target); + target.updateAllDrawableProperties(); + } + this.runtime.executableTargets.sort((a,b) => a.layerOrder-b.layerOrder); + targets.forEach(target => { delete target.layerOrder; }); + this.editingTarget = targets[1] || targets[0]; + this.runtime.setEditingTarget(this.editingTarget); + this.runtime.ioDevices.cloud.setStage(this.runtime.getTargetForStage()); + this.runtime.handleProjectLoaded(); + }`).program.body[0].body; + let changed = true; + while (changed) { + changed = false; + for (const member of cls.body.body) if (roots.has(member.key.name)) { + t.traverseFast(member, node => { + if (t.isMemberExpression(node) && t.isThisExpression(node.object) && + !node.computed && !roots.has(node.property.name)) { + roots.add(node.property.name); changed = true; + } + }); + } + } + cls.body.body = cls.body.body.filter(member => roots.has(member.key.name)); + } + if (id.endsWith('/engine/sequencer.js')) { + tree.program.body.unshift(...ast("const SpecializedBlockUtility = require('../compiler/specialized-block-utility'); const Cast = require('../util/cast'); const log = require('../util/log');").program.body); + const cls = tree.program.body.find(n => t.isClassDeclaration(n)); + cls.body.body = cls.body.body.filter(n => !['stepThread', 'stepToBranch', 'stepToProcedure'].includes(n.key.name)); + cls.body.body.push(method('stepThread(thread)', `{ '__SPECIALIZED_STEP__'; }`)); + const retire = cls.body.body.find(n => n.key.name === 'retireThread'); + retire.body.body.unshift(...ast('thread.__spLifetime = null; thread.__spFrames = [];').program.body); + } + if (id.endsWith('/engine/runtime.js')) { + const cls = tree.program.body.find(n => t.isClassDeclaration(n)); + cls.body.body = cls.body.body.filter(n => !['startHats', 'getOpcodeFunctionById'].includes(n.key.name)); + cls.body.body.push(method('startHats(opcode, fields, target)', `{ '__SPECIALIZED_HATS__'; }`)); + cls.body.body.push(method('__spPrimitive(opcode, args, utility)', `{ '__SPECIALIZED_PRIMITIVE__'; }`)); + for (const name of ['_pushThread', '_restartThread']) { + const m = cls.body.body.find(n => n.key.name === name); + // All threads use the linked sequencer. A new Thread has a new lifetime identity. + t.traverseFast(m, node => { + if (t.isCallExpression(node) && t.isMemberExpression(node.callee) && + t.isIdentifier(node.callee.property, {name: 'tryCompile'})) { + node.callee.property.name = '__spInitialize'; + } + }); + if (name === '_restartThread') { + m.body.body.unshift(...ast('thread.__spLifetime = null;').program.body); + t.traverseFast(m, node => { + if (t.isReturnStatement(node) && t.isIdentifier(node.argument, {name: 'newThread'})) { + node.argument = t.sequenceExpression([ + t.assignmentExpression('=', t.memberExpression(t.identifier('newThread'), t.identifier('executableHat')), + t.memberExpression(t.identifier('thread'), t.identifier('executableHat'))), node.argument]); + } + }); + } + } + } + if (id.endsWith('/engine/thread.js')) { + const cls = tree.program.body.find(n => t.isClassDeclaration(n, {id: {name: 'Thread'}})) || + tree.program.body.filter(n => t.isClassDeclaration(n)).at(-1); + cls.body.body = cls.body.body.filter(n => !['tryCompile', 'getCompiledProcedure', 'stopThisScript'].includes(n.key.name)); + cls.body.body.push(method('__spInitialize()', `{ + this.triedToCompile = true; this.isCompiled = true; + this.__spLifetime = {}; + this.__spFrames = [{__spPC: Number(this.topBlock), __spSlots: [], __spArgs: [], __spReturn: -1}]; + }`)); + } + // Remove now-unused top-level require bindings before module resolution. + traverse(tree, {Program(p) { + p.scope.crawl(); + for (const binding of Object.values(p.scope.bindings)) { + if (binding.referenced || ['SpecializedBlockUtility', 'Cast', 'log', 'execute'].includes(binding.identifier.name) || !binding.path.isVariableDeclarator()) continue; + if (binding.path.node.init && t.isCallExpression(binding.path.node.init) && + t.isIdentifier(binding.path.node.init.callee, {name: 'require'})) binding.path.remove(); + } + }}); + return generate(tree, {comments: false}).code; +} + +const stub = `const unavailable = () => { throw new Error('This player contains no dynamic compiler'); }; +unavailable.saveGlobalState = () => {}; unavailable.restoreGlobalState = () => {}; +export default unavailable;`; +const aliases = { + 'scratch-vm': path.join(vmSource, 'index.js'), + 'scratch-render': path.join(root, 'node_modules/scratch-render/src/index.js'), + 'scratch-audio': path.join(root, 'node_modules/scratch-audio/src/index.js'), + '@turbowarp/scratch-storage': path.join(root, 'node_modules/@turbowarp/scratch-storage/src/index.js'), + '@turbowarp/jszip': path.join(root, 'node_modules/@turbowarp/jszip/lib/index.js'), + '@turbowarp/scratch-svg-renderer': path.join(root, 'node_modules/@turbowarp/scratch-svg-renderer/src/index.js'), + 'transformation-matrix': path.join(root, 'node_modules/transformation-matrix/src/index.js'), + '@vernier/godirect': path.join(vm, 'node_modules/@vernier/godirect/src/godirect.js'), + 'scratch-parser': path.join(sourceRoot, 'scaffolding/scratch-parser/index.js'), + 'text-encoding': path.join(sourceRoot, 'scaffolding/text-encoding/index.js'), + 'htmlparser2': path.join(sourceRoot, 'scaffolding/htmlparser2/index.js'), + 'scratch-translate-extension-languages': path.join(sourceRoot, 'scaffolding/scratch-translate-extension-languages/languages.json'), + events: require.resolve('events/'), + buffer: require.resolve('buffer/'), + util: require.resolve('util/'), + stream: require.resolve('stream-browserify'), + crypto: require.resolve('crypto-browserify'), + assert: require.resolve('assert/'), + process: require.resolve('process/browser') +}; + +const bundle = await rollup({ + input: ['\0specialized-entry', '\0specialized-extension-api'], + treeshake: false, + onwarn(warning) { + if (['CIRCULAR_DEPENDENCY', 'THIS_IS_UNDEFINED'].includes(warning.code)) return; + console.warn(warning.message); + }, + plugins: [{ + name: 'prepared-runtime', + resolveId(source, importer) { + if (source.startsWith('\0specialized')) return source; + if (source === 'twgl.js') return '\0specialized-twgl:9.js'; + if (source === 'setimmediate') return '\0specialized-immediate'; + if (aliases[source]) return aliases[source]; + if (source.startsWith('!ify-loader!')) return require.resolve(source.slice('!ify-loader!'.length)); + if (/^!?(worker-loader|raw-loader|arraybuffer-loader|base64-loader)[!?]/.test(source)) { + return `\0asset:${path.resolve(path.dirname(importer), source.slice(source.lastIndexOf('!') + 1))}:${source.includes('raw-loader') ? 'raw' : source.includes('arraybuffer-loader') ? 'bytes' : source.includes('base64-loader') ? 'base64' : 'worker'}`; + } + if (source === 'fs' || source === 'path' || source === 'http' || source === 'https') return '\0specialized-empty'; + }, + async load(id) { + if (id.startsWith('\0specialized-twgl:')) return nativeModules(twglModules[parseInt(id.split(':')[1], 10)], id); + // Production players run in a browser, including desktop WebViews. ZIP + // resources never expose Node streams and the supported engines have Promise. + if (id.endsWith('/@turbowarp/jszip/lib/external.js')) return 'module.exports = {Promise};'; + if (id === '\0specialized-extension-api') return ` + import Cast from ${JSON.stringify(path.join(vmSource, 'util/cast.js'))}; + ${['ArgumentType:argument-type', 'BlockType:block-type', 'BlockShape:tw-block-shape', 'TargetType:target-type'].map(item => { + const [name, file] = item.split(':'); + return `import ${name} from ${JSON.stringify(path.join(vmSource, `extension-support/${file}.js`))};`; + }).join('\n')} + export default {Cast, ArgumentType, BlockType, BlockShape, TargetType};`; + if (id.endsWith('/scaffolding/scratch-libraries.js')) return ` + export {default as VM} from 'scratch-vm'; + export {default as Renderer} from 'scratch-render'; + export {default as ScratchStorage} from '@turbowarp/scratch-storage'; + export {default as AudioEngine} from 'scratch-audio'; + export {default as BitmapAdapter} from ${JSON.stringify(path.join(root, 'node_modules/@turbowarp/scratch-svg-renderer/src/bitmap-adapter.js'))};`; + if (id === '\0specialized-empty') return 'export default {};'; + if (id === '\0specialized-immediate') return `if (!globalThis.setImmediate) { + globalThis.setImmediate = (callback, ...args) => { + if (typeof callback !== 'function') throw new TypeError('setImmediate requires a function'); + return setTimeout(callback, 0, ...args); + }; globalThis.clearImmediate = clearTimeout; + } export default {};`; + if (id.endsWith('/extension-support/tw-iframe-extension-worker.js')) return `export default class Unavailable { constructor() { throw new Error('Only worker extensions are supported'); } }`; + if (id.endsWith('/extension-support/tw-unsandboxed-extension-runner.js')) return `export default {load() { throw new Error('Unsandboxed extensions are not supported'); }};`; + if (id.endsWith('/addons/unsafe-cloud-behaviors.js')) return `export default function () { throw new Error('Dynamic cloud code is not supported'); }`; + if (id === path.join(vmSource, 'index.js')) return `export {default} from './virtual-machine.js';`; + if (id === '\0specialized-entry') return `import * as Scaffolding from ${JSON.stringify(path.join(sourceRoot, 'scaffolding/scaffolding.js'))}; + import * as ScaffoldingAddons from ${JSON.stringify(path.join(sourceRoot, 'addons/index.js'))}; + export {Scaffolding, ScaffoldingAddons};`; + if (/\/compiler\/(compile|jsexecute|specialized-project)(\.js)?$/.test(id)) return stub; + if (id.endsWith('/engine/execute.js')) return `export default function execute(sequencer, thread) { + if (!thread.updateMonitor) throw new Error('No interpreter in specialized player'); + const runtime = sequencer.runtime; + const block = runtime.monitorBlocks.getBlock(thread.topBlock); + if (!block) { sequencer.retireThread(thread); return; } + const args = {}; + for (const [key, field] of Object.entries(block.fields || {})) args[key] = key === 'VARIABLE' || key === 'LIST' ? field : field.value; + const utility = {target: thread.target, runtime, thread, + ioQuery: (device, operation, args = []) => runtime.ioDevices[device][operation](...args)}; + const value = runtime.__spPrimitive(block.opcode, args, utility); + if (value !== undefined) runtime.requestUpdateMonitor({id: thread.topBlock, value, + spriteName: block.targetId ? thread.target.getName() : null}); + sequencer.retireThread(thread); + }`; + if (id.startsWith('\0asset:')) { + const [, filename, kind] = id.split(':'); + if (kind === 'worker') return `export default class WorkerUnavailable { constructor() { throw new Error('Extension worker source was not linked'); } }`; + const bytes = await fs.readFile(filename); + if (kind === 'raw') return `export default ${JSON.stringify(bytes.toString())};`; + if (kind === 'base64') return `export default ${JSON.stringify(bytes.toString('base64'))};`; + return `export default Uint8Array.from(atob(${JSON.stringify(bytes.toString('base64'))}), c => c.charCodeAt(0)).buffer;`; + } + if (/\.(svg|png|jpg|mp3|wav)$/.test(id)) { + const bytes = await fs.readFile(id); + if (id.includes('/scratch-storage/') || id.includes('/@turbowarp/scratch-storage/')) { + return `export default Uint8Array.from(atob(${JSON.stringify(bytes.toString('base64'))}), c => c.charCodeAt(0));`; + } + const ext = path.extname(id).slice(1); + const type = {svg: 'image/svg+xml', png: 'image/png', jpg: 'image/jpeg', mp3: 'audio/mpeg', wav: 'audio/wav'}[ext]; + return `export default ${JSON.stringify(`data:${type};base64,${bytes.toString('base64')}`)};`; + } + if (id.endsWith('.css')) { + const css = await fs.readFile(id, 'utf8'); + const classes = {}; + const transformed = css.replace(/\.([a-zA-Z_][\w-]*)/g, (_, name) => { + classes[name.replace(/-([a-z])/g, (_, char) => char.toUpperCase())] = `sc-${name}`; + return `.sc-${name}`; + }); + return `const style = document.createElement('style'); style.textContent = ${JSON.stringify(transformed)}; + document.head.appendChild(style); export default ${JSON.stringify(classes)};`; + } + }, + async transform(code, id) { + if (id.endsWith('/@turbowarp/jszip/lib/support.js')) { + const tree = ast(code); + tree.program.body = tree.program.body.filter(node => !t.isTryStatement(node)); + tree.program.body.push(...ast('exports.nodestream = false;').program.body); + code = generate(tree, {comments: false}).code; + } + if (id.endsWith('/@turbowarp/jszip/lib/stream/StreamHelper.js')) { + const tree = ast(code); + tree.program.body = tree.program.body.filter(node => !(t.isIfStatement(node) && + t.isMemberExpression(node.test) && t.isIdentifier(node.test.object, {name: 'support'}) && + t.isIdentifier(node.test.property, {name: 'nodestream'}))); + code = generate(tree, {comments: false}).code; + } + if (/\.[cm]?js$/.test(id)) { + const globals = new Set(); + const tree = ast(code); + traverse(tree, {ReferencedIdentifier(p) { + if (['global', 'process', 'Buffer'].includes(p.node.name) && !p.scope.hasBinding(p.node.name)) globals.add(p.node.name); + }}); + code = [...globals].map(name => name === 'global' ? 'const global = globalThis;' : + name === 'Buffer' ? "const Buffer = require('buffer').Buffer;" : "const process = require('process');").join('\n') + '\n' + code; + } + if (/node_modules\/js-md5\/src\/md5.js$/.test(id)) { + code = code.replace(/var NODE_JS = [^;]+;/, 'var NODE_JS = false;'); + return {code, map: null}; + } + const textUtility = /node_modules\/(grapheme-breaker|linebreak)\/(?!node_modules\/).*\.js$/.test(id); + if (textUtility) { + const matches = [...code.matchAll(/fs\.readFileSync\(__dirname \+ '([^']+)'(?:, '([^']+)')?\)/g)]; + for (const match of matches) { + const bytes = await fs.readFile(path.join(path.dirname(id), match[1])); + code = code.replace(match[0], match[2] === 'base64' ? JSON.stringify(bytes.toString('base64')) : + `require('buffer').Buffer.from(${JSON.stringify(bytes.toString('base64'))}, 'base64')`); + } + // CoffeeScript's outer one-shot closure is only a module scope. Unwrap + // it before converting its exports; nested implementation closures stay. + const tree = ast(code); + const statement = tree.program.body.at(-1); + const call = statement?.expression; + if (t.isCallExpression(call) && t.isMemberExpression(call.callee) && + t.isFunctionExpression(call.callee.object) && t.isIdentifier(call.callee.property, {name: 'call'}) && + call.arguments.length === 1 && t.isThisExpression(call.arguments[0])) { + tree.program.body.splice(-1, 1, ...call.callee.object.body.body); + } + code = generate(tree, {comments: false}).code; + } + const acyclicUtility = textUtility || /\/node_modules\/(pako|tiny-inflate|base64-js|unicode-trie)\//.test(id); + if (!id.startsWith(vmSource) && !id.startsWith(sourceRoot) && !acyclicUtility && !nativeDependencies.some(prefix => id.startsWith(prefix))) return {code, map: null}; + if (!/\.[cm]?js$/.test(id)) return; + if (id.endsWith('/addons/index.js')) code = code.replace('window.ScaffoldingAddons = {\n run\n};', 'export {run};'); + if (id.startsWith(vmSource)) code = prepareVM(code, id); + code = code.replace(/process\.env\.NODE_ENV/g, '"production"'); + const result = nativeModules(code, id); + return {code: result, map: null}; + } + }, nodeResolve({browser: true, preferBuiltins: false, mainFields: ['browser', 'module', 'main']}), + json(), commonjs({strictRequires: [...auditedWrappers.keys()], transformMixedEsModules: true, requireReturnsDefault: 'auto'})] +}); +const generated = await bundle.generate({format: 'es', preserveModules: true, entryFileNames: '[name].js', + chunkFileNames: '[name]-[hash].js', generatedCode: 'es2015'}); +let entry, extensionApi; +const wrapperAudit = {}; +for (const item of generated.output) { + if (item.type !== 'chunk') throw new Error(`Unexpected source-pack asset ${item.fileName}`); + sources[item.fileName] = item.code; + provenance[item.fileName] = Object.keys(item.modules).map(id => id.startsWith('\0specialized-twgl:') ? + `tw-packager/node_modules/twgl.js/dist/4.x/twgl-full.js#module-${parseInt(id.split(':')[1], 10)}` : + id.replace(vm, 'scratch-vm').replace(root, 'tw-packager')); + if (item.exports.includes('__require')) { + const audits = Object.keys(item.modules).map(id => auditedWrappers.get(id)).filter(Boolean); + if (audits.length !== 1) throw new Error(`Unaudited CommonJS wrapper: ${item.fileName}`); + wrapperAudit[item.fileName] = audits[0]; + } + if (item.facadeModuleId === '\0specialized-entry') entry = item.fileName; + if (item.facadeModuleId === '\0specialized-extension-api') extensionApi = item.fileName; +} +// Share the existing scalar semantics, while excluding generator/execution helpers. +const helperTree = ast(await fs.readFile(path.join(vmSource, 'compiler/jsexecute.js'), 'utf8')); +const helperParts = []; +const excluded = new Set(['startHats', 'waitThreads', 'executeInCompatibilityLayer', 'retire', + 'timer', 'distance', 'yieldThenCall', 'yieldThenCallGenerator', 'createBranchInfo']); +traverse(helperTree, {AssignmentExpression(p) { + const {left, right} = p.node; + if (!t.isTemplateLiteral(right) || right.expressions.length) return; + if (t.isIdentifier(left, {name: 'baseRuntime'}) || + (t.isMemberExpression(left) && t.isIdentifier(left.object, {name: 'runtimeFunctions'}) && !excluded.has(left.property.name))) { + helperParts.push(right.quasis[0].value.cooked); + } +}}); +const helperAST = ast(helperParts.join('\n')); +helperAST.program.body = helperAST.program.body.filter(n => !t.isVariableDeclaration(n) || + !n.declarations.some(d => ['stuckCounter', 'isStuck', 'colorToList'].includes(d.id.name))); +helperAST.program.body = helperAST.program.body.map(n => t.isVariableDeclaration(n) ? t.exportNamedDeclaration(n) : n); +const helpers = generate(helperAST, {comments: false}).code; +const fingerprint = createHash('sha256').update(JSON.stringify({sources, helpers})).digest('hex'); +await fs.mkdir(path.dirname(output), {recursive: true}); +await fs.writeFile(output, JSON.stringify({version: 1, fingerprint, entry, extensionApi, sources, provenance, helpers, wrapperAudit})); +await bundle.close(); +console.log(`Prepared ${Object.keys(sources).length} runtime modules (${fingerprint.slice(0, 12)})`); diff --git a/scripts/unbundle-twgl.mjs b/scripts/unbundle-twgl.mjs new file mode 100644 index 0000000..38be5e9 --- /dev/null +++ b/scripts/unbundle-twgl.mjs @@ -0,0 +1,39 @@ +import {createHash} from 'node:crypto'; +import {parse} from '@babel/parser'; +import traverseModule from '@babel/traverse'; +import generateModule from '@babel/generator'; +import * as t from '@babel/types'; + +const traverse = traverseModule.default; +const generate = generateModule.default; + +// twgl 4.4.0's npm package only ships a webpack bundle. Its 15 modules have +// an acyclic static graph; the webpack cache/factories add no required ordering. +// Fail closed when upstream changes instead of guessing another bundle format. +export function unbundleTWGL(code) { + if (createHash('sha256').update(code).digest('hex') !== + '60a924fb69acaba30567e9bd349890aa27b74664e4015c44634838c016191e8c') { + throw new Error('TWGL source changed; review its module graph before updating the source pack'); + } + let functions; + traverse(parse(code), {CallExpression(p) { + const array = p.node.arguments[0]; + if (t.isArrayExpression(array) && array.elements.length === 15 && + array.elements.every(item => t.isFunctionExpression(item))) functions = array.elements; + }}); + if (!functions) throw new Error('Missing audited TWGL module table'); + return functions.map(fn => { + const tree = t.file(t.program(fn.body.body)); + traverse(tree, {CallExpression(p) { + if (!t.isIdentifier(p.node.callee, {name: '__webpack_require__'})) return; + const [id] = p.node.arguments; + if (p.node.arguments.length !== 1 || !t.isNumericLiteral(id) || id.value < 0 || id.value >= 15) { + throw new Error('Non-static TWGL dependency'); + } + p.replaceWith(t.callExpression(t.identifier('require'), [t.stringLiteral(`\0specialized-twgl:${id.value}.js`)])); + }}); + const result = generate(tree, {comments: false}).code; + if (result.includes('__webpack_require__')) throw new Error('Unconverted TWGL bootstrap reference'); + return result; + }); +} diff --git a/src/addons/gamepad/userscript.js b/src/addons/gamepad/userscript.js index bb21dcc..487a9c5 100644 --- a/src/addons/gamepad/userscript.js +++ b/src/addons/gamepad/userscript.js @@ -22,6 +22,8 @@ export default async function (scaffolding, pointerlock) { vm.runtime.once("PROJECT_LOADED", resolve); }); + if (scaffolding._disposed) return; + const vmStarted = () => vm.runtime._steppingInterval !== null; const scratchKeyToKey = (key) => { @@ -98,6 +100,10 @@ export default async function (scaffolding, pointerlock) { GamepadLib.setConsole(console); const gamepad = new GamepadLib(); + scaffolding._listeners.push(() => { + gamepad.removeEventHandlers(); + cancelAnimationFrame(gamepad.animationFrame); + }); const parsedOptions = parseOptionsComment(); gamepad.getUserHints = () => { @@ -140,6 +146,7 @@ export default async function (scaffolding, pointerlock) { const virtualCursorHide = () => { virtualCursorSetVisible(false); }; + scaffolding._listeners.push(() => { clearTimeout(hideCursorTimeout); showRealCursor(); virtualCursorElement.remove(); }); const virtualCursorSetDown = (down) => { virtualCursorSetVisible(true); virtualCursorElement.classList.toggle("sa-gamepad-cursor-down", down); @@ -152,7 +159,7 @@ export default async function (scaffolding, pointerlock) { virtualCursorElement.style.transform = `translate(${stageX}px, ${stageY}px)`; }; - document.addEventListener("mousemove", () => { + scaffolding._listen(document, "mousemove", () => { virtualCursorSetVisible(false); showRealCursor(); }); @@ -169,6 +176,7 @@ export default async function (scaffolding, pointerlock) { } }); resizeObserver.observe(canvas); + scaffolding._listeners.push(() => resizeObserver.disconnect()); getCanvasSize = () => [canvasWidth, canvasHeight]; } else { getCanvasSize = () => { diff --git a/src/addons/index.js b/src/addons/index.js index 70aff21..d6c4f42 100644 --- a/src/addons/index.js +++ b/src/addons/index.js @@ -1,7 +1,6 @@ import gamepad from './gamepad'; import pointerlock from './pointerlock'; import specialCloudBehaviors from './special-cloud-behaviors'; -import unsafeCloudBehaviors from './unsafe-cloud-behaviors'; import pause from './pause'; const run = (scaffolding, options) => { @@ -13,7 +12,6 @@ const run = (scaffolding, options) => { if (options.gamepad) gamepad(api); if (options.pointerlock) pointerlock(api); if (options.specialCloudBehaviors) specialCloudBehaviors(api); - if (options.unsafeCloudBehaviors) unsafeCloudBehaviors(api); if (options.pause) pause(api); }; diff --git a/src/addons/pause.js b/src/addons/pause.js index 8d03804..80544f5 100644 --- a/src/addons/pause.js +++ b/src/addons/pause.js @@ -17,7 +17,7 @@ export default function ({ scaffolding }) { if (paused) { audioContextStateChange = audioContextStateChange.then(() => { - return vm.runtime.audioEngine.audioContext.suspend(); + return vm.runtime.audioEngine && vm.runtime.audioEngine.audioContext.suspend(); }); if (!vm.runtime.ioDevices.clock._paused) { vm.runtime.ioDevices.clock.pause(); @@ -40,7 +40,7 @@ export default function ({ scaffolding }) { vm.runtime.emit('RUNTIME_PAUSED'); } else { audioContextStateChange = audioContextStateChange.then(() => { - return vm.runtime.audioEngine.audioContext.resume(); + return vm.runtime.audioEngine && vm.runtime.audioEngine.audioContext.resume(); }); vm.runtime.ioDevices.clock.resume(); @@ -125,4 +125,4 @@ export default function ({ scaffolding }) { vm.setPaused = setPaused; vm.isPaused = () => paused; -} \ No newline at end of file +} diff --git a/src/addons/pointerlock.js b/src/addons/pointerlock.js index f38de5c..e00d9a0 100644 --- a/src/addons/pointerlock.js +++ b/src/addons/pointerlock.js @@ -1,6 +1,7 @@ const run = ({ scaffolding }) => { const canvas = scaffolding._canvas; const vm = scaffolding.vm; + scaffolding._listeners.push(() => { if (document.pointerLockElement === canvas) document.exitPointerLock(); }); const mouse = vm.runtime.ioDevices.mouse; let isLocked = false; @@ -21,7 +22,7 @@ const run = ({ scaffolding }) => { vm.postIOData('mouse', data); } }; - document.addEventListener("mousedown", (e) => { + scaffolding._listen(document, "mousedown", (e) => { if (canvas.contains(e.target)) { e.stopPropagation(); if (isLocked) { @@ -31,7 +32,7 @@ const run = ({ scaffolding }) => { } } }, true); - document.addEventListener("mouseup", (e) => { + scaffolding._listen(document, "mouseup", (e) => { e.stopPropagation(); if (isLocked) { postMouseData(e, false); @@ -39,23 +40,23 @@ const run = ({ scaffolding }) => { canvas.requestPointerLock(); } }, true); - document.addEventListener("mousemove", (e) => { + scaffolding._listen(document, "mousemove", (e) => { e.stopPropagation(); if (isLocked) { postMouseData(e); } }, true); - scaffolding.addEventListener('PROJECT_RUN_START', () => { + scaffolding._listen(scaffolding, 'PROJECT_RUN_START', () => { if (!isLocked) { canvas.requestPointerLock(); } }); - document.addEventListener('pointerlockchange', () => { + scaffolding._listen(document, 'pointerlockchange', () => { isLocked = document.pointerLockElement === canvas; }); - document.addEventListener('pointerlockerror', (e) => { + scaffolding._listen(document, 'pointerlockerror', (e) => { console.error('Pointer lock error', e); }); diff --git a/src/addons/unsafe-cloud-behaviors.js b/src/addons/unsafe-cloud-behaviors.js deleted file mode 100644 index 48eaae3..0000000 --- a/src/addons/unsafe-cloud-behaviors.js +++ /dev/null @@ -1,52 +0,0 @@ -const isPromise = (v) => !!v && typeof v.then === 'function'; - -const jsValueToScratchValue = (v) => { - if (typeof v === 'boolean' || typeof v === 'number' || typeof v === 'string') { - return v; - } - return '' + v; -}; - -class UnsafeCloudBehaviorsProvider { - enable () { - - } - - setEvalValue (value) { - this.manager.setVariable(this, '☁ eval output', jsValueToScratchValue(value)); - } - - setEvalError (error) { - console.error('Error evaluating ☁ eval', error); - this.manager.setVariable(this, '☁ eval error', jsValueToScratchValue(error)); - } - - evaluateAsync (js) { - try { - const value = eval(js); - if (isPromise(value)) { - value - .then((v) => this.setEvalValue(v)) - .catch((e) => this.setEvalError(e)); - } else { - this.setEvalValue(value) - } - } catch (e) { - this.setEvalError(e); - } - } - - handleUpdateVariable (name, value) { - if (name === '☁ eval') { - this.evaluateAsync(value); - } - } -} - -export default function ({ scaffolding }) { - const provider = new UnsafeCloudBehaviorsProvider(); - scaffolding.addCloudProvider(provider); - scaffolding.addCloudProviderOverride('☁ eval', provider); - scaffolding.addCloudProviderOverride('☁ eval output', provider); - scaffolding.addCloudProviderOverride('☁ eval error', provider); -} diff --git a/src/build/bytes-loader.js b/src/build/bytes-loader.js new file mode 100644 index 0000000..7ed7a4e --- /dev/null +++ b/src/build/bytes-loader.js @@ -0,0 +1,4 @@ +module.exports = function (bytes) { + return `module.exports = Uint8Array.from(atob(${JSON.stringify(bytes.toString('base64'))}), c => c.charCodeAt(0));`; +}; +module.exports.raw = true; diff --git a/src/build/generate-scaffolding-build-id.js b/src/build/generate-scaffolding-build-id.js deleted file mode 100644 index 59f992d..0000000 --- a/src/build/generate-scaffolding-build-id.js +++ /dev/null @@ -1,31 +0,0 @@ -const fs = require('fs'); -const crypto = require('crypto'); -const glob = require('glob'); -const path = require('path'); - -const hash = crypto.createHash('sha256'); - -const getAllFiles = (g) => glob.sync(g, { - cwd: root -}); - -const root = path.join(__dirname, '..', '..'); -const files = [ - __filename, - ...getAllFiles('./src/scaffolding/**/*'), - ...getAllFiles('./src/addons/**/*'), - ...getAllFiles('./src/common/**/*'), - path.join(root, 'webpack.config.js'), - path.join(root, 'package.json'), - path.join(root, 'package-lock.json') -]; -for (const file of files) { - const stat = fs.statSync(file); - if (!stat.isDirectory()) { - hash.update(fs.readFileSync(file, 'utf-8')); - } -} - -const hex = hash.digest('hex'); -console.log('Scaffolding build ID: ' + hex); -module.exports = hex; diff --git a/src/build/generate-service-worker-plugin.js b/src/build/generate-service-worker-plugin.js index 32aa7e2..46fa32d 100644 --- a/src/build/generate-service-worker-plugin.js +++ b/src/build/generate-service-worker-plugin.js @@ -19,7 +19,8 @@ class GenerateServiceWorkerPlugin { const assetNames = Array.from(allAssetNames) .filter((name) => { if (name.endsWith('.map')) return false; - return name.startsWith('assets/') || name.startsWith('js/') || CACHE_PAGES.includes(name); + return name.startsWith('assets/') || name.startsWith('js/') || CACHE_PAGES.includes(name) || + ['specialized-worker.js', 'rollup.browser.js', 'bindings_wasm_bg.wasm'].includes(name); }); const stringifiedAssets = JSON.stringify(assetNames); const hash = crypto.createHash('sha256').update(stringifiedAssets).digest('hex'); diff --git a/src/build/generate-standalone.js b/src/build/generate-standalone.js index da7a399..1d8f478 100644 --- a/src/build/generate-standalone.js +++ b/src/build/generate-standalone.js @@ -1,18 +1,12 @@ const pathUtil = require('path'); const fs = require('fs'); -const glob = require('glob'); const dist = pathUtil.join(__dirname, '..', '..', 'dist'); console.log(`dist: ${dist}`); -const scaffoldingFiles = glob.sync('scaffolding/*.js', { - cwd: dist -}); -console.log(`scaffolding: ${scaffoldingFiles.join(', ')}`); -const scaffoldingAssets = {}; -for (const path of scaffoldingFiles) { - if (path.includes('extension-worker')) continue; - scaffoldingAssets[path] = fs.readFileSync(pathUtil.join(dist, path), 'utf-8'); +const buildAssets = {}; +for (const name of ['specialized-worker.js', 'rollup.browser.js', 'bindings_wasm_bg.wasm']) { + buildAssets[name] = fs.readFileSync(pathUtil.join(dist, name)).toString('base64'); } const indexPath = pathUtil.join(dist, 'index.html'); @@ -29,13 +23,14 @@ console.log(`favicon.ico: ${faviconData ? faviconPath : 'none'}`); const makeSafeForInlineScript = (content) => content.replace(/<\/script>/g, '\\u003c/script>'); let standaloneJS = ''; -standaloneJS += Object.entries(scaffoldingAssets).map(([name, content]) => ( - `` +standaloneJS += Object.entries(buildAssets).map(([name, content]) => ( + `` )).join(''); standaloneJS += ``; diff --git a/src/build/global-self-loader.js b/src/build/global-self-loader.js deleted file mode 100644 index 6af8f43..0000000 --- a/src/build/global-self-loader.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = source => `var self = typeof globalThis === 'undefined' ? this : globalThis;\n${source}`; diff --git a/src/common/errors.js b/src/common/errors.js index 49c216f..7919f68 100644 --- a/src/common/errors.js +++ b/src/common/errors.js @@ -29,16 +29,6 @@ export class UnknownNetworkError extends Error { } } -/** - * Error indicating the packager is outdated and must be updated before it can continue. - */ -export class OutdatedPackagerError extends Error { - constructor (message) { - super(message); - this.name = 'OutdatedPackagerError'; - } -} - /** * Error indicating an HTTP status error. */ diff --git a/src/locales/en.json b/src/locales/en.json index c28bfd2..6029c19 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -418,99 +418,12 @@ "enableCompiler": { "string": "Enable Compiler" }, - "aotCompile": { - "string": "Enable compiled project mode" - }, - "compiledProtection": { - "string": "Compiled Project Protection" - }, - "compiledProtectionDescription": { - "string": "A featured build mode of SB3C Packager: compile the block graph ahead of time, optionally replace VM member accesses, and apply additional JavaScript transformations." - }, - "obfuscateCompiledProject": { - "string": "Obfuscate compiled project" - }, - "javascriptObfuscator": { - "string": "Apply JavaScript Obfuscator to compiled factories" - }, - "javascriptObfuscatorOptions": { - "string": "JavaScript Obfuscator options" - }, - "compactOutput": { - "string": "Compact output" - }, "simplifyCode": { "string": "Simplify and transform expressions" }, - "identifierNamesGenerator": { - "string": "Identifier style" - }, - "stringArray": { - "string": "Move string literals into encoded arrays" - }, - "stringArrayEncoding": { - "string": "String encoding" - }, "encodingNone": { "string": "No additional encoding" }, - "transformationThreshold": { - "string": "Transformation probability (0–1)" - }, - "controlFlowFlattening": { - "string": "Control-flow flattening" - }, - "deadCodeInjection": { - "string": "Dead-code injection" - }, - "splitStrings": { - "string": "Split string literals" - }, - "splitStringsChunkLength": { - "string": "String chunk length" - }, - "numbersToExpressions": { - "string": "Convert numbers to expressions" - }, - "transformObjectKeys": { - "string": "Transform object keys" - }, - "unicodeEscapeSequence": { - "string": "Use Unicode escape sequences" - }, - "disableConsoleOutput": { - "string": "Disable console output" - }, - "selfDefending": { - "string": "Self-defending output" - }, - "selfDefendingWarning": { - "string": "Self-defending output must not be reformatted or minified again. It increases output size and can make compatibility failures harder to diagnose." - }, - "debugProtection": { - "string": "Developer Tools debug protection" - }, - "debugProtectionInterval": { - "string": "Repeat interval in milliseconds (0 means once)" - }, - "debugProtectionGlobal": { - "string": "The anti-debugging guard is generated once as a global player script instead of being duplicated in every compiled procedure." - }, - "debugProtectionWarning": { - "string": "Debug protection can freeze or severely disrupt browser Developer Tools, especially with a repeating interval. Use 2000–4000 ms only when necessary and test the packaged project carefully." - }, - "javascriptObfuscatorPerformance": { - "string": "Control-flow flattening, dead-code injection, RC4 strings, and self-defending output can substantially increase build time, file size, startup time, and runtime overhead." - }, - "obfuscationSeed": { - "string": "Obfuscation seed" - }, - "derivedFromProject": { - "string": "Derived from project when empty" - }, - "aotCompileWarning": { - "string": "Compilation and obfuscation only raise the cost of casual reading, copying, and automated analysis. They do not encrypt or completely hide the code: the player must receive executable JavaScript and a determined user can inspect, instrument, or modify it. Do not embed secrets, private keys, or security decisions in the project." - }, "warpTimer": { "string": "Warp Timer" }, @@ -527,9 +440,7 @@ "customExtensions": { "string": "Custom extensions (one URL per line)" }, - "customExtensionsSecurity": { - "string": "Custom extensions are always run unsandboxed in the packager. Depending on the environment you're packaging for, this grants extensions full control over your computer, including the ability to install viruses. Don't use extensions you don't trust." - }, + "customExtensionsSecurity": "Extension code and dependencies are frozen at build time and run in an isolated Worker. Extensions requiring full VM access are unsupported.", "bakeExtensions": { "string": "Try to embed cached copy of custom extensions instead of downloading them each time the project is run", "developer_comment": "Option that tries to store " @@ -726,6 +637,18 @@ "downloadsWillAppearHere": { "string": "Downloads will appear here", "developer_comment": "Placeholder that appears when no project has been packaged yet so there is nothing to download" + }, + "specializedProtection": { + "string": "Program-specific player" + }, + "specializedProtectionDescription": { + "string": "Project scripts and the runtime are compiled together into one offline player." + }, + "buildSeed": { + "string": "Build seed" + }, + "randomBuildSeed": { + "string": "Random for each build when empty" } }, "downloads": { diff --git a/src/locales/zh-cn.json b/src/locales/zh-cn.json index b9901fd..826cac4 100644 --- a/src/locales/zh-cn.json +++ b/src/locales/zh-cn.json @@ -51,7 +51,7 @@ "customCSS": "自定义 CSS(如果你不确定你在做什么,别动它)", "customCursor": "自定义指针", "customExtensions": "自定义扩展(每行一个 URL)", - "customExtensionsSecurity": "自定义扩展总是在打包程序中未被隔离地运行。根据您打包的环境,这将授予扩展对计算机的完全控制权,包括安装病毒的能力。不要使用不信任的扩展。", + "customExtensionsSecurity": "扩展及代码依赖在构建时固定,并在独立 Worker 中运行。不支持需要访问完整 VM 的扩展。", "customJS": "自定义 JS (如果你不确定你在做什么,别动它)", "customUsernameWarning": "如果您修改了默认用户名,请小心,因为它可能造成与默认云变量服务器的连接被阻止。只有默认用户名是确保可用的。", "defaultControls": "默认", @@ -142,40 +142,15 @@ "version": "版本", "versionHelp": "更新作品版本是完全可选的,不会影响作品的。根据环境的不同,它可能会出现在关于页面等地方。格式应该像 X.Y.Z 这样", "warpTimer": "循环计时器", - "aotCompile": "打包前编译项目", - "compiledProtection": "编译项目保护(特色功能)", - "compiledProtectionDescription": "SB3C Packager 的特色构建模式:预先编译积木图,并可替换 VM 成员访问、继续进行 JavaScript 变换。", - "obfuscateCompiledProject": "混淆编译项目", - "javascriptObfuscator": "对编译后的 factory 使用 JavaScript Obfuscator", - "javascriptObfuscatorOptions": "JavaScript Obfuscator 选项", - "compactOutput": "紧凑输出", "simplifyCode": "简化并变换表达式", - "identifierNamesGenerator": "标识符样式", - "stringArray": "将字符串字面量移入编码数组", - "stringArrayEncoding": "字符串编码", "encodingNone": "不额外编码", - "transformationThreshold": "变换概率(0–1)", - "controlFlowFlattening": "控制流平坦化", - "deadCodeInjection": "死代码注入", - "splitStrings": "拆分字符串字面量", - "splitStringsChunkLength": "字符串分块长度", - "numbersToExpressions": "将数字转换为表达式", - "transformObjectKeys": "变换对象键", - "unicodeEscapeSequence": "使用 Unicode 转义序列", - "disableConsoleOutput": "禁用控制台输出", - "selfDefending": "自我保护输出", - "selfDefendingWarning": "自我保护输出不能再次格式化或压缩;它会增大产物,也会让兼容性问题更难诊断。", - "debugProtection": "开发者工具防调试", - "debugProtectionInterval": "重复间隔(毫秒,0 表示只执行一次)", - "debugProtectionGlobal": "防调试守卫只生成一份全局播放器脚本,不会重复注入每个编译入口和自定义过程。", - "debugProtectionWarning": "防调试可能冻结或严重干扰浏览器开发者工具,定时重复时风险更高。仅在必要时使用 2000–4000 毫秒,并充分测试打包结果。", - "javascriptObfuscatorPerformance": "控制流平坦化、死代码注入、RC4 字符串和自我保护可能显著增加构建时间、文件大小、启动时间和运行开销。", - "obfuscationSeed": "混淆种子", - "derivedFromProject": "留空时从项目派生", - "aotCompileWarning": "编译和混淆只能提高直接阅读、复制及自动分析的成本,并不是加密,也无法完全隐藏代码:播放器必须获得可执行 JavaScript,有意分析者仍可检查、插桩或修改它。不要把密钥、私钥或安全判断写进项目。", "windowControls": "窗口控制栏", "zip": "Zip 压缩包(适合嵌入网站)", - "zip-one-asset": "Zip 压缩包,把所有素材打包在一个文件中(不推荐)" + "zip-one-asset": "Zip 压缩包,把所有素材打包在一个文件中(不推荐)", + "specializedProtection": "程序专用化保护", + "specializedProtectionDescription": "作品脚本与运行时联合编译,生成专用于此作品的离线播放器。", + "buildSeed": "构建 seed", + "randomBuildSeed": "留空时每次构建随机生成" }, "p4": { "browserNotSupported": "不支持浏览器", diff --git a/src/locales/zh-tw.json b/src/locales/zh-tw.json index cee702d..5e515b4 100644 --- a/src/locales/zh-tw.json +++ b/src/locales/zh-tw.json @@ -83,7 +83,12 @@ "username": "用戶名稱(以「#」代表一個隨機數)", "warpTimer": "防卡死", "zip": "Zip(用於網站的最佳選擇)", - "zip-one-asset": "Zip,封裝所有素材(不推薦)" + "zip-one-asset": "Zip,封裝所有素材(不推薦)", + "specializedProtection": "程式專用化保護", + "specializedProtectionDescription": "作品腳本與執行環境聯合編譯,產生此作品專用的離線播放器。", + "buildSeed": "建置 seed", + "randomBuildSeed": "每次建置隨機產生", + "customExtensionsSecurity": "擴充功能及程式碼相依套件會在建置時固定,並在獨立 Worker 中執行。不支援需要存取完整 VM 的擴充功能。" }, "p4": { "close": "關閉", @@ -129,4 +134,4 @@ "light": "明亮主題", "system": "使用系統主題" } -} \ No newline at end of file +} diff --git a/src/p4/Modals.svelte b/src/p4/Modals.svelte index 1968b03..aebeb26 100644 --- a/src/p4/Modals.svelte +++ b/src/p4/Modals.svelte @@ -1,7 +1,7 @@