From ddc7ff3827e1b37662706e15dc191369a9c834a4 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Sun, 10 May 2026 16:39:26 +0800 Subject: [PATCH 01/30] :wrench: chore(vm): convret util to be ts Signed-off-by: SimonShiki --- packages/vm/package.json | 2 + packages/vm/src/blocks/category_prototype.ts | 20 +++ packages/vm/src/blocks/scratch3_control.js | 2 +- packages/vm/src/blocks/scratch3_data.js | 2 +- packages/vm/src/blocks/scratch3_event.js | 2 +- packages/vm/src/blocks/scratch3_looks.js | 10 +- packages/vm/src/blocks/scratch3_motion.js | 6 +- packages/vm/src/blocks/scratch3_operators.js | 4 +- packages/vm/src/blocks/scratch3_sensing.js | 8 +- packages/vm/src/blocks/scratch3_sound.js | 6 +- packages/vm/src/dispatch/central-dispatch.js | 2 +- packages/vm/src/dispatch/shared-dispatch.js | 2 +- packages/vm/src/dispatch/worker-dispatch.js | 2 +- packages/vm/src/engine/adapter.js | 2 +- packages/vm/src/engine/block-utility.js | 2 +- packages/vm/src/engine/blocks.js | 8 +- packages/vm/src/engine/comment.js | 4 +- packages/vm/src/engine/execute.js | 4 +- packages/vm/src/engine/runtime.js | 12 +- packages/vm/src/engine/sequencer.js | 2 +- packages/vm/src/engine/target.js | 8 +- packages/vm/src/engine/variable.js | 4 +- .../extension-support/extension-manager.js | 4 +- .../vm/src/extensions/scratch3_boost/index.js | 10 +- .../vm/src/extensions/scratch3_ev3/index.js | 12 +- .../src/extensions/scratch3_gdx_for/index.js | 4 +- .../scratch-link-device-adapter.js | 2 +- .../extensions/scratch3_makeymakey/index.js | 2 +- .../src/extensions/scratch3_microbit/index.js | 6 +- .../vm/src/extensions/scratch3_music/index.js | 8 +- .../vm/src/extensions/scratch3_pen/index.js | 8 +- .../extensions/scratch3_speech2text/index.js | 4 +- .../extensions/scratch3_text2speech/index.js | 10 +- .../extensions/scratch3_translate/index.js | 6 +- .../scratch3_video_sensing/index.js | 4 +- .../vm/src/extensions/scratch3_wedo2/index.js | 10 +- packages/vm/src/import/load-costume.js | 4 +- packages/vm/src/import/load-sound.js | 4 +- packages/vm/src/io/ble.js | 2 +- packages/vm/src/io/bt.js | 2 +- packages/vm/src/io/clock.js | 2 +- packages/vm/src/io/cloud.js | 2 +- packages/vm/src/io/keyboard.js | 2 +- packages/vm/src/io/mouse.js | 2 +- .../src/serialization/deserialize-assets.js | 2 +- packages/vm/src/serialization/migration.js | 2 +- packages/vm/src/serialization/sb2.js | 8 +- packages/vm/src/serialization/sb3.js | 10 +- packages/vm/src/sprites/rendered-target.js | 8 +- packages/vm/src/sprites/sprite.js | 4 +- .../util/{base64-util.js => base64-util.ts} | 22 +-- packages/vm/src/util/{cast.js => cast.ts} | 84 +++++------ packages/vm/src/util/{clone.js => clone.ts} | 7 +- packages/vm/src/util/color.ts | 4 +- ...-with-timeout.js => fetch-with-timeout.ts} | 16 +-- .../{get-monitor-id.js => get-monitor-id.ts} | 8 +- packages/vm/src/util/jsonrpc.js | 114 --------------- packages/vm/src/util/jsonrpc.ts | 135 ++++++++++++++++++ packages/vm/src/util/{log.js => log.ts} | 0 .../src/util/{math-util.js => math-util.ts} | 68 ++++----- packages/vm/src/util/maybe-format-message.js | 18 --- packages/vm/src/util/maybe-format-message.ts | 18 +++ .../{new-block-ids.js => new-block-ids.ts} | 30 ++-- .../util/{rateLimiter.js => rateLimiter.ts} | 21 +-- ...websocket.js => scratch-link-websocket.ts} | 60 ++++---- .../util/{string-util.js => string-util.ts} | 29 ++-- .../src/util/{task-queue.js => task-queue.ts} | 95 ++++++------ packages/vm/src/util/{timer.js => timer.ts} | 43 +++--- packages/vm/src/util/{uid.js => uid.ts} | 4 +- .../{variable-util.js => variable-util.ts} | 20 +-- .../src/util/{xml-escape.js => xml-escape.ts} | 9 +- packages/vm/src/virtual-machine.js | 10 +- .../vm/test/fixtures/dispatch-test-worker.js | 2 +- .../broadcast_special_chars_sb2.js | 4 +- .../broadcast_special_chars_sb3.js | 4 +- packages/vm/test/integration/sb3-roundtrip.js | 2 +- .../integration/variable_special_chars_sb2.js | 4 +- .../integration/variable_special_chars_sb3.js | 4 +- .../vm/test/unit/extension_video_sensing.js | 2 +- packages/vm/test/unit/maybe_format_message.js | 2 +- packages/vm/test/unit/mock-timer.js | 2 +- packages/vm/test/unit/util_cast.js | 2 +- packages/vm/test/unit/util_math.js | 2 +- packages/vm/test/unit/util_new-block-ids.js | 2 +- packages/vm/test/unit/util_rateLimiter.js | 2 +- packages/vm/test/unit/util_string.js | 2 +- packages/vm/test/unit/util_task-queue.js | 4 +- packages/vm/test/unit/util_timer.js | 2 +- packages/vm/test/unit/util_variable.js | 2 +- packages/vm/test/unit/util_xml.js | 2 +- pnpm-lock.yaml | 25 +++- 91 files changed, 621 insertions(+), 523 deletions(-) create mode 100644 packages/vm/src/blocks/category_prototype.ts rename packages/vm/src/util/{base64-util.js => base64-util.ts} (61%) rename packages/vm/src/util/{cast.js => cast.ts} (71%) rename packages/vm/src/util/{clone.js => clone.ts} (65%) rename packages/vm/src/util/{fetch-with-timeout.js => fetch-with-timeout.ts} (55%) rename packages/vm/src/util/{get-monitor-id.js => get-monitor-id.ts} (82%) delete mode 100644 packages/vm/src/util/jsonrpc.js create mode 100644 packages/vm/src/util/jsonrpc.ts rename packages/vm/src/util/{log.js => log.ts} (100%) rename packages/vm/src/util/{math-util.js => math-util.ts} (57%) delete mode 100644 packages/vm/src/util/maybe-format-message.js create mode 100644 packages/vm/src/util/maybe-format-message.ts rename packages/vm/src/util/{new-block-ids.js => new-block-ids.ts} (57%) rename packages/vm/src/util/{rateLimiter.js => rateLimiter.ts} (86%) rename packages/vm/src/util/{scratch-link-websocket.js => scratch-link-websocket.ts} (69%) rename packages/vm/src/util/{string-util.js => string-util.ts} (74%) rename packages/vm/src/util/{task-queue.js => task-queue.ts} (70%) rename packages/vm/src/util/{timer.js => timer.ts} (73%) rename packages/vm/src/util/{uid.js => uid.ts} (90%) rename packages/vm/src/util/{variable-util.js => variable-util.ts} (54%) rename packages/vm/src/util/{xml-escape.js => xml-escape.ts} (79%) diff --git a/packages/vm/package.json b/packages/vm/package.json index 6cd7e701..8a7f8cf4 100644 --- a/packages/vm/package.json +++ b/packages/vm/package.json @@ -57,6 +57,8 @@ "@babel/eslint-parser": "7.28.6", "@babel/preset-env": "7.29.2", "@babel/preset-typescript": "^7.28.5", + "@types/atob": "^2.1.4", + "@types/btoa": "^1.2.5", "@types/fastestsmallesttextencoderdecoder": "^1.0.2", "@types/node": "^25.5.2", "adm-zip": "0.4.11", diff --git a/packages/vm/src/blocks/category_prototype.ts b/packages/vm/src/blocks/category_prototype.ts new file mode 100644 index 00000000..2db9dc94 --- /dev/null +++ b/packages/vm/src/blocks/category_prototype.ts @@ -0,0 +1,20 @@ +import type Runtime from '../engine/runtime'; +import type BlockUtility from '../engine/block-utility'; +import type {HatMetadata, MonitorBlockInfo} from '../engine/runtime'; + +export type BlockFunction = (args: { + [argName: string]: any; + mutation?: Record; +}, util: BlockUtility) => any; + +export interface CategoryPrototype { + new(runtime: Runtime): void; + /** + * Retrieve the block primitives implemented by this package. + * @returns {Record} Mapping of opcode to Function. + */ + getPrimitives(): Record; + getHats?(): Record; + getMonitored?(): Record; + getOrders?(): Record; +} diff --git a/packages/vm/src/blocks/scratch3_control.js b/packages/vm/src/blocks/scratch3_control.js index db7d5fd1..d94eb06f 100644 --- a/packages/vm/src/blocks/scratch3_control.js +++ b/packages/vm/src/blocks/scratch3_control.js @@ -1,4 +1,4 @@ -import Cast from '../util/cast.js'; +import Cast from '../util/cast'; class Scratch3ControlBlocks { constructor (runtime) { diff --git a/packages/vm/src/blocks/scratch3_data.js b/packages/vm/src/blocks/scratch3_data.js index 81e14f05..adbc6d4c 100644 --- a/packages/vm/src/blocks/scratch3_data.js +++ b/packages/vm/src/blocks/scratch3_data.js @@ -1,4 +1,4 @@ -import Cast from '../util/cast.js'; +import Cast from '../util/cast'; class Scratch3DataBlocks { constructor (runtime) { diff --git a/packages/vm/src/blocks/scratch3_event.js b/packages/vm/src/blocks/scratch3_event.js index c32ef79c..d3fe56f9 100644 --- a/packages/vm/src/blocks/scratch3_event.js +++ b/packages/vm/src/blocks/scratch3_event.js @@ -1,4 +1,4 @@ -import Cast from '../util/cast.js'; +import Cast from '../util/cast'; class Scratch3EventBlocks { constructor (runtime) { diff --git a/packages/vm/src/blocks/scratch3_looks.js b/packages/vm/src/blocks/scratch3_looks.js index 7a84e72d..5b47a036 100644 --- a/packages/vm/src/blocks/scratch3_looks.js +++ b/packages/vm/src/blocks/scratch3_looks.js @@ -1,10 +1,10 @@ -import Cast from '../util/cast.js'; -import Clone from '../util/clone.js'; +import Cast from '../util/cast'; +import Clone from '../util/clone'; import RenderedTarget from '../sprites/rendered-target.js'; -import uid from '../util/uid.js'; +import uid from '../util/uid'; import StageLayering from '../engine/stage-layering.js'; -import getMonitorIdForBlockWithArgs from '../util/get-monitor-id.js'; -import MathUtil from '../util/math-util.js'; +import getMonitorIdForBlockWithArgs from '../util/get-monitor-id'; +import MathUtil from '../util/math-util'; /** * @typedef {object} BubbleState - the bubble state associated with a particular target. diff --git a/packages/vm/src/blocks/scratch3_motion.js b/packages/vm/src/blocks/scratch3_motion.js index ebfa0b65..19a9362b 100644 --- a/packages/vm/src/blocks/scratch3_motion.js +++ b/packages/vm/src/blocks/scratch3_motion.js @@ -1,6 +1,6 @@ -import Cast from '../util/cast.js'; -import MathUtil from '../util/math-util.js'; -import Timer from '../util/timer.js'; +import Cast from '../util/cast'; +import MathUtil from '../util/math-util'; +import Timer from '../util/timer'; class Scratch3MotionBlocks { constructor (runtime) { diff --git a/packages/vm/src/blocks/scratch3_operators.js b/packages/vm/src/blocks/scratch3_operators.js index a19b82e4..538b88b9 100644 --- a/packages/vm/src/blocks/scratch3_operators.js +++ b/packages/vm/src/blocks/scratch3_operators.js @@ -1,5 +1,5 @@ -import Cast from '../util/cast.js'; -import MathUtil from '../util/math-util.js'; +import Cast from '../util/cast'; +import MathUtil from '../util/math-util'; class Scratch3OperatorsBlocks { constructor (runtime) { diff --git a/packages/vm/src/blocks/scratch3_sensing.js b/packages/vm/src/blocks/scratch3_sensing.js index c5110650..8bd241ad 100644 --- a/packages/vm/src/blocks/scratch3_sensing.js +++ b/packages/vm/src/blocks/scratch3_sensing.js @@ -1,8 +1,8 @@ -import Cast from '../util/cast.js'; +import Cast from '../util/cast'; import Color from '../util/color'; -import MathUtil from '../util/math-util.js'; -import Timer from '../util/timer.js'; -import getMonitorIdForBlockWithArgs from '../util/get-monitor-id.js'; +import MathUtil from '../util/math-util'; +import Timer from '../util/timer'; +import getMonitorIdForBlockWithArgs from '../util/get-monitor-id'; class Scratch3SensingBlocks { constructor (runtime) { diff --git a/packages/vm/src/blocks/scratch3_sound.js b/packages/vm/src/blocks/scratch3_sound.js index 5b10e451..20f75be0 100644 --- a/packages/vm/src/blocks/scratch3_sound.js +++ b/packages/vm/src/blocks/scratch3_sound.js @@ -1,6 +1,6 @@ -import MathUtil from '../util/math-util.js'; -import Cast from '../util/cast.js'; -import Clone from '../util/clone.js'; +import MathUtil from '../util/math-util'; +import Cast from '../util/cast'; +import Clone from '../util/clone'; /** * Occluded boolean value to make its use more understandable. diff --git a/packages/vm/src/dispatch/central-dispatch.js b/packages/vm/src/dispatch/central-dispatch.js index add863df..cb1ff9d9 100644 --- a/packages/vm/src/dispatch/central-dispatch.js +++ b/packages/vm/src/dispatch/central-dispatch.js @@ -1,5 +1,5 @@ import SharedDispatch from './shared-dispatch.js'; -import log from '../util/log.js'; +import log from '../util/log'; /** * This class serves as the central broker for message dispatch. It expects to operate on the main thread / Window and diff --git a/packages/vm/src/dispatch/shared-dispatch.js b/packages/vm/src/dispatch/shared-dispatch.js index 1776d2f5..d98fc2be 100644 --- a/packages/vm/src/dispatch/shared-dispatch.js +++ b/packages/vm/src/dispatch/shared-dispatch.js @@ -1,4 +1,4 @@ -import log from '../util/log.js'; +import log from '../util/log'; /** * @typedef {object} DispatchCallMessage - a message to the dispatch system representing a service method call diff --git a/packages/vm/src/dispatch/worker-dispatch.js b/packages/vm/src/dispatch/worker-dispatch.js index 20bcbcf9..83533b83 100644 --- a/packages/vm/src/dispatch/worker-dispatch.js +++ b/packages/vm/src/dispatch/worker-dispatch.js @@ -1,5 +1,5 @@ import SharedDispatch from './shared-dispatch.js'; -import log from '../util/log.js'; +import log from '../util/log'; /** * This class provides a Worker with the means to participate in the message dispatch system managed by CentralDispatch. diff --git a/packages/vm/src/engine/adapter.js b/packages/vm/src/engine/adapter.js index 24350dd9..9d373e17 100644 --- a/packages/vm/src/engine/adapter.js +++ b/packages/vm/src/engine/adapter.js @@ -1,6 +1,6 @@ import mutationAdapter from './mutation-adapter.js'; import * as html from 'htmlparser2'; -import uid from '../util/uid.js'; +import uid from '../util/uid'; /** * @import * as Blockly from 'blockly'; diff --git a/packages/vm/src/engine/block-utility.js b/packages/vm/src/engine/block-utility.js index c1de1682..92952d03 100644 --- a/packages/vm/src/engine/block-utility.js +++ b/packages/vm/src/engine/block-utility.js @@ -1,5 +1,5 @@ import Thread from './thread.js'; -import Timer from '../util/timer.js'; +import Timer from '../util/timer'; /** * @fileoverview diff --git a/packages/vm/src/engine/blocks.js b/packages/vm/src/engine/blocks.js index a670c407..aaea63ab 100644 --- a/packages/vm/src/engine/blocks.js +++ b/packages/vm/src/engine/blocks.js @@ -1,11 +1,11 @@ import adapter from './adapter.js'; -import xmlEscape from '../util/xml-escape.js'; +import xmlEscape from '../util/xml-escape'; import MonitorRecord from './monitor-record.js'; -import Clone from '../util/clone.js'; +import Clone from '../util/clone'; import {Map} from 'immutable'; -import log from '../util/log.js'; +import log from '../util/log'; import Variable from './variable.js'; -import getMonitorIdForBlockWithArgs from '../util/get-monitor-id.js'; +import getMonitorIdForBlockWithArgs from '../util/get-monitor-id'; /** * @fileoverview diff --git a/packages/vm/src/engine/comment.js b/packages/vm/src/engine/comment.js index 620c42e5..b980a9a5 100644 --- a/packages/vm/src/engine/comment.js +++ b/packages/vm/src/engine/comment.js @@ -3,9 +3,9 @@ * Object representing a Scratch Comment (block or workspace). */ -import uid from '../util/uid.js'; +import uid from '../util/uid'; -import xmlEscape from '../util/xml-escape.js'; +import xmlEscape from '../util/xml-escape'; class Comment { /** diff --git a/packages/vm/src/engine/execute.js b/packages/vm/src/engine/execute.js index c94267bd..a7bb0211 100644 --- a/packages/vm/src/engine/execute.js +++ b/packages/vm/src/engine/execute.js @@ -1,9 +1,9 @@ import BlockUtility from './block-utility.js'; import {getCached as getCachedExecuteBlock} from './blocks-execute-cache.js'; -import log from '../util/log.js'; +import log from '../util/log'; import Thread from './thread.js'; import {Map} from 'immutable'; -import cast from '../util/cast.js'; +import cast from '../util/cast'; /** * Single BlockUtility instance reused by execute for every pritimive ran. diff --git a/packages/vm/src/engine/runtime.js b/packages/vm/src/engine/runtime.js index c51a80e8..039facee 100644 --- a/packages/vm/src/engine/runtime.js +++ b/packages/vm/src/engine/runtime.js @@ -10,12 +10,12 @@ import execute from './execute.js'; import ScratchBlocksConstants from './scratch-blocks-constants.js'; import TargetType from '../extension-support/target-type'; import Thread from './thread.js'; -import log from '../util/log.js'; -import maybeFormatMessage from '../util/maybe-format-message.js'; +import log from '../util/log'; +import maybeFormatMessage from '../util/maybe-format-message'; import StageLayering from './stage-layering.js'; import Variable from './variable.js'; -import xmlEscape from '../util/xml-escape.js'; -import ScratchLinkWebSocket from '../util/scratch-link-websocket.js'; +import xmlEscape from '../util/xml-escape'; +import ScratchLinkWebSocket from '../util/scratch-link-websocket'; import Clock from '../io/clock.js'; @@ -26,8 +26,8 @@ import MouseWheel from '../io/mouseWheel.js'; import UserData from '../io/userData.js'; import Video from '../io/video.js'; import Joystick from '../io/joystick.js'; -import StringUtil from '../util/string-util.js'; -import uid from '../util/uid.js'; +import StringUtil from '../util/string-util'; +import uid from '../util/uid'; import control from '../blocks/scratch3_control.js'; import event from '../blocks/scratch3_event.js'; import looks from '../blocks/scratch3_looks.js'; diff --git a/packages/vm/src/engine/sequencer.js b/packages/vm/src/engine/sequencer.js index 73d05b4e..7a9dd086 100644 --- a/packages/vm/src/engine/sequencer.js +++ b/packages/vm/src/engine/sequencer.js @@ -1,4 +1,4 @@ -import Timer from '../util/timer.js'; +import Timer from '../util/timer'; import Thread from './thread.js'; import execute from './execute.js'; diff --git a/packages/vm/src/engine/target.js b/packages/vm/src/engine/target.js index 80f91a61..e6fb1f84 100644 --- a/packages/vm/src/engine/target.js +++ b/packages/vm/src/engine/target.js @@ -2,11 +2,11 @@ import EventEmitter from 'events'; import Blocks from './blocks.js'; import Variable from '../engine/variable.js'; import Comment from '../engine/comment.js'; -import uid from '../util/uid.js'; +import uid from '../util/uid'; import {Map} from 'immutable'; -import log from '../util/log.js'; -import StringUtil from '../util/string-util.js'; -import VariableUtil from '../util/variable-util.js'; +import log from '../util/log'; +import StringUtil from '../util/string-util'; +import VariableUtil from '../util/variable-util'; /** * @typedef {import('./runtime')} Runtime diff --git a/packages/vm/src/engine/variable.js b/packages/vm/src/engine/variable.js index 5e834bc0..081b0259 100644 --- a/packages/vm/src/engine/variable.js +++ b/packages/vm/src/engine/variable.js @@ -3,9 +3,9 @@ * Object representing a Scratch variable. */ -import uid from '../util/uid.js'; +import uid from '../util/uid'; -import xmlEscape from '../util/xml-escape.js'; +import xmlEscape from '../util/xml-escape'; class Variable { /** diff --git a/packages/vm/src/extension-support/extension-manager.js b/packages/vm/src/extension-support/extension-manager.js index 235fa735..a64918b0 100644 --- a/packages/vm/src/extension-support/extension-manager.js +++ b/packages/vm/src/extension-support/extension-manager.js @@ -1,6 +1,6 @@ import dispatch from '../dispatch/central-dispatch.js'; -import log from '../util/log.js'; -import maybeFormatMessage from '../util/maybe-format-message.js'; +import log from '../util/log'; +import maybeFormatMessage from '../util/maybe-format-message'; import BlockType from './block-type'; // These extensions are currently built into the VM repository but should not be loaded at startup. diff --git a/packages/vm/src/extensions/scratch3_boost/index.js b/packages/vm/src/extensions/scratch3_boost/index.js index 236247d1..18bbd8fc 100644 --- a/packages/vm/src/extensions/scratch3_boost/index.js +++ b/packages/vm/src/extensions/scratch3_boost/index.js @@ -1,13 +1,13 @@ import ArgumentType from '../../extension-support/argument-type'; import BlockType from '../../extension-support/block-type'; -import Cast from '../../util/cast.js'; +import Cast from '../../util/cast'; import formatMessage from 'format-message'; import color from '../../util/color'; import BLE from '../../io/ble.js'; -import Base64Util from '../../util/base64-util.js'; -import MathUtil from '../../util/math-util.js'; -import RateLimiter from '../../util/rateLimiter.js'; -import log from '../../util/log.js'; +import Base64Util from '../../util/base64-util'; +import MathUtil from '../../util/math-util'; +import RateLimiter from '../../util/rateLimiter'; +import log from '../../util/log'; /** * The LEGO Wireless Protocol documentation used to create this extension can be found at: diff --git a/packages/vm/src/extensions/scratch3_ev3/index.js b/packages/vm/src/extensions/scratch3_ev3/index.js index 1a347e4f..91b0e5c5 100644 --- a/packages/vm/src/extensions/scratch3_ev3/index.js +++ b/packages/vm/src/extensions/scratch3_ev3/index.js @@ -1,13 +1,13 @@ import ArgumentType from '../../extension-support/argument-type'; import BlockType from '../../extension-support/block-type'; -import Cast from '../../util/cast.js'; +import Cast from '../../util/cast'; import formatMessage from 'format-message'; -import uid from '../../util/uid.js'; +import uid from '../../util/uid'; import BT from '../../io/bt.js'; -import Base64Util from '../../util/base64-util.js'; -import MathUtil from '../../util/math-util.js'; -import RateLimiter from '../../util/rateLimiter.js'; -import log from '../../util/log.js'; +import Base64Util from '../../util/base64-util'; +import MathUtil from '../../util/math-util'; +import RateLimiter from '../../util/rateLimiter'; +import log from '../../util/log'; /** * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. diff --git a/packages/vm/src/extensions/scratch3_gdx_for/index.js b/packages/vm/src/extensions/scratch3_gdx_for/index.js index 37ba6b44..67243967 100644 --- a/packages/vm/src/extensions/scratch3_gdx_for/index.js +++ b/packages/vm/src/extensions/scratch3_gdx_for/index.js @@ -1,8 +1,8 @@ import ArgumentType from '../../extension-support/argument-type'; import BlockType from '../../extension-support/block-type'; -import log from '../../util/log.js'; +import log from '../../util/log'; import formatMessage from 'format-message'; -import MathUtil from '../../util/math-util.js'; +import MathUtil from '../../util/math-util'; import BLE from '../../io/ble.js'; import godirect from '@vernier/godirect/dist/godirect.min.umd.js'; import ScratchLinkDeviceAdapter from './scratch-link-device-adapter.js'; diff --git a/packages/vm/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js b/packages/vm/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js index 2e61d3b8..434abc9f 100644 --- a/packages/vm/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js +++ b/packages/vm/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js @@ -1,4 +1,4 @@ -import Base64Util from '../../util/base64-util.js'; +import Base64Util from '../../util/base64-util'; /** * Adapter class diff --git a/packages/vm/src/extensions/scratch3_makeymakey/index.js b/packages/vm/src/extensions/scratch3_makeymakey/index.js index 7441f0b4..32d8d7c6 100644 --- a/packages/vm/src/extensions/scratch3_makeymakey/index.js +++ b/packages/vm/src/extensions/scratch3_makeymakey/index.js @@ -1,7 +1,7 @@ import formatMessage from 'format-message'; import ArgumentType from '../../extension-support/argument-type'; import BlockType from '../../extension-support/block-type'; -import Cast from '../../util/cast.js'; +import Cast from '../../util/cast'; /** * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. diff --git a/packages/vm/src/extensions/scratch3_microbit/index.js b/packages/vm/src/extensions/scratch3_microbit/index.js index dbb083a1..42be2f6a 100644 --- a/packages/vm/src/extensions/scratch3_microbit/index.js +++ b/packages/vm/src/extensions/scratch3_microbit/index.js @@ -1,10 +1,10 @@ import ArgumentType from '../../extension-support/argument-type'; import BlockType from '../../extension-support/block-type'; -import log from '../../util/log.js'; -import cast from '../../util/cast.js'; +import log from '../../util/log'; +import cast from '../../util/cast'; import formatMessage from 'format-message'; import BLE from '../../io/ble.js'; -import Base64Util from '../../util/base64-util.js'; +import Base64Util from '../../util/base64-util'; /** * Icon png to be displayed at the left edge of each extension block, encoded as a data URI. diff --git a/packages/vm/src/extensions/scratch3_music/index.js b/packages/vm/src/extensions/scratch3_music/index.js index 4a2384a4..a28e71bd 100644 --- a/packages/vm/src/extensions/scratch3_music/index.js +++ b/packages/vm/src/extensions/scratch3_music/index.js @@ -1,10 +1,10 @@ import ArgumentType from '../../extension-support/argument-type'; import BlockType from '../../extension-support/block-type'; -import Clone from '../../util/clone.js'; -import Cast from '../../util/cast.js'; +import Clone from '../../util/clone'; +import Cast from '../../util/cast'; import formatMessage from 'format-message'; -import MathUtil from '../../util/math-util.js'; -import Timer from '../../util/timer.js'; +import MathUtil from '../../util/math-util'; +import Timer from '../../util/timer'; /** * The instrument and drum sounds, loaded as static assets. diff --git a/packages/vm/src/extensions/scratch3_pen/index.js b/packages/vm/src/extensions/scratch3_pen/index.js index d60d35e6..c2a7fbd3 100644 --- a/packages/vm/src/extensions/scratch3_pen/index.js +++ b/packages/vm/src/extensions/scratch3_pen/index.js @@ -1,13 +1,13 @@ import ArgumentType from '../../extension-support/argument-type'; import BlockType from '../../extension-support/block-type'; import TargetType from '../../extension-support/target-type'; -import Cast from '../../util/cast.js'; -import Clone from '../../util/clone.js'; +import Cast from '../../util/cast'; +import Clone from '../../util/clone'; import Color from '../../util/color'; import formatMessage from 'format-message'; -import MathUtil from '../../util/math-util.js'; +import MathUtil from '../../util/math-util'; import RenderedTarget from '../../sprites/rendered-target.js'; -import log from '../../util/log.js'; +import log from '../../util/log'; import StageLayering from '../../engine/stage-layering.js'; /** diff --git a/packages/vm/src/extensions/scratch3_speech2text/index.js b/packages/vm/src/extensions/scratch3_speech2text/index.js index 57645868..72a0d6ed 100644 --- a/packages/vm/src/extensions/scratch3_speech2text/index.js +++ b/packages/vm/src/extensions/scratch3_speech2text/index.js @@ -1,8 +1,8 @@ import ArgumentType from '../../extension-support/argument-type'; -import Cast from '../../util/cast.js'; +import Cast from '../../util/cast'; import BlockType from '../../extension-support/block-type'; import formatMessage from 'format-message'; -import log from '../../util/log.js'; +import log from '../../util/log'; import DiffMatchPatch from 'diff-match-patch'; diff --git a/packages/vm/src/extensions/scratch3_text2speech/index.js b/packages/vm/src/extensions/scratch3_text2speech/index.js index 0755d019..a54873b3 100644 --- a/packages/vm/src/extensions/scratch3_text2speech/index.js +++ b/packages/vm/src/extensions/scratch3_text2speech/index.js @@ -2,11 +2,11 @@ import formatMessage from 'format-message'; import languageNames from 'scratch-translate-extension-languages'; import ArgumentType from '../../extension-support/argument-type'; import BlockType from '../../extension-support/block-type'; -import Cast from '../../util/cast.js'; -import MathUtil from '../../util/math-util.js'; -import Clone from '../../util/clone.js'; -import log from '../../util/log.js'; -import fetchWithTimeout from '../../util/fetch-with-timeout.js'; +import Cast from '../../util/cast'; +import MathUtil from '../../util/math-util'; +import Clone from '../../util/clone'; +import log from '../../util/log'; +import fetchWithTimeout from '../../util/fetch-with-timeout'; /** * Icon svg to be displayed in the blocks category menu, encoded as a data URI. diff --git a/packages/vm/src/extensions/scratch3_translate/index.js b/packages/vm/src/extensions/scratch3_translate/index.js index 9840625f..e07488e8 100644 --- a/packages/vm/src/extensions/scratch3_translate/index.js +++ b/packages/vm/src/extensions/scratch3_translate/index.js @@ -1,8 +1,8 @@ import ArgumentType from '../../extension-support/argument-type'; import BlockType from '../../extension-support/block-type'; -import Cast from '../../util/cast.js'; -import log from '../../util/log.js'; -import fetchWithTimeout from '../../util/fetch-with-timeout.js'; +import Cast from '../../util/cast'; +import log from '../../util/log'; +import fetchWithTimeout from '../../util/fetch-with-timeout'; import languageNames from 'scratch-translate-extension-languages'; import formatMessage from 'format-message'; diff --git a/packages/vm/src/extensions/scratch3_video_sensing/index.js b/packages/vm/src/extensions/scratch3_video_sensing/index.js index 89fd7322..b43091a5 100644 --- a/packages/vm/src/extensions/scratch3_video_sensing/index.js +++ b/packages/vm/src/extensions/scratch3_video_sensing/index.js @@ -1,8 +1,8 @@ import Runtime from '../../engine/runtime.js'; import ArgumentType from '../../extension-support/argument-type'; import BlockType from '../../extension-support/block-type'; -import Clone from '../../util/clone.js'; -import Cast from '../../util/cast.js'; +import Clone from '../../util/clone'; +import Cast from '../../util/cast'; import formatMessage from 'format-message'; import Video from '../../io/video.js'; import VideoMotion from './library.js'; diff --git a/packages/vm/src/extensions/scratch3_wedo2/index.js b/packages/vm/src/extensions/scratch3_wedo2/index.js index 896cc8ff..2557cfc2 100644 --- a/packages/vm/src/extensions/scratch3_wedo2/index.js +++ b/packages/vm/src/extensions/scratch3_wedo2/index.js @@ -1,13 +1,13 @@ import ArgumentType from '../../extension-support/argument-type'; import BlockType from '../../extension-support/block-type'; -import Cast from '../../util/cast.js'; +import Cast from '../../util/cast'; import formatMessage from 'format-message'; import color from '../../util/color'; import BLE from '../../io/ble.js'; -import Base64Util from '../../util/base64-util.js'; -import MathUtil from '../../util/math-util.js'; -import RateLimiter from '../../util/rateLimiter.js'; -import log from '../../util/log.js'; +import Base64Util from '../../util/base64-util'; +import MathUtil from '../../util/math-util'; +import RateLimiter from '../../util/rateLimiter'; +import log from '../../util/log'; /** * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. diff --git a/packages/vm/src/import/load-costume.js b/packages/vm/src/import/load-costume.js index 4968ad80..b0cf93b6 100644 --- a/packages/vm/src/import/load-costume.js +++ b/packages/vm/src/import/load-costume.js @@ -1,5 +1,5 @@ -import StringUtil from '../util/string-util.js'; -import log from '../util/log.js'; +import StringUtil from '../util/string-util'; +import log from '../util/log'; import {loadSvgString, serializeSvgToString} from 'clipcc-svg-renderer'; const loadVector_ = function (costume, runtime, rotationCenter, optVersion) { diff --git a/packages/vm/src/import/load-sound.js b/packages/vm/src/import/load-sound.js index bdd98ba6..9af3779e 100644 --- a/packages/vm/src/import/load-sound.js +++ b/packages/vm/src/import/load-sound.js @@ -1,5 +1,5 @@ -import StringUtil from '../util/string-util.js'; -import log from '../util/log.js'; +import StringUtil from '../util/string-util'; +import log from '../util/log'; /** * Initialize a sound from an asset asynchronously. diff --git a/packages/vm/src/io/ble.js b/packages/vm/src/io/ble.js index 674d32aa..1778f29e 100644 --- a/packages/vm/src/io/ble.js +++ b/packages/vm/src/io/ble.js @@ -1,4 +1,4 @@ -import JSONRPC from '../util/jsonrpc.js'; +import JSONRPC from '../util/jsonrpc'; class BLE extends JSONRPC { diff --git a/packages/vm/src/io/bt.js b/packages/vm/src/io/bt.js index 9a791160..fb906992 100644 --- a/packages/vm/src/io/bt.js +++ b/packages/vm/src/io/bt.js @@ -1,4 +1,4 @@ -import JSONRPC from '../util/jsonrpc.js'; +import JSONRPC from '../util/jsonrpc'; class BT extends JSONRPC { diff --git a/packages/vm/src/io/clock.js b/packages/vm/src/io/clock.js index 31082aa2..6c0e2194 100644 --- a/packages/vm/src/io/clock.js +++ b/packages/vm/src/io/clock.js @@ -1,4 +1,4 @@ -import Timer from '../util/timer.js'; +import Timer from '../util/timer'; class Clock { constructor (runtime) { diff --git a/packages/vm/src/io/cloud.js b/packages/vm/src/io/cloud.js index 82375c84..04979dd1 100644 --- a/packages/vm/src/io/cloud.js +++ b/packages/vm/src/io/cloud.js @@ -1,5 +1,5 @@ import Variable from '../engine/variable.js'; -import log from '../util/log.js'; +import log from '../util/log'; class Cloud { /** diff --git a/packages/vm/src/io/keyboard.js b/packages/vm/src/io/keyboard.js index d633b5eb..0b5a7991 100644 --- a/packages/vm/src/io/keyboard.js +++ b/packages/vm/src/io/keyboard.js @@ -1,4 +1,4 @@ -import Cast from '../util/cast.js'; +import Cast from '../util/cast'; /** * Names used internally for keys used in scratch, also known as "scratch keys". diff --git a/packages/vm/src/io/mouse.js b/packages/vm/src/io/mouse.js index aabcd0c4..91f3886b 100644 --- a/packages/vm/src/io/mouse.js +++ b/packages/vm/src/io/mouse.js @@ -1,4 +1,4 @@ -import MathUtil from '../util/math-util.js'; +import MathUtil from '../util/math-util'; class Mouse { constructor (runtime) { diff --git a/packages/vm/src/serialization/deserialize-assets.js b/packages/vm/src/serialization/deserialize-assets.js index 965df0bc..f8776fb0 100644 --- a/packages/vm/src/serialization/deserialize-assets.js +++ b/packages/vm/src/serialization/deserialize-assets.js @@ -1,5 +1,5 @@ import JSZip from 'jszip'; -import log from '../util/log.js'; +import log from '../util/log'; /** * Deserializes sound from file into storage cache so that it can diff --git a/packages/vm/src/serialization/migration.js b/packages/vm/src/serialization/migration.js index 553d9a75..bc2057be 100644 --- a/packages/vm/src/serialization/migration.js +++ b/packages/vm/src/serialization/migration.js @@ -2,7 +2,7 @@ * @fileoverview * Migration from legacy ClipCC. */ -import log from '../util/log.js'; +import log from '../util/log'; const migrationMap = { procedures_definition_return: { diff --git a/packages/vm/src/serialization/sb2.js b/packages/vm/src/serialization/sb2.js index e2f8404b..77338102 100644 --- a/packages/vm/src/serialization/sb2.js +++ b/packages/vm/src/serialization/sb2.js @@ -14,10 +14,10 @@ import Blocks from '../engine/blocks.js'; import RenderedTarget from '../sprites/rendered-target.js'; import Sprite from '../sprites/sprite.js'; import Color from '../util/color'; -import log from '../util/log.js'; -import uid from '../util/uid.js'; -import StringUtil from '../util/string-util.js'; -import MathUtil from '../util/math-util.js'; +import log from '../util/log'; +import uid from '../util/uid'; +import StringUtil from '../util/string-util'; +import MathUtil from '../util/math-util'; import specMap from './sb2_specmap.js'; import Comment from '../engine/comment.js'; import Variable from '../engine/variable.js'; diff --git a/packages/vm/src/serialization/sb3.js b/packages/vm/src/serialization/sb3.js index 8df3dbdb..7f8167fa 100644 --- a/packages/vm/src/serialization/sb3.js +++ b/packages/vm/src/serialization/sb3.js @@ -12,11 +12,11 @@ import Variable from '../engine/variable.js'; import Comment from '../engine/comment.js'; import MonitorRecord from '../engine/monitor-record.js'; import StageLayering from '../engine/stage-layering.js'; -import log from '../util/log.js'; -import uid from '../util/uid.js'; -import MathUtil from '../util/math-util.js'; -import StringUtil from '../util/string-util.js'; -import VariableUtil from '../util/variable-util.js'; +import log from '../util/log'; +import uid from '../util/uid'; +import MathUtil from '../util/math-util'; +import StringUtil from '../util/string-util'; +import VariableUtil from '../util/variable-util'; import {migrationMap, mergeDeep, migrateMutation} from './migration.js'; import {loadCostume} from '../import/load-costume.js'; import {loadSound} from '../import/load-sound.js'; diff --git a/packages/vm/src/sprites/rendered-target.js b/packages/vm/src/sprites/rendered-target.js index a21a6039..84781030 100644 --- a/packages/vm/src/sprites/rendered-target.js +++ b/packages/vm/src/sprites/rendered-target.js @@ -1,7 +1,7 @@ -import MathUtil from '../util/math-util.js'; -import StringUtil from '../util/string-util.js'; -import Cast from '../util/cast.js'; -import Clone from '../util/clone.js'; +import MathUtil from '../util/math-util'; +import StringUtil from '../util/string-util'; +import Cast from '../util/cast'; +import Clone from '../util/clone'; import Target from '../engine/target.js'; import StageLayering from '../engine/stage-layering.js'; diff --git a/packages/vm/src/sprites/sprite.js b/packages/vm/src/sprites/sprite.js index 14ff2eb4..49cdb5a5 100644 --- a/packages/vm/src/sprites/sprite.js +++ b/packages/vm/src/sprites/sprite.js @@ -2,8 +2,8 @@ import RenderedTarget from './rendered-target.js'; import Blocks from '../engine/blocks.js'; import {loadSoundFromAsset} from '../import/load-sound.js'; import {loadCostumeFromAsset} from '../import/load-costume.js'; -import newBlockIds from '../util/new-block-ids.js'; -import StringUtil from '../util/string-util.js'; +import newBlockIds from '../util/new-block-ids'; +import StringUtil from '../util/string-util'; import StageLayering from '../engine/stage-layering.js'; class Sprite { diff --git a/packages/vm/src/util/base64-util.js b/packages/vm/src/util/base64-util.ts similarity index 61% rename from packages/vm/src/util/base64-util.js rename to packages/vm/src/util/base64-util.ts index 839d858d..89ce26bb 100644 --- a/packages/vm/src/util/base64-util.js +++ b/packages/vm/src/util/base64-util.ts @@ -5,10 +5,10 @@ class Base64Util { /** * Convert a base64 encoded string to a Uint8Array. - * @param {string} base64 - a base64 encoded string. - * @returns {Uint8Array} - a decoded Uint8Array. + * @param base64 - a base64 encoded string. + * @returns - a decoded Uint8Array. */ - static base64ToUint8Array (base64) { + static base64ToUint8Array (base64: string): Uint8Array { const binaryString = atob(base64); const len = binaryString.length; const array = new Uint8Array(len); @@ -20,25 +20,25 @@ class Base64Util { /** * Convert a Uint8Array to a base64 encoded string. - * @param {Uint8Array} array - the array to convert. - * @returns {string} - the base64 encoded string. + * @param array - the array to convert. + * @returns - the base64 encoded string. */ - static uint8ArrayToBase64 (array) { - const base64 = btoa(String.fromCharCode.apply(null, array)); + static uint8ArrayToBase64 (array: Uint8Array): string { + const base64 = btoa(String.fromCharCode.apply(null, array as unknown as number[])); return base64; } /** * Convert an array buffer to a base64 encoded string. - * @param {Array} buffer - an array buffer to convert. - * @returns {string} - the base64 encoded string. + * @param buffer - an array buffer to convert. + * @returns - the base64 encoded string. */ - static arrayBufferToBase64 (buffer) { + static arrayBufferToBase64 (buffer: ArrayBuffer): string { let binary = ''; const bytes = new Uint8Array(buffer); const len = bytes.byteLength; for (let i = 0; i < len; i++) { - binary += String.fromCharCode(bytes[ i ]); + binary += String.fromCharCode(bytes[i]); } return btoa(binary); } diff --git a/packages/vm/src/util/cast.js b/packages/vm/src/util/cast.ts similarity index 71% rename from packages/vm/src/util/cast.js rename to packages/vm/src/util/cast.ts index c719cb81..f7478bf8 100644 --- a/packages/vm/src/util/cast.js +++ b/packages/vm/src/util/cast.ts @@ -1,4 +1,4 @@ -import Color from '../util/color'; +import Color, {type RGBObject} from '../util/color'; /** * @fileoverview @@ -16,10 +16,10 @@ class Cast { * Scratch cast to number. * Treats NaN as 0. * In Scratch 2.0, this is captured by `interp.numArg.` - * @param {*} value Value to cast to number. - * @returns {number} The Scratch-casted number value. + * @param value Value to cast to number. + * @returns The Scratch-casted number value. */ - static toNumber (value) { + static toNumber (value: unknown): number { // If value is already a number we don't need to coerce it with // Number(). if (typeof value === 'number') { @@ -43,10 +43,10 @@ class Cast { * Scratch cast to boolean. * In Scratch 2.0, this is captured by `interp.boolArg.` * Treats some string values differently from JavaScript. - * @param {*} value Value to cast to boolean. - * @returns {boolean} The Scratch-casted boolean value. + * @param value Value to cast to boolean. + * @returns The Scratch-casted boolean value. */ - static toBoolean (value) { + static toBoolean (value: unknown): boolean { // Already a boolean? if (typeof value === 'boolean') { return value; @@ -67,35 +67,39 @@ class Cast { /** * Scratch cast to string. - * @param {*} value Value to cast to string. - * @returns {string} The Scratch-casted string value. + * @param value Value to cast to string. + * @returns The Scratch-casted string value. */ - static toString (value) { + static toString (value: unknown): string { return String(value); } /** * Cast any Scratch argument to an RGB color array to be used for the renderer. - * @param {*} value Value to convert to RGB color array. - * @returns {Array.} [r,g,b], values between 0-255. + * @param value Value to convert to RGB color array. + * @returns [r,g,b], values between 0-255. */ - static toRgbColorList (value) { + static toRgbColorList (value: unknown): number[] { const color = Cast.toRgbColorObject(value); return [color.r, color.g, color.b]; } /** * Cast any Scratch argument to an RGB color object to be used for the renderer. - * @param {*} value Value to convert to RGB color object. - * @returns {RGBOject} [r,g,b], values between 0-255. + * @param value Value to convert to RGB color object. + * @returns [r,g,b], values between 0-255. */ - static toRgbColorObject (value) { - let color; + static toRgbColorObject (value: unknown): RGBObject { + let color: RGBObject; if (typeof value === 'string' && value.substring(0, 1) === '#') { - color = Color.hexToRgb(value); + const hexResult = Color.hexToRgb(value); // If the color wasn't *actually* a hex color, cast to black - if (!color) color = {r: 0, g: 0, b: 0, a: 255}; + if (!hexResult) { + color = {r: 0, g: 0, b: 0, a: 255}; + } else { + color = hexResult; + } } else { color = Color.decimalToRgb(Cast.toNumber(value)); } @@ -104,21 +108,21 @@ class Cast { /** * Determine if a Scratch argument is a white space string (or null / empty). - * @param {*} val value to check. - * @returns {boolean} True if the argument is all white spaces or null / empty. + * @param val value to check. + * @returns True if the argument is all white spaces or null / empty. */ - static isWhiteSpace (val) { + static isWhiteSpace (val: unknown): boolean { return val === null || (typeof val === 'string' && val.trim().length === 0); } /** * Compare two values, using Scratch cast, case-insensitive string compare, etc. * In Scratch 2.0, this is captured by `interp.compare.` - * @param {*} v1 First value to compare. - * @param {*} v2 Second value to compare. - * @returns {number} Negative number if v1 < v2; 0 if equal; positive otherwise. + * @param v1 First value to compare. + * @param v2 Second value to compare. + * @returns Negative number if v1 < v2; 0 if equal; positive otherwise. */ - static compare (v1, v2) { + static compare (v1: unknown, v2: unknown): number { let n1 = Number(v1); let n2 = Number(v2); if (n1 === 0 && Cast.isWhiteSpace(v1)) { @@ -151,17 +155,17 @@ class Cast { /** * Determine if a Scratch argument number represents a round integer. - * @param {*} val Value to check. - * @returns {boolean} True if number looks like an integer. + * @param val Value to check. + * @returns True if number looks like an integer. */ - static isInt (val) { + static isInt (val: unknown): boolean { // Values that are already numbers. if (typeof val === 'number') { if (isNaN(val)) { // NaN is considered an integer. return true; } // True if it's "round" (e.g., 2.0 and 2). - return val === parseInt(val, 10); + return val === parseInt(String(val), 10); } else if (typeof val === 'boolean') { // `True` and `false` always represent integer after Scratch cast. return true; @@ -172,11 +176,11 @@ class Cast { return false; } - static get LIST_INVALID () { + static get LIST_INVALID (): string { return 'INVALID'; } - static get LIST_ALL () { + static get LIST_ALL (): string { return 'ALL'; } @@ -185,12 +189,12 @@ class Cast { * Two special cases may be returned: * LIST_ALL: if the block is referring to all of the items in the list. * LIST_INVALID: if the index was invalid in any way. - * @param {*} index Scratch arg, including 1-based numbers or special cases. - * @param {number} length Length of the list. - * @param {boolean} acceptAll Whether it should accept "all" or not. - * @returns {(number|string)} 1-based index for list, LIST_ALL, or LIST_INVALID. + * @param index Scratch arg, including 1-based numbers or special cases. + * @param length Length of the list. + * @param acceptAll Whether it should accept "all" or not. + * @returns 1-based index for list, LIST_ALL, or LIST_INVALID. */ - static toListIndex (index, length, acceptAll) { + static toListIndex (index: unknown, length: number, acceptAll: boolean): number | string { if (typeof index !== 'number') { if (index === 'all') { return acceptAll ? Cast.LIST_ALL : Cast.LIST_INVALID; @@ -207,11 +211,11 @@ class Cast { return Cast.LIST_INVALID; } } - index = Math.floor(Cast.toNumber(index)); - if (index < 1 || index > length) { + const numericIndex = Math.floor(Cast.toNumber(index)); + if (numericIndex < 1 || numericIndex > length) { return Cast.LIST_INVALID; } - return index; + return numericIndex; } } diff --git a/packages/vm/src/util/clone.js b/packages/vm/src/util/clone.ts similarity index 65% rename from packages/vm/src/util/clone.js rename to packages/vm/src/util/clone.ts index 8b8b4e7a..6583925a 100644 --- a/packages/vm/src/util/clone.js +++ b/packages/vm/src/util/clone.ts @@ -1,15 +1,14 @@ /** * Methods for cloning JavaScript objects. - * @type {object} */ class Clone { /** * Deep-clone a "simple" object: one which can be fully expressed with JSON. * Non-JSON values, such as functions, will be stripped from the clone. - * @param {object} original - the object to be cloned. - * @returns {object} a deep clone of the original object. + * @param original - the object to be cloned. + * @returns a deep clone of the original object. */ - static simple (original) { + static simple (original: T): T { return JSON.parse(JSON.stringify(original)); } } diff --git a/packages/vm/src/util/color.ts b/packages/vm/src/util/color.ts index 54defa7f..e48ec672 100644 --- a/packages/vm/src/util/color.ts +++ b/packages/vm/src/util/color.ts @@ -1,11 +1,11 @@ -interface RGBObject { +export interface RGBObject { r: number; g: number; b: number; a?: number; } -interface HSVObject { +export interface HSVObject { h: number; s: number; v: number; diff --git a/packages/vm/src/util/fetch-with-timeout.js b/packages/vm/src/util/fetch-with-timeout.ts similarity index 55% rename from packages/vm/src/util/fetch-with-timeout.js rename to packages/vm/src/util/fetch-with-timeout.ts index f6ae699d..3eca57f9 100644 --- a/packages/vm/src/util/fetch-with-timeout.js +++ b/packages/vm/src/util/fetch-with-timeout.ts @@ -1,22 +1,22 @@ /** * Fetch a remote resource like `fetch` does, but with a time limit. - * @param {Request|string} resource Remote resource to fetch. - * @param {?object} init An options object containing any custom settings that you want to apply to the request. - * @param {number} timeout The amount of time before the request is canceled, in milliseconds - * @returns {Promise} The response from the server. + * @param resource Remote resource to fetch. + * @param init An options object containing any custom settings that you want to apply to the request. + * @param timeout The amount of time before the request is canceled, in milliseconds + * @returns The response from the server. */ -const fetchWithTimeout = (resource, init, timeout) => { - let timeoutID = null; +const fetchWithTimeout = (resource: RequestInfo | URL, init: RequestInit | null, timeout: number): Promise => { + let timeoutID: ReturnType | null = null; // Not supported in Safari <11 const controller = window.AbortController ? new window.AbortController() : null; const signal = controller ? controller.signal : null; // The fetch call races a timer. return Promise.race([ fetch(resource, Object.assign({signal}, init)).then(response => { - clearTimeout(timeoutID); + clearTimeout(timeoutID!); return response; }), - new Promise((resolve, reject) => { + new Promise((resolve, reject) => { timeoutID = setTimeout(() => { if (controller) controller.abort(); reject(new Error(`Fetch timed out after ${timeout} ms`)); diff --git a/packages/vm/src/util/get-monitor-id.js b/packages/vm/src/util/get-monitor-id.ts similarity index 82% rename from packages/vm/src/util/get-monitor-id.js rename to packages/vm/src/util/get-monitor-id.ts index be3f40e2..0c8cf7f7 100644 --- a/packages/vm/src/util/get-monitor-id.js +++ b/packages/vm/src/util/get-monitor-id.ts @@ -3,14 +3,14 @@ * where a single reporter block can have more than one monitor * (and therefore more than one monitor block) associated * with it (e.g. when reporter blocks have inputs). - * @param {string} id The base id to use for the different monitor blocks - * @param {object} fields The monitor block's fields object. - * @returns {string} A string representing a unique id for a monitored block. + * @param id The base id to use for the different monitor blocks + * @param fields The monitor block's fields object. + * @returns A string representing a unique id for a monitored block. */ // TODO this function should eventually be the single place where all monitor // IDs are obtained given an opcode for the reporter block and the list of // selected parameters. -const getMonitorIdForBlockWithArgs = function (id, fields) { +const getMonitorIdForBlockWithArgs = function (id: string, fields: Record): string { let fieldString = ''; for (const fieldKey in fields) { let fieldValue = fields[fieldKey].value; diff --git a/packages/vm/src/util/jsonrpc.js b/packages/vm/src/util/jsonrpc.js deleted file mode 100644 index d0243f5b..00000000 --- a/packages/vm/src/util/jsonrpc.js +++ /dev/null @@ -1,114 +0,0 @@ -class JSONRPC { - constructor () { - this._requestID = 0; - this._openRequests = {}; - } - - /** - * Make an RPC request and retrieve the result. - * @param {string} method - the remote method to call. - * @param {object} params - the parameters to pass to the remote method. - * @returns {Promise} - a promise for the result of the call. - */ - sendRemoteRequest (method, params) { - const requestID = this._requestID++; - - const promise = new Promise((resolve, reject) => { - this._openRequests[requestID] = {resolve, reject}; - }); - - this._sendRequest(method, params, requestID); - - return promise; - } - - /** - * Make an RPC notification with no expectation of a result or callback. - * @param {string} method - the remote method to call. - * @param {object} params - the parameters to pass to the remote method. - */ - sendRemoteNotification (method, params) { - this._sendRequest(method, params); - } - - /** - * Handle an RPC request from remote, should return a result or Promise for result, if appropriate. - * @param {string} method - the method requested by the remote caller. - * @param {object} params - the parameters sent with the remote caller's request. - */ - didReceiveCall (method, params) { // eslint-disable-line no-unused-vars - throw new Error('Must override didReceiveCall'); - } - - _sendMessage (jsonMessageObject) { // eslint-disable-line no-unused-vars - throw new Error('Must override _sendMessage'); - } - - _sendRequest (method, params, id) { - const request = { - jsonrpc: '2.0', - method, - params - }; - - if (id !== null) { - request.id = id; - } - - this._sendMessage(request); - } - - _handleMessage (json) { - if (json.jsonrpc !== '2.0') { - throw new Error(`Bad or missing JSON-RPC version in message: ${json}`); - } - if (Object.prototype.hasOwnProperty.call(json, 'method')) { - this._handleRequest(json); - } else { - this._handleResponse(json); - } - } - - _sendResponse (id, result, error) { - const response = { - jsonrpc: '2.0', - id - }; - if (error) { - response.error = error; - } else { - response.result = result || null; - } - this._sendMessage(response); - } - - _handleResponse (json) { - const {result, error, id} = json; - const openRequest = this._openRequests[id]; - delete this._openRequests[id]; - if (openRequest) { - if (error) { - openRequest.reject(error); - } else { - openRequest.resolve(result); - } - } - } - - _handleRequest (json) { - const {method, params, id} = json; - const rawResult = this.didReceiveCall(method, params); - if (id !== null && typeof id !== 'undefined') { - Promise.resolve(rawResult).then( - result => { - this._sendResponse(id, result); - }, - error => { - this._sendResponse(id, null, error); - } - ); - } - } -} - -export default JSONRPC; diff --git a/packages/vm/src/util/jsonrpc.ts b/packages/vm/src/util/jsonrpc.ts new file mode 100644 index 00000000..8312ad4f --- /dev/null +++ b/packages/vm/src/util/jsonrpc.ts @@ -0,0 +1,135 @@ +interface OpenRequest { + resolve: (result: unknown) => void; + reject: (error: Error) => void; +} + +interface BaseMessage { + jsonrpc: '2.0'; +} + +interface ResponseMessage extends BaseMessage { + result?: unknown; + error?: unknown; + id: number; +} + +interface RequestMessage extends BaseMessage { + method: string; + params: object; + id?: number | null; +} + +type JSONRPCMessage = ResponseMessage | RequestMessage; + +class JSONRPC { + _requestID = 0; + _openRequests: Record = {}; + + /** + * Make an RPC request and retrieve the result. + * @param method - the remote method to call. + * @param params - the parameters to pass to the remote method. + * @returns - a promise for the result of the call. + */ + sendRemoteRequest (method: string, params: object): Promise { + const requestID = this._requestID++; + + const promise = new Promise((resolve, reject) => { + this._openRequests[requestID] = {resolve, reject}; + }); + + this._sendRequest(method, params, requestID); + + return promise; + } + + /** + * Make an RPC notification with no expectation of a result or callback. + * @param method - the remote method to call. + * @param params - the parameters to pass to the remote method. + */ + sendRemoteNotification (method: string, params: object): void { + this._sendRequest(method, params); + } + + /** + * Handle an RPC request from remote, should return a result or Promise for result, if appropriate. + * @param method - the method requested by the remote caller. + * @param params - the parameters sent with the remote caller's request. + */ + didReceiveCall (method: string, params: object): unknown { // eslint-disable-line no-unused-vars + throw new Error('Must override didReceiveCall'); + } + + _sendMessage (jsonMessageObject: object): void { // eslint-disable-line no-unused-vars + throw new Error('Must override _sendMessage'); + } + + _sendRequest (method: string, params: object, id?: number): void { + const request: Record = { + jsonrpc: '2.0', + method, + params + }; + + if (id !== null) { + request.id = id; + } + + this._sendMessage(request); + } + + _handleMessage (json: JSONRPCMessage): void { + if (json.jsonrpc !== '2.0') { + throw new Error(`Bad or missing JSON-RPC version in message: ${json}`); + } + if (Object.prototype.hasOwnProperty.call(json, 'method')) { + this._handleRequest(json as RequestMessage); + } else { + this._handleResponse(json as ResponseMessage); + } + } + + _sendResponse (id: number, result: unknown, error?: Error): void { + const response: Record = { + jsonrpc: '2.0', + id + }; + if (error) { + response.error = error; + } else { + response.result = result || null; + } + this._sendMessage(response); + } + + _handleResponse (json: ResponseMessage): void { + const {result, error, id} = json; + const openRequest = this._openRequests[id]; + delete this._openRequests[id]; + if (openRequest) { + if (error) { + openRequest.reject(error as Error); + } else { + openRequest.resolve(result); + } + } + } + + _handleRequest (json: RequestMessage): void { + const {method, params, id} = json; + const rawResult = this.didReceiveCall(method, params); + if (id !== null && typeof id !== 'undefined') { + Promise.resolve(rawResult).then( + result => { + this._sendResponse(id as number, result); + }, + error => { + this._sendResponse(id as number, null, error as Error); + } + ); + } + } +} + +export default JSONRPC; diff --git a/packages/vm/src/util/log.js b/packages/vm/src/util/log.ts similarity index 100% rename from packages/vm/src/util/log.js rename to packages/vm/src/util/log.ts diff --git a/packages/vm/src/util/math-util.js b/packages/vm/src/util/math-util.ts similarity index 57% rename from packages/vm/src/util/math-util.js rename to packages/vm/src/util/math-util.ts index 84b4e16b..67ddcaa3 100644 --- a/packages/vm/src/util/math-util.js +++ b/packages/vm/src/util/math-util.ts @@ -1,31 +1,31 @@ class MathUtil { /** * Convert a value from degrees to radians. - * @param {!number} deg Value in degrees. - * @returns {!number} Equivalent value in radians. + * @param deg Value in degrees. + * @returns Equivalent value in radians. */ - static degToRad (deg) { + static degToRad (deg: number): number { return deg * Math.PI / 180; } /** * Convert a value from radians to degrees. - * @param {!number} rad Value in radians. - * @returns {!number} Equivalent value in degrees. + * @param rad Value in radians. + * @returns Equivalent value in degrees. */ - static radToDeg (rad) { + static radToDeg (rad: number): number { return rad * 180 / Math.PI; } /** * Clamp a number between two limits. * If n < min, return min. If n > max, return max. Else, return n. - * @param {!number} n Number to clamp. - * @param {!number} min Minimum limit. - * @param {!number} max Maximum limit. - * @returns {!number} Value of n clamped to min and max. + * @param n Number to clamp. + * @param min Minimum limit. + * @param max Maximum limit. + * @returns Value of n clamped to min and max. */ - static clamp (n, min, max) { + static clamp (n: number, min: number, max: number): number { return Math.min(Math.max(n, min), max); } @@ -34,12 +34,12 @@ class MathUtil { * e.g., wrapClamp(7, 1, 5) == 2 * wrapClamp(0, 1, 5) == 5 * wrapClamp(-11, -10, 6) == 6, etc. - * @param {!number} n Number to wrap. - * @param {!number} min Minimum limit. - * @param {!number} max Maximum limit. - * @returns {!number} Value of n wrapped between min and max. + * @param n Number to wrap. + * @param min Minimum limit. + * @param max Maximum limit. + * @returns Value of n wrapped between min and max. */ - static wrapClamp (n, min, max) { + static wrapClamp (n: number, min: number, max: number): number { const range = (max - min) + 1; return n - (Math.floor((n - min) / range) * range); } @@ -47,10 +47,10 @@ class MathUtil { /** * Convert a value from tan function in degrees. - * @param {!number} angle in degrees - * @returns {!number} Correct tan value + * @param angle in degrees + * @returns Correct tan value */ - static tan (angle) { + static tan (angle: number): number { angle = angle % 360; switch (angle) { case -270: @@ -70,10 +70,10 @@ class MathUtil { * represents the position of that element in a sorted version of the * original array. * E.g. [5, 19. 13, 1] => [1, 3, 2, 0] - * @param {Array} elts The elements to sort and reduce - * @returns {Array} The array of reduced orderings + * @param elts The elements to sort and reduce + * @returns The array of reduced orderings */ - static reducedSortOrdering (elts) { + static reducedSortOrdering (elts: number[]): number[] { const sorted = elts.slice(0).sort((a, b) => a - b); return elts.map(e => sorted.indexOf(e)); } @@ -85,12 +85,12 @@ class MathUtil { * For instance, (1, 5, 3) will only pick 1, 2, 4, or 5 (with equal * probability) * - * @param {number} lower - The lower bound (inlcusive) - * @param {number} upper - The upper bound (inclusive), such that lower <= upper - * @param {number} excluded - The number to exclude (MUST be in the range) - * @returns {number} A random integer in the range [lower, upper] that is not "excluded" + * @param lower - The lower bound (inlcusive) + * @param upper - The upper bound (inclusive), such that lower <= upper + * @param excluded - The number to exclude (MUST be in the range) + * @returns A random integer in the range [lower, upper] that is not "excluded" */ - static inclusiveRandIntWithout (lower, upper, excluded) { + static inclusiveRandIntWithout (lower: number, upper: number, excluded: number): number { // Note that subtraction is the number of items in the // inclusive range [lower, upper] minus 1 already // (e.g. in the set {3, 4, 5}, 5 - 3 = 2). @@ -106,14 +106,14 @@ class MathUtil { /** * Scales a number from one range to another. - * @param {number} i number to be scaled - * @param {number} iMin input range minimum - * @param {number} iMax input range maximum - * @param {number} oMin output range minimum - * @param {number} oMax output range maximum - * @returns {number} scaled number + * @param i number to be scaled + * @param iMin input range minimum + * @param iMax input range maximum + * @param oMin output range minimum + * @param oMax output range maximum + * @returns scaled number */ - static scale (i, iMin, iMax, oMin, oMax) { + static scale (i: number, iMin: number, iMax: number, oMin: number, oMax: number): number { const p = (i - iMin) / (iMax - iMin); return (p * (oMax - oMin)) + oMin; } diff --git a/packages/vm/src/util/maybe-format-message.js b/packages/vm/src/util/maybe-format-message.js deleted file mode 100644 index dac3361d..00000000 --- a/packages/vm/src/util/maybe-format-message.js +++ /dev/null @@ -1,18 +0,0 @@ -import formatMessage from 'format-message'; - -/** - * Check if `maybeMessage` looks like a message object, and if so pass it to `formatMessage`. - * Otherwise, return `maybeMessage` as-is. - * @param {*} maybeMessage - something that might be a message descriptor object. - * @param {object} [args] - the arguments to pass to `formatMessage` if it gets called. - * @param {string} [locale] - the locale to pass to `formatMessage` if it gets called. - * @returns {string|*} - the formatted message OR the original `maybeMessage` input. - */ -const maybeFormatMessage = function (maybeMessage, args, locale) { - if (maybeMessage && maybeMessage.id && maybeMessage.default) { - return formatMessage(maybeMessage, args, locale); - } - return maybeMessage; -}; - -export default maybeFormatMessage; diff --git a/packages/vm/src/util/maybe-format-message.ts b/packages/vm/src/util/maybe-format-message.ts new file mode 100644 index 00000000..c7c31575 --- /dev/null +++ b/packages/vm/src/util/maybe-format-message.ts @@ -0,0 +1,18 @@ +import formatMessage from 'format-message'; + +/** + * Check if `maybeMessage` looks like a message object, and if so pass it to `formatMessage`. + * Otherwise, return `maybeMessage` as-is. + * @param maybeMessage - something that might be a message descriptor object. + * @param [args] - the arguments to pass to `formatMessage` if it gets called. + * @param [locale] - the locale to pass to `formatMessage` if it gets called. + * @returns - the formatted message OR the original `maybeMessage` input. + */ +const maybeFormatMessage = function (maybeMessage: unknown, args?: Record, locale?: string): unknown { + if (maybeMessage && (maybeMessage as Record).id && (maybeMessage as Record).default) { + return formatMessage(maybeMessage as { id: string; default: string }, args, locale); + } + return maybeMessage; +}; + +export default maybeFormatMessage; diff --git a/packages/vm/src/util/new-block-ids.js b/packages/vm/src/util/new-block-ids.ts similarity index 57% rename from packages/vm/src/util/new-block-ids.js rename to packages/vm/src/util/new-block-ids.ts index b8c2a52d..caa5e99d 100644 --- a/packages/vm/src/util/new-block-ids.js +++ b/packages/vm/src/util/new-block-ids.ts @@ -1,12 +1,24 @@ -import uid from './uid.js'; +import uid from './uid'; + +interface BlockInput { + block: string; + shadow: string; +} + +interface BlockWithMutation { + id: string; + inputs: Record; + parent?: string; + next?: string; +} /** * Mutate the given blocks to have new IDs and update all internal ID references. * Does not return anything to make it clear that the blocks are updated in-place. - * @param {Array} blocks - blocks to be mutated. + * @param blocks - blocks to be mutated. */ -export default blocks => { - const oldToNew = {}; +export default (blocks: BlockWithMutation[]): void => { + const oldToNew: Record = {}; // First update all top-level IDs and create old-to-new mapping for (let i = 0; i < blocks.length; i++) { @@ -23,11 +35,13 @@ export default blocks => { input.block = oldToNew[input.block]; input.shadow = oldToNew[input.shadow]; } - if (blocks[i].parent) { - blocks[i].parent = oldToNew[blocks[i].parent]; + const parent = blocks[i].parent; + if (parent) { + blocks[i].parent = oldToNew[parent]; } - if (blocks[i].next) { - blocks[i].next = oldToNew[blocks[i].next]; + const next = blocks[i].next; + if (next) { + blocks[i].next = oldToNew[next]; } } }; diff --git a/packages/vm/src/util/rateLimiter.js b/packages/vm/src/util/rateLimiter.ts similarity index 86% rename from packages/vm/src/util/rateLimiter.js rename to packages/vm/src/util/rateLimiter.ts index 2ad75871..ae7638f1 100644 --- a/packages/vm/src/util/rateLimiter.js +++ b/packages/vm/src/util/rateLimiter.ts @@ -1,31 +1,33 @@ -import Timer from '../util/timer.js'; +import Timer from '../util/timer'; class RateLimiter { + _maxTokens: number; + _refillInterval: number; + _count: number; + _timer: Timer; + _lastUpdateTime: number; + /** * A utility for limiting the rate of repetitive send operations, such as * bluetooth messages being sent to hardware devices. It uses the token bucket * strategy: a counter accumulates tokens at a steady rate, and each send costs * a token. If no tokens remain, it's not okay to send. - * @param {number} maxRate the maximum number of sends allowed per second - * @class + * @param maxRate the maximum number of sends allowed per second */ - constructor (maxRate) { + constructor (maxRate: number) { /** * The maximum number of tokens. - * @type {number} */ this._maxTokens = maxRate; /** * The interval in milliseconds for refilling one token. It is calculated * so that the tokens will be filled to maximum in one second. - * @type {number} */ this._refillInterval = 1000 / maxRate; /** * The current number of tokens in the bucket. - * @type {number} */ this._count = this._maxTokens; @@ -34,7 +36,6 @@ class RateLimiter { /** * The last time in milliseconds when the token count was updated. - * @type {number} */ this._lastUpdateTime = this._timer.timeElapsed(); } @@ -42,9 +43,9 @@ class RateLimiter { /** * Check if it is okay to send a message, by updating the token count, * taking a token and then checking if we are still under the rate limit. - * @returns {boolean} true if we are under the rate limit + * @returns true if we are under the rate limit */ - okayToSend () { + okayToSend (): boolean { // Calculate the number of tokens to refill the bucket with, based on the // amount of time since the last refill. const now = this._timer.timeElapsed(); diff --git a/packages/vm/src/util/scratch-link-websocket.js b/packages/vm/src/util/scratch-link-websocket.ts similarity index 69% rename from packages/vm/src/util/scratch-link-websocket.js rename to packages/vm/src/util/scratch-link-websocket.ts index a5aeb97c..e500b144 100644 --- a/packages/vm/src/util/scratch-link-websocket.js +++ b/packages/vm/src/util/scratch-link-websocket.ts @@ -12,7 +12,15 @@ * - isOpen() */ class ScratchLinkWebSocket { - constructor (type) { + _type: string; + _onOpen: ((e: Event) => void) | null; + _onClose: ((e: Event) => void) | null; + _onError: ((e: Event) => void) | null; + _handleMessage: ((json: unknown) => void) | null; + + _ws: WebSocket | null; + + constructor (type: string) { this._type = type; this._onOpen = null; this._onClose = null; @@ -22,12 +30,12 @@ class ScratchLinkWebSocket { this._ws = null; } - open () { + open (): void { if (!(this._onOpen && this._onClose && this._onError && this._handleMessage)) { throw new Error('Must set open, close, message and error handlers before calling open on the socket'); } - let pathname; + let pathname: string; switch (this._type) { case 'BLE': pathname = 'scratch/ble'; @@ -44,7 +52,7 @@ class ScratchLinkWebSocket { // those who need the fallback. // If both connections fail we should report only one error. - const setSocket = (socketToUse, socketToClose) => { + const setSocket = (socketToUse: WebSocket, socketToClose: WebSocket) => { socketToClose.onopen = socketToClose.onerror = null; socketToClose.close(); @@ -61,72 +69,72 @@ class ScratchLinkWebSocket { const connectTimeout = setTimeout(() => { // neither socket succeeded before the timeout setSocket(ws, wss); - this._ws.onerror(new Event('timeout')); + this._ws!.onerror!(new Event('timeout')); }, 15 * 1000); - ws.onopen = openEvent => { + ws.onopen = (openEvent: Event) => { clearTimeout(connectTimeout); setSocket(ws, wss); - this._ws.onopen(openEvent); + this._ws!.onopen!(openEvent); }; - wss.onopen = openEvent => { + wss.onopen = (openEvent: Event) => { clearTimeout(connectTimeout); setSocket(wss, ws); - this._ws.onopen(openEvent); + this._ws!.onopen!(openEvent); }; - let wsError; - let wssError; + let wsError: Event | null = null; + let wssError: Event | null = null; const errorHandler = () => { // if only one has received an error, we haven't overall failed yet if (wsError && wssError) { clearTimeout(connectTimeout); setSocket(ws, wss); - this._ws.onerror(wsError); + this._ws!.onerror!(wsError); } }; - ws.onerror = errorEvent => { + ws.onerror = (errorEvent: Event) => { wsError = errorEvent; errorHandler(); }; - wss.onerror = errorEvent => { + wss.onerror = (errorEvent: Event) => { wssError = errorEvent; errorHandler(); }; } - close () { - this._ws.close(); + close (): void { + this._ws!.close(); this._ws = null; } - sendMessage (message) { + sendMessage (message: object): void { const messageText = JSON.stringify(message); - this._ws.send(messageText); + this._ws!.send(messageText); } - setOnOpen (fn) { + setOnOpen (fn: (e: Event) => void): void { this._onOpen = fn; } - setOnClose (fn) { + setOnClose (fn: (e: Event) => void): void { this._onClose = fn; } - setOnError (fn) { + setOnError (fn: (e: Event) => void): void { this._onError = fn; } - setHandleMessage (fn) { + setHandleMessage (fn: (json: unknown) => void): void { this._handleMessage = fn; } - isOpen () { - return this._ws && this._ws.readyState === this._ws.OPEN; + isOpen (): boolean { + return !!(this._ws && this._ws.readyState === this._ws.OPEN); } - _onMessage (e) { + _onMessage (e: MessageEvent): void { const json = JSON.parse(e.data); - this._handleMessage(json); + this._handleMessage!(json); } } diff --git a/packages/vm/src/util/string-util.js b/packages/vm/src/util/string-util.ts similarity index 74% rename from packages/vm/src/util/string-util.js rename to packages/vm/src/util/string-util.ts index 982c8ec6..87665f61 100644 --- a/packages/vm/src/util/string-util.js +++ b/packages/vm/src/util/string-util.ts @@ -1,13 +1,13 @@ -import log from './log.js'; +import log from './log'; class StringUtil { - static withoutTrailingDigits (s) { + static withoutTrailingDigits (s: string): string { let i = s.length - 1; while ((i >= 0) && ('0123456789'.indexOf(s.charAt(i)) > -1)) i--; return s.slice(0, i + 1); } - static unusedName (name, existingNames) { + static unusedName (name: string, existingNames: string[]): string { if (existingNames.indexOf(name) < 0) return name; name = StringUtil.withoutTrailingDigits(name); let i = 2; @@ -17,9 +17,9 @@ class StringUtil { /** * Split a string on the first occurrence of a split character. - * @param {string} text - the string to split. - * @param {string} separator - split the text on this character. - * @returns {string[]} - the two parts of the split string, or [text, null] if no split character found. + * @param text - the string to split. + * @param separator - split the text on this character. + * @returns - the two parts of the split string, or [text, null] if no split character found. * @example * // returns ['foo', 'tar.gz'] * splitFirst('foo.tar.gz', '.'); @@ -30,7 +30,7 @@ class StringUtil { * // returns ['foo', ''] * splitFirst('foo.', '.'); */ - static splitFirst (text, separator) { + static splitFirst (text: string, separator: string): [string, string | null] { const index = text.indexOf(separator); if (index >= 0) { return [text.substring(0, index), text.substring(index + 1)]; @@ -47,11 +47,11 @@ class StringUtil { * It is also consistent with the behavior of saving 2.0 projects. * This is only needed when stringifying an object for saving. * - * @param {!object} obj - The object to serialize - * @returns {!string} The JSON.stringified string with Infinity/NaN replaced with 0 + * @param obj - The object to serialize + * @returns The JSON.stringified string with Infinity/NaN replaced with 0 */ - static stringify (obj) { - return JSON.stringify(obj, (_key, value) => { + static stringify (obj: object): string { + return JSON.stringify(obj, (_key: string, value: unknown) => { if (typeof value === 'number' && (value === Infinity || value === -Infinity || isNaN(value))){ return 0; @@ -64,11 +64,11 @@ class StringUtil { * in cases where we're replacing non-user facing strings (e.g. variable IDs). * When replacing user facing strings, the xmlEscape utility function should be used * instead so that the user facing string does not change how it displays. - * @param {!string | !Array.} unsafe Unsafe string possibly containing unicode control characters. + * @param unsafe Unsafe string possibly containing unicode control characters. * In some cases this argument may be an array (e.g. hacked inputs from 2.0) - * @returns {string} String with control characters replaced. + * @returns String with control characters replaced. */ - static replaceUnsafeChars (unsafe) { + static replaceUnsafeChars (unsafe: string | string[]): string { if (typeof unsafe !== 'string') { if (Array.isArray(unsafe)) { // This happens when we have hacked blocks from 2.0 @@ -86,6 +86,7 @@ class StringUtil { case '&': return 'amp'; case '\'': return 'apos'; case '"': return 'quot'; + default: return c; } }); } diff --git a/packages/vm/src/util/task-queue.js b/packages/vm/src/util/task-queue.ts similarity index 70% rename from packages/vm/src/util/task-queue.js rename to packages/vm/src/util/task-queue.ts index 8e3cbba4..70e3d332 100644 --- a/packages/vm/src/util/task-queue.js +++ b/packages/vm/src/util/task-queue.ts @@ -1,68 +1,81 @@ -import Timer from '../util/timer.js'; +import Timer from '../util/timer'; + +interface TaskRecord { + cost: number; + promise?: Promise; + cancel?: () => void; + wrappedTask?: () => void; +} /** * This class uses the token bucket algorithm to control a queue of tasks. */ class TaskQueue { + _maxTokens: number; + _refillRate: number; + _pendingTaskRecords: TaskRecord[]; + _tokenCount: number; + _maxTotalCost: number; + _timer: Timer; + _timeout: ReturnType | null; + _lastUpdateTime: number; + + _runTasks: () => void; + /** * Creates an instance of TaskQueue. * To allow bursts, set `maxTokens` to several times the average task cost. * To prevent bursts, set `maxTokens` to the cost of the largest tasks. * Note that tasks with a cost greater than `maxTokens` will be rejected. * - * @param {number} maxTokens - the maximum number of tokens in the bucket (burst size). - * @param {number} refillRate - the number of tokens to be added per second (sustain rate). - * @param {object} options - optional settings for the new task queue instance. - * @property {number} startingTokens - the number of tokens the bucket starts with (default: `maxTokens`). - * @property {number} maxTotalCost - reject a task if total queue cost would pass this limit (default: no limit). - * @memberof TaskQueue + * @param maxTokens - the maximum number of tokens in the bucket (burst size). + * @param refillRate - the number of tokens to be added per second (sustain rate). + * @param options - optional settings for the new task queue instance. */ - constructor (maxTokens, refillRate, options = {}) { + constructor (maxTokens: number, refillRate: number, options: { startingTokens?: number; maxTotalCost?: number } = {}) { this._maxTokens = maxTokens; this._refillRate = refillRate; this._pendingTaskRecords = []; this._tokenCount = Object.prototype.hasOwnProperty.call(options, 'startingTokens') ? - options.startingTokens : maxTokens; + options.startingTokens! : maxTokens; this._maxTotalCost = Object.prototype.hasOwnProperty.call(options, 'maxTotalCost') ? - options.maxTotalCost : Infinity; + options.maxTotalCost! : Infinity; this._timer = new Timer(); this._timer.start(); this._timeout = null; this._lastUpdateTime = this._timer.timeElapsed(); - this._runTasks = this._runTasks.bind(this); + this._runTasks = this._runTasksImpl.bind(this); } /** * Get the number of queued tasks which have not yet started. * * @readonly - * @memberof TaskQueue - * @returns {number} the number of pending tasks. + * @returns the number of pending tasks. */ - get length () { + get length (): number { return this._pendingTaskRecords.length; } /** * Wait until the token bucket is full enough, then run the provided task. * - * @param {Function} task - the task to run. - * @param {number} [cost] - the number of tokens this task consumes from the bucket. - * @returns {Promise} - a promise for the task's return value. - * @memberof TaskQueue + * @param task - the task to run. + * @param [cost] - the number of tokens this task consumes from the bucket. + * @returns - a promise for the task's return value. */ - do (task, cost = 1) { + do (task: () => unknown, cost: number = 1): Promise { if (this._maxTotalCost < Infinity) { const currentTotalCost = this._pendingTaskRecords.reduce((t, r) => t + r.cost, 0); if (currentTotalCost + cost > this._maxTotalCost) { return Promise.reject('Maximum total cost exceeded'); } } - const newRecord = { + const newRecord: TaskRecord = { cost }; - newRecord.promise = new Promise((resolve, reject) => { + newRecord.promise = new Promise((resolve, reject) => { newRecord.cancel = () => { reject(new Error('Task canceled')); }; @@ -89,15 +102,14 @@ class TaskQueue { /** * Cancel one pending task, rejecting its promise. * - * @param {Promise} taskPromise - the promise returned by `do()`. - * @returns {boolean} - true if the task was found, or false otherwise. - * @memberof TaskQueue + * @param taskPromise - the promise returned by `do()`. + * @returns - true if the task was found, or false otherwise. */ - cancel (taskPromise) { + cancel (taskPromise: Promise): boolean { const taskIndex = this._pendingTaskRecords.findIndex(r => r.promise === taskPromise); if (taskIndex !== -1) { const [taskRecord] = this._pendingTaskRecords.splice(taskIndex, 1); - taskRecord.cancel(); + taskRecord.cancel!(); if (taskIndex === 0 && this._pendingTaskRecords.length > 0) { this._runTasks(); } @@ -109,28 +121,26 @@ class TaskQueue { /** * Cancel all pending tasks, rejecting all their promises. * - * @memberof TaskQueue */ - cancelAll () { + cancelAll (): void { if (this._timeout !== null) { this._timer.clearTimeout(this._timeout); this._timeout = null; } const oldTasks = this._pendingTaskRecords; this._pendingTaskRecords = []; - oldTasks.forEach(r => r.cancel()); + oldTasks.forEach(r => r.cancel!()); } /** * Shorthand for calling _refill() then _spend(cost). * - * @see {@link TaskQueue#_refill} - * @see {@link TaskQueue#_spend} - * @param {number} cost - the number of tokens to try to spend. - * @returns {boolean} true if we had enough tokens; false otherwise. - * @memberof TaskQueue + * @see _refill + * @see _spend + * @param cost - the number of tokens to try to spend. + * @returns true if we had enough tokens; false otherwise. */ - _refillAndSpend (cost) { + _refillAndSpend (cost: number): boolean { this._refill(); return this._spend(cost); } @@ -138,9 +148,8 @@ class TaskQueue { /** * Refill the token bucket based on the amount of time since the last refill. * - * @memberof TaskQueue */ - _refill () { + _refill (): void { const now = this._timer.timeElapsed(); const timeSinceRefill = now - this._lastUpdateTime; if (timeSinceRefill <= 0) return; @@ -154,11 +163,10 @@ class TaskQueue { * If we can "afford" the given cost, subtract that many tokens and return true. * Otherwise, return false. * - * @param {number} cost - the number of tokens to try to spend. - * @returns {boolean} true if we had enough tokens; false otherwise. - * @memberof TaskQueue + * @param cost - the number of tokens to try to spend. + * @returns true if we had enough tokens; false otherwise. */ - _spend (cost) { + _spend (cost: number): boolean { if (cost <= this._tokenCount) { this._tokenCount -= cost; return true; @@ -170,9 +178,8 @@ class TaskQueue { * Loop until the task queue is empty, running each task and spending tokens to do so. * Any time the bucket can't afford the next task, delay asynchronously until it can. * - * @memberof TaskQueue */ - _runTasks () { + _runTasksImpl (): void { if (this._timeout) { this._timer.clearTimeout(this._timeout); this._timeout = null; @@ -188,7 +195,7 @@ class TaskQueue { } // Refill before each task in case the time it took for the last task to run was enough to afford the next. if (this._refillAndSpend(nextRecord.cost)) { - nextRecord.wrappedTask(); + nextRecord.wrappedTask!(); } else { // We can't currently afford this task. Put it back and wait until we can and try again. this._pendingTaskRecords.unshift(nextRecord); diff --git a/packages/vm/src/util/timer.js b/packages/vm/src/util/timer.ts similarity index 73% rename from packages/vm/src/util/timer.js rename to packages/vm/src/util/timer.ts index cfbc38b9..dc6deedc 100644 --- a/packages/vm/src/util/timer.js +++ b/packages/vm/src/util/timer.ts @@ -13,7 +13,10 @@ */ class Timer { - constructor (nowObj = Timer.nowObj) { + startTime: number; + nowObj: { now: () => number }; + + constructor (nowObj: { now: () => number } = Timer.nowObj) { /** * Used to store the start time of a timer action. * Updated when calling `timer.start`. @@ -30,9 +33,8 @@ class Timer { /** * Disable use of self.performance for now as it results in lower performance * However, instancing it like below (caching the self.performance to a local variable) negates most of the issues. - * @type {boolean} */ - static get USE_PERFORMANCE () { + static get USE_PERFORMANCE (): boolean { return false; } @@ -41,9 +43,9 @@ class Timer { * @deprecated This is only called via the nowObj.now() if no other means is possible... * @returns An object with a now function that returns the current time in ms since 1 January 1970 00:00:00 UTC. */ - static get legacyDateCode () { + static get legacyDateCode (): { now: () => number } { return { - now: function () { + now () { return new Date().getTime(); } }; @@ -53,20 +55,18 @@ class Timer { * Use this object to route all time functions through single access points. * @returns An object with a now function that returns timestamp. */ - static get nowObj () { + static get nowObj (): { now: () => number } { if (Timer.USE_PERFORMANCE && typeof self !== 'undefined' && self.performance && 'now' in self.performance) { return self.performance; - } else if (Date.now) { - return Date; } - return Timer.legacyDateCode; + return Date; } /** * Return the currently known absolute time, in ms precision. - * @returns {number} ms elapsed since 1 January 1970 00:00:00 UTC. + * @returns ms elapsed since 1 January 1970 00:00:00 UTC. */ - time () { + time (): number { return this.nowObj.now(); } @@ -75,9 +75,9 @@ class Timer { * If possible, will use sub-millisecond precision. * If not, will use millisecond precision. * Not guaranteed to produce the same absolute values per-system. - * @returns {number} ms-scale accurate time relative to other relative times. + * @returns ms-scale accurate time relative to other relative times. */ - relativeTime () { + relativeTime (): number { return this.nowObj.now(); } @@ -85,30 +85,29 @@ class Timer { * Start a timer for measuring elapsed time, * at the most accurate precision possible. */ - start () { + start (): void { this.startTime = this.nowObj.now(); } - timeElapsed () { + timeElapsed (): number { return this.nowObj.now() - this.startTime; } /** * Call a handler function after a specified amount of time has elapsed. - * @param {Function} handler - function to call after the timeout - * @param {number} timeout - number of milliseconds to delay before calling the handler - * @returns {number} - the ID of the new timeout + * @param handler - function to call after the timeout + * @param timeout - number of milliseconds to delay before calling the handler + * @returns - the ID of the new timeout */ - setTimeout (handler, timeout) { + setTimeout (handler: () => void, timeout: number): ReturnType { return global.setTimeout(handler, timeout); } /** * Clear a timeout from the pending timeout pool. - * @param {number} timeoutId - the ID returned by `setTimeout()` - * @memberof Timer + * @param timeoutId - the ID returned by `setTimeout()` */ - clearTimeout (timeoutId) { + clearTimeout (timeoutId: ReturnType): void { global.clearTimeout(timeoutId); } } diff --git a/packages/vm/src/util/uid.js b/packages/vm/src/util/uid.ts similarity index 90% rename from packages/vm/src/util/uid.js rename to packages/vm/src/util/uid.ts index ba67654d..f55be658 100644 --- a/packages/vm/src/util/uid.js +++ b/packages/vm/src/util/uid.ts @@ -14,9 +14,9 @@ const soup_ = '!#%()*+,-./:;=?@[]^_`{|}~' + /** * Generate a unique ID, from Blockly. This should be globally unique. * 87 characters ^ 20 length > 128 bits (better than a UUID). - * @returns {string} A globally unique ID string. + * @returns A globally unique ID string. */ -const uid = function () { +const uid = function (): string { const length = 20; const soupLength = soup_.length; const id = []; diff --git a/packages/vm/src/util/variable-util.js b/packages/vm/src/util/variable-util.ts similarity index 54% rename from packages/vm/src/util/variable-util.js rename to packages/vm/src/util/variable-util.ts index 00f4e956..25764346 100644 --- a/packages/vm/src/util/variable-util.js +++ b/packages/vm/src/util/variable-util.ts @@ -1,5 +1,7 @@ +type VarRefMap = Record; + class VariableUtil { - static _mergeVarRefObjects (accum, obj2) { + static _mergeVarRefObjects (accum: VarRefMap, obj2: VarRefMap): VarRefMap { for (const id in obj2) { if (accum[id]) { accum[id] = accum[id].concat(obj2[id]); @@ -13,13 +15,13 @@ class VariableUtil { /** * Get all variable/list references in the given list of targets * in the project. - * @param {Array.} targets The list of targets to get the variable + * @param targets The list of targets to get the variable * and list references from. - * @param {boolean} shouldIncludeBroadcast Whether to include broadcast message fields. - * @returns {object} An object with variable ids as the keys and a list of block fields referencing + * @param shouldIncludeBroadcast Whether to include broadcast message fields. + * @returns An object with variable ids as the keys and a list of block fields referencing * the variable. */ - static getAllVarRefsForTargets (targets, shouldIncludeBroadcast) { + static getAllVarRefsForTargets (targets: Array<{ blocks: { getAllVariableAndListReferences: (a: null, b: boolean) => VarRefMap } }>, shouldIncludeBroadcast: boolean): VarRefMap { return targets .map(t => t.blocks.getAllVariableAndListReferences(null, shouldIncludeBroadcast)) .reduce(VariableUtil._mergeVarRefObjects, {}); @@ -27,14 +29,14 @@ class VariableUtil { /** * Give all variable references provided a new id and possibly new name. - * @param {Array} referencesToUpdate Context of the change, the object containing variable + * @param referencesToUpdate Context of the change, the object containing variable * references to update. - * @param {string} newId ID of the variable that the old references should be replaced with - * @param {?string} optNewName New variable name to merge with. The old + * @param newId ID of the variable that the old references should be replaced with + * @param optNewName New variable name to merge with. The old * variable name in the references being updated should be replaced with this new name. * If this parameter is not provided or is '', no name change occurs. */ - static updateVariableIdentifiers (referencesToUpdate, newId, optNewName) { + static updateVariableIdentifiers (referencesToUpdate: Array<{ referencingField: { id: string; value: string } }>, newId: string, optNewName?: string): void { referencesToUpdate.map(ref => { ref.referencingField.id = newId; if (optNewName) { diff --git a/packages/vm/src/util/xml-escape.js b/packages/vm/src/util/xml-escape.ts similarity index 79% rename from packages/vm/src/util/xml-escape.js rename to packages/vm/src/util/xml-escape.ts index 1ab80df7..60e7ee00 100644 --- a/packages/vm/src/util/xml-escape.js +++ b/packages/vm/src/util/xml-escape.ts @@ -1,14 +1,14 @@ -import log from './log.js'; +import log from './log'; /** * Escape a string to be safe to use in XML content. * CC-BY-SA: hgoebl * https://stackoverflow.com/questions/7918868/ * how-to-escape-xml-entities-in-javascript - * @param {!string | !Array.} unsafe Unsafe string. - * @returns {string} XML-escaped string, for use within an XML tag. + * @param unsafe Unsafe string. + * @returns XML-escaped string, for use within an XML tag. */ -const xmlEscape = function (unsafe) { +const xmlEscape = function (unsafe: string | string[]): string { if (typeof unsafe !== 'string') { if (Array.isArray(unsafe)) { // This happens when we have hacked blocks from 2.0 @@ -26,6 +26,7 @@ const xmlEscape = function (unsafe) { case '&': return '&'; case '\'': return '''; case '"': return '"'; + default: return c; } }); }; diff --git a/packages/vm/src/virtual-machine.js b/packages/vm/src/virtual-machine.js index b0e03b4f..7302b1df 100644 --- a/packages/vm/src/virtual-machine.js +++ b/packages/vm/src/virtual-machine.js @@ -10,17 +10,17 @@ import JSZip from 'jszip'; import {Buffer} from 'buffer'; import centralDispatch from './dispatch/central-dispatch.js'; import ExtensionManager from './extension-support/extension-manager.js'; -import log from './util/log.js'; -import MathUtil from './util/math-util.js'; +import log from './util/log'; +import MathUtil from './util/math-util'; import Runtime from './engine/runtime.js'; -import StringUtil from './util/string-util.js'; +import StringUtil from './util/string-util'; import formatMessage from 'format-message'; import Variable from './engine/variable.js'; -import newBlockIds from './util/new-block-ids.js'; +import newBlockIds from './util/new-block-ids'; import {loadCostume} from './import/load-costume.js'; import {loadSound} from './import/load-sound.js'; import {serializeSounds, serializeCostumes} from './serialization/serialize-assets.js'; -import uid from './util/uid.js'; +import uid from './util/uid'; import 'canvas-toBlob'; const RESERVED_NAMES = ['_mouse_', '_stage_', '_edge_', '_myself_', '_random_']; diff --git a/packages/vm/test/fixtures/dispatch-test-worker.js b/packages/vm/test/fixtures/dispatch-test-worker.js index c00e925c..b4e76036 100644 --- a/packages/vm/test/fixtures/dispatch-test-worker.js +++ b/packages/vm/test/fixtures/dispatch-test-worker.js @@ -1,6 +1,6 @@ import dispatch from '../../src/dispatch/worker-dispatch.js'; import DispatchTestService from './dispatch-test-service.js'; -import log from '../../src/util/log.js'; +import log from '../../src/util/log'; dispatch.setService('RemoteDispatchTest', new DispatchTestService()); diff --git a/packages/vm/test/integration/broadcast_special_chars_sb2.js b/packages/vm/test/integration/broadcast_special_chars_sb2.js index bd512b2c..ba6a2ae2 100644 --- a/packages/vm/test/integration/broadcast_special_chars_sb2.js +++ b/packages/vm/test/integration/broadcast_special_chars_sb2.js @@ -4,8 +4,8 @@ import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index.js'; import Variable from '../../src/engine/variable.js'; -import StringUtil from '../../src/util/string-util.js'; -import VariableUtil from '../../src/util/variable-util.js'; +import StringUtil from '../../src/util/string-util'; +import VariableUtil from '../../src/util/variable-util'; const projectUri = path.resolve(__dirname, '../fixtures/broadcast_special_chars.sb2'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/broadcast_special_chars_sb3.js b/packages/vm/test/integration/broadcast_special_chars_sb3.js index 1cd1085f..d7ba06ae 100644 --- a/packages/vm/test/integration/broadcast_special_chars_sb3.js +++ b/packages/vm/test/integration/broadcast_special_chars_sb3.js @@ -4,8 +4,8 @@ import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index.js'; import Variable from '../../src/engine/variable.js'; -import StringUtil from '../../src/util/string-util.js'; -import VariableUtil from '../../src/util/variable-util.js'; +import StringUtil from '../../src/util/string-util'; +import VariableUtil from '../../src/util/variable-util'; const projectUri = path.resolve(__dirname, '../fixtures/broadcast_special_chars.sb3'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/sb3-roundtrip.js b/packages/vm/test/integration/sb3-roundtrip.js index d306c709..3dcdc9cd 100644 --- a/packages/vm/test/integration/sb3-roundtrip.js +++ b/packages/vm/test/integration/sb3-roundtrip.js @@ -1,6 +1,6 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import Blocks from '../../src/engine/blocks.js'; -import Clone from '../../src/util/clone.js'; +import Clone from '../../src/util/clone'; import {loadCostume} from '../../src/import/load-costume.js'; import {loadSound} from '../../src/import/load-sound.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; diff --git a/packages/vm/test/integration/variable_special_chars_sb2.js b/packages/vm/test/integration/variable_special_chars_sb2.js index 9f73eac4..2942f7cb 100644 --- a/packages/vm/test/integration/variable_special_chars_sb2.js +++ b/packages/vm/test/integration/variable_special_chars_sb2.js @@ -4,8 +4,8 @@ import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index.js'; import Variable from '../../src/engine/variable.js'; -import StringUtil from '../../src/util/string-util.js'; -import VariableUtil from '../../src/util/variable-util.js'; +import StringUtil from '../../src/util/string-util'; +import VariableUtil from '../../src/util/variable-util'; const projectUri = path.resolve(__dirname, '../fixtures/variable_characters.sb2'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/variable_special_chars_sb3.js b/packages/vm/test/integration/variable_special_chars_sb3.js index 2512a7f4..e67272f2 100644 --- a/packages/vm/test/integration/variable_special_chars_sb3.js +++ b/packages/vm/test/integration/variable_special_chars_sb3.js @@ -4,8 +4,8 @@ import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index.js'; import Variable from '../../src/engine/variable.js'; -import StringUtil from '../../src/util/string-util.js'; -import VariableUtil from '../../src/util/variable-util.js'; +import StringUtil from '../../src/util/string-util'; +import VariableUtil from '../../src/util/variable-util'; const projectUri = path.resolve(__dirname, '../fixtures/variable_characters.sb3'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/unit/extension_video_sensing.js b/packages/vm/test/unit/extension_video_sensing.js index 37ed23c6..0667e629 100644 --- a/packages/vm/test/unit/extension_video_sensing.js +++ b/packages/vm/test/unit/extension_video_sensing.js @@ -2,7 +2,7 @@ import {createReadStream} from 'fs'; import {join} from 'path'; import {PNG} from 'pngjs'; import {test} from '../fixtures/jest-tap-bridge.js'; -import MathUtil from '../../src/util/math-util.js'; +import MathUtil from '../../src/util/math-util'; import VideoSensing from '../../src/extensions/scratch3_video_sensing/index.js'; import VideoMotion from '../../src/extensions/scratch3_video_sensing/library.js'; diff --git a/packages/vm/test/unit/maybe_format_message.js b/packages/vm/test/unit/maybe_format_message.js index 96619d26..cb3aa00d 100644 --- a/packages/vm/test/unit/maybe_format_message.js +++ b/packages/vm/test/unit/maybe_format_message.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import maybeFormatMessage from '../../src/util/maybe-format-message.js'; +import maybeFormatMessage from '../../src/util/maybe-format-message'; const nonMessages = [ 'hi', diff --git a/packages/vm/test/unit/mock-timer.js b/packages/vm/test/unit/mock-timer.js index c29e681c..457bb9ec 100644 --- a/packages/vm/test/unit/mock-timer.js +++ b/packages/vm/test/unit/mock-timer.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import MockTimer from '../fixtures/mock-timer.js'; +import MockTimer from '../fixtures/mock-timer'; test('spec', t => { const timer = new MockTimer(); diff --git a/packages/vm/test/unit/util_cast.js b/packages/vm/test/unit/util_cast.js index 11e37859..3261d540 100644 --- a/packages/vm/test/unit/util_cast.js +++ b/packages/vm/test/unit/util_cast.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import cast from '../../src/util/cast.js'; +import cast from '../../src/util/cast'; test('toNumber', t => { // Numeric diff --git a/packages/vm/test/unit/util_math.js b/packages/vm/test/unit/util_math.js index c3337776..8f0c5bd3 100644 --- a/packages/vm/test/unit/util_math.js +++ b/packages/vm/test/unit/util_math.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import math from '../../src/util/math-util.js'; +import math from '../../src/util/math-util'; test('degToRad', t => { t.equal(math.degToRad(0), 0); diff --git a/packages/vm/test/unit/util_new-block-ids.js b/packages/vm/test/unit/util_new-block-ids.js index c5934261..7d7e64f4 100644 --- a/packages/vm/test/unit/util_new-block-ids.js +++ b/packages/vm/test/unit/util_new-block-ids.js @@ -1,4 +1,4 @@ -import newBlockIds from '../../src/util/new-block-ids.js'; +import newBlockIds from '../../src/util/new-block-ids'; import simpleStack from '../fixtures/simple-stack.js'; import {test} from '../fixtures/jest-tap-bridge.js'; let originals; diff --git a/packages/vm/test/unit/util_rateLimiter.js b/packages/vm/test/unit/util_rateLimiter.js index b5851496..59dc7c15 100644 --- a/packages/vm/test/unit/util_rateLimiter.js +++ b/packages/vm/test/unit/util_rateLimiter.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import RateLimiter from '../../src/util/rateLimiter.js'; +import RateLimiter from '../../src/util/rateLimiter'; test('rate limiter', t => { // Create a rate limiter with maximum of 20 sends per second diff --git a/packages/vm/test/unit/util_string.js b/packages/vm/test/unit/util_string.js index 1eb09a41..76632473 100644 --- a/packages/vm/test/unit/util_string.js +++ b/packages/vm/test/unit/util_string.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import StringUtil from '../../src/util/string-util.js'; +import StringUtil from '../../src/util/string-util'; test('splitFirst', t => { t.same(StringUtil.splitFirst('asdf.1234', '.'), ['asdf', '1234']); diff --git a/packages/vm/test/unit/util_task-queue.js b/packages/vm/test/unit/util_task-queue.js index 738c731e..af0ee4ea 100644 --- a/packages/vm/test/unit/util_task-queue.js +++ b/packages/vm/test/unit/util_task-queue.js @@ -1,6 +1,6 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import TaskQueue from '../../src/util/task-queue.js'; -import MockTimer from '../fixtures/mock-timer.js'; +import TaskQueue from '../../src/util/task-queue'; +import MockTimer from '../fixtures/mock-timer'; import testCompare from '../fixtures/test-compare.js'; // Max tokens = 1000 diff --git a/packages/vm/test/unit/util_timer.js b/packages/vm/test/unit/util_timer.js index 62a77921..95f3f5fd 100644 --- a/packages/vm/test/unit/util_timer.js +++ b/packages/vm/test/unit/util_timer.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import Timer from '../../src/util/timer.js'; +import Timer from '../../src/util/timer'; // Stubbed current time let NOW = 0; diff --git a/packages/vm/test/unit/util_variable.js b/packages/vm/test/unit/util_variable.js index 3f5faf8d..50c7c178 100644 --- a/packages/vm/test/unit/util_variable.js +++ b/packages/vm/test/unit/util_variable.js @@ -1,7 +1,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import Target from '../../src/engine/target.js'; import Runtime from '../../src/engine/runtime.js'; -import VariableUtil from '../../src/util/variable-util.js'; +import VariableUtil from '../../src/util/variable-util'; let target1; let target2; diff --git a/packages/vm/test/unit/util_xml.js b/packages/vm/test/unit/util_xml.js index e073b9b7..971fbb38 100644 --- a/packages/vm/test/unit/util_xml.js +++ b/packages/vm/test/unit/util_xml.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import xml from '../../src/util/xml-escape.js'; +import xml from '../../src/util/xml-escape'; test('escape', t => { const input = ''; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 937c97e9..aae1217b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1223,6 +1223,12 @@ importers: '@babel/preset-typescript': specifier: ^7.28.5 version: 7.28.5(@babel/core@7.29.0) + '@types/atob': + specifier: ^2.1.4 + version: 2.1.4 + '@types/btoa': + specifier: ^1.2.5 + version: 1.2.5 '@types/fastestsmallesttextencoderdecoder': specifier: ^1.0.2 version: 1.0.2 @@ -3009,6 +3015,9 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/atob@2.1.4': + resolution: {integrity: sha512-FisOhG87cCFqzCgq6FUtSYsTMOHCB/p28zJbSN1QBo4ZGJfg9PEhMjdIV++NDeOnloUUe0Gz6jwBV+L1Ac00Mw==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -3027,6 +3036,9 @@ packages: '@types/bonjour@3.5.13': resolution: {integrity: sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==} + '@types/btoa@1.2.5': + resolution: {integrity: sha512-BItINdjZRlcGdI2efwK4bwxY5vEAT0SnIVfMOZVT18wp4900F1Lurqk/9PNdF9hMP1zgFmWbjVEtAsQKVcbqxA==} + '@types/connect-history-api-fallback@1.5.4': resolution: {integrity: sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==} @@ -3384,6 +3396,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} @@ -11969,6 +11982,8 @@ snapshots: tslib: 2.8.1 optional: true + '@types/atob@2.1.4': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.0 @@ -11999,6 +12014,10 @@ snapshots: dependencies: '@types/node': 25.5.2 + '@types/btoa@1.2.5': + dependencies: + '@types/node': 25.5.2 + '@types/connect-history-api-fallback@1.5.4': dependencies: '@types/express-serve-static-core': 4.19.8 @@ -12584,12 +12603,12 @@ snapshots: '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.105.4)': dependencies: webpack: 5.105.4(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.105.4) + webpack-cli: 6.0.1(webpack-dev-server@5.2.3)(webpack@5.105.4) '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.105.4)': dependencies: webpack: 5.105.4(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.105.4) + webpack-cli: 6.0.1(webpack-dev-server@5.2.3)(webpack@5.105.4) '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack-dev-server@5.2.3)(webpack@5.105.4)': dependencies: @@ -19704,7 +19723,7 @@ snapshots: watchpack: 2.5.1 webpack-sources: 3.3.4 optionalDependencies: - webpack-cli: 6.0.1(webpack@5.105.4) + webpack-cli: 6.0.1(webpack-dev-server@5.2.3)(webpack@5.105.4) transitivePeerDependencies: - '@swc/core' - esbuild From 9c129b1109a6d5475ca5d158e26c0e6163253e49 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Sun, 10 May 2026 18:17:43 +0800 Subject: [PATCH 02/30] :wrench: chore(vm): migrate some engine stuffs to typescript Signed-off-by: SimonShiki --- packages/vm/src/engine/comment.js | 70 --------------- packages/vm/src/engine/comment.ts | 90 +++++++++++++++++++ packages/vm/src/engine/monitor-record.js | 23 ----- packages/vm/src/engine/monitor-record.ts | 43 +++++++++ ...nstants.js => scratch-blocks-constants.ts} | 3 +- .../{stage-layering.js => stage-layering.ts} | 0 packages/vm/src/types/global.d.ts | 5 ++ 7 files changed, 139 insertions(+), 95 deletions(-) delete mode 100644 packages/vm/src/engine/comment.js create mode 100644 packages/vm/src/engine/comment.ts delete mode 100644 packages/vm/src/engine/monitor-record.js create mode 100644 packages/vm/src/engine/monitor-record.ts rename packages/vm/src/engine/{scratch-blocks-constants.js => scratch-blocks-constants.ts} (84%) rename packages/vm/src/engine/{stage-layering.js => stage-layering.ts} (100%) diff --git a/packages/vm/src/engine/comment.js b/packages/vm/src/engine/comment.js deleted file mode 100644 index b980a9a5..00000000 --- a/packages/vm/src/engine/comment.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * @fileoverview - * Object representing a Scratch Comment (block or workspace). - */ - -import uid from '../util/uid'; - -import xmlEscape from '../util/xml-escape'; - -class Comment { - /** - * @param {string} id Id of the comment. - * @param {string} text Text content of the comment. - * @param {number} x X position of the comment on the workspace. - * @param {number} y Y position of the comment on the workspace. - * @param {number} width The width of the comment when it is full size. - * @param {number} height The height of the comment when it is full size. - * @param {boolean} minimized Whether the comment is minimized. - * @class - */ - constructor (id, text, x, y, width, height, minimized) { - this.id = id || uid(); - this.text = text; - this.x = x; - this.y = y; - this.width = Math.max(Number(width), Comment.MIN_WIDTH); - this.height = Math.max(Number(height), Comment.MIN_HEIGHT); - this.minimized = minimized || false; - this.blockId = null; - } - - toXML () { - return `${xmlEscape(this.text)}`; - } - - toState () { - return { - id: this.id, - text: this.text, - x: this.x, - y: this.y, - width: this.width, - height: this.height, - collapsed: this.minimized - }; - } - - - // TODO choose min and defaults for width and height - static get MIN_WIDTH () { - return 20; - } - - static get MIN_HEIGHT () { - return 20; - } - - static get DEFAULT_WIDTH () { - return 100; - } - - static get DEFAULT_HEIGHT () { - return 100; - } - -} - -export default Comment; diff --git a/packages/vm/src/engine/comment.ts b/packages/vm/src/engine/comment.ts new file mode 100644 index 00000000..92273c20 --- /dev/null +++ b/packages/vm/src/engine/comment.ts @@ -0,0 +1,90 @@ +/** + * @fileoverview + * Object representing a Scratch Comment (block or workspace). + */ + +import uid from '../util/uid'; +import xmlEscape from '../util/xml-escape'; +import type * as ClipCCBlocks from 'clipcc-block'; + +class Comment { + /** + * Id of the comment. + */ + id: string; + blockId: string | null = null; + /** + * The width of the comment when it is full size. + */ + width: number; + /** + * The height of the comment when it is full size. + */ + height: number; + /** + * Whether the comment is minimized. + */ + minimized: boolean; + + /** + * @param id Id of the comment. + * @param text Text content of the comment. + * @param x X position of the comment on the workspace. + * @param y Y position of the comment on the workspace. + * @param width The width of the comment when it is full size. + * @param height The height of the comment when it is full size. + * @param minimized Whether the comment is minimized. + * @class + */ + constructor ( + id: string, + public text: string, + public x: number, + public y: number, + width: number, + height: number, + minimized: boolean) { + this.id = id || uid(); + this.width = Math.max(Number(width), Comment.MIN_WIDTH); + this.height = Math.max(Number(height), Comment.MIN_HEIGHT); + this.minimized = minimized || false; + } + + toXML (): string { + return `${xmlEscape(this.text)}`; + } + + toState (): ClipCCBlocks.serialization.workspaceComments.State { + return { + id: this.id, + text: this.text, + x: this.x, + y: this.y, + width: this.width, + height: this.height, + collapsed: this.minimized + }; + } + + // TODO choose min and defaults for width and height + static get MIN_WIDTH (): number { + return 20; + } + + static get MIN_HEIGHT (): number { + return 20; + } + + static get DEFAULT_WIDTH (): number { + return 100; + } + + static get DEFAULT_HEIGHT (): number { + return 100; + } + +} + +export default Comment; diff --git a/packages/vm/src/engine/monitor-record.js b/packages/vm/src/engine/monitor-record.js deleted file mode 100644 index 706a56a6..00000000 --- a/packages/vm/src/engine/monitor-record.js +++ /dev/null @@ -1,23 +0,0 @@ -import {Record} from 'immutable'; - -const MonitorRecord = Record({ - id: null, // Block Id - /** Present only if the monitor is sprite-specific, such as x position */ - spriteName: null, - /** Present only if the monitor is sprite-specific, such as x position */ - targetId: null, - opcode: null, - value: null, - params: null, - mode: 'default', - sliderMin: 0, - sliderMax: 100, - isDiscrete: true, - x: null, // (x: null, y: null) Indicates that the monitor should be auto-positioned - y: null, - width: 0, - height: 0, - visible: true -}); - -export default MonitorRecord; diff --git a/packages/vm/src/engine/monitor-record.ts b/packages/vm/src/engine/monitor-record.ts new file mode 100644 index 00000000..859fc7ad --- /dev/null +++ b/packages/vm/src/engine/monitor-record.ts @@ -0,0 +1,43 @@ +import {Record} from 'immutable'; + +interface MonitorRecordProps { + id: string | null; + /** Present only if the monitor is sprite-specific, such as x position */ + spriteName: string | null; + /** Present only if the monitor is sprite-specific, such as x position */ + targetId: string | null; + opcode: string | null; + value: unknown; + params: unknown; + mode: string; + sliderMin: number; + sliderMax: number; + isDiscrete: boolean; + x: number | null; + y: number | null; + width: number; + height: number; + visible: boolean; +} + +const defaultMonitorRecord: MonitorRecordProps = { + id: null, + spriteName: null, + targetId: null, + opcode: null, + value: null, + params: null, + mode: 'default', + sliderMin: 0, + sliderMax: 100, + isDiscrete: true, + x: null, + y: null, + width: 0, + height: 0, + visible: true +}; + +const MonitorRecord = Record(defaultMonitorRecord); + +export default MonitorRecord; diff --git a/packages/vm/src/engine/scratch-blocks-constants.js b/packages/vm/src/engine/scratch-blocks-constants.ts similarity index 84% rename from packages/vm/src/engine/scratch-blocks-constants.js rename to packages/vm/src/engine/scratch-blocks-constants.ts index 1f382b5f..1d8bc67c 100644 --- a/packages/vm/src/engine/scratch-blocks-constants.js +++ b/packages/vm/src/engine/scratch-blocks-constants.ts @@ -1,6 +1,5 @@ /** * These constants are copied from scratch-blocks/core/constants.js - * @todo find a way to require() these straight from scratch-blocks... maybe make a scratch-blocks/dist/constants.js? * @readonly * @enum {int} */ @@ -22,7 +21,7 @@ const ScratchBlocksConstants = { * @constant */ OUTPUT_SHAPE_SQUARE: 3 -}; +} as const; export default ScratchBlocksConstants; diff --git a/packages/vm/src/engine/stage-layering.js b/packages/vm/src/engine/stage-layering.ts similarity index 100% rename from packages/vm/src/engine/stage-layering.js rename to packages/vm/src/engine/stage-layering.ts diff --git a/packages/vm/src/types/global.d.ts b/packages/vm/src/types/global.d.ts index 0ab8d963..ad9aab6d 100644 --- a/packages/vm/src/types/global.d.ts +++ b/packages/vm/src/types/global.d.ts @@ -1,3 +1,8 @@ +declare module 'decode-html' { + function decodeHtml(html: string): string; + export default decodeHtml; +} + declare global { type int = number; } From b9655b8d443ba53d6d5dc0b67ac008705a9c3ef9 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Sun, 10 May 2026 18:20:39 +0800 Subject: [PATCH 03/30] :bug: fix(vm): correct reference Signed-off-by: SimonShiki --- packages/vm/src/blocks/scratch3_looks.js | 2 +- packages/vm/src/engine/runtime.js | 4 ++-- packages/vm/src/engine/target.js | 2 +- packages/vm/src/extensions/scratch3_pen/index.js | 2 +- packages/vm/src/io/video.js | 2 +- packages/vm/src/serialization/sb2.js | 6 +++--- packages/vm/src/serialization/sb3.js | 6 +++--- packages/vm/src/sprites/rendered-target.js | 2 +- packages/vm/src/sprites/sprite.js | 2 +- packages/vm/test/unit/engine_runtime.js | 2 +- packages/vm/test/unit/extension_conversion.js | 2 +- 11 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/vm/src/blocks/scratch3_looks.js b/packages/vm/src/blocks/scratch3_looks.js index 5b47a036..18d86db2 100644 --- a/packages/vm/src/blocks/scratch3_looks.js +++ b/packages/vm/src/blocks/scratch3_looks.js @@ -2,7 +2,7 @@ import Cast from '../util/cast'; import Clone from '../util/clone'; import RenderedTarget from '../sprites/rendered-target.js'; import uid from '../util/uid'; -import StageLayering from '../engine/stage-layering.js'; +import StageLayering from '../engine/stage-layering'; import getMonitorIdForBlockWithArgs from '../util/get-monitor-id'; import MathUtil from '../util/math-util'; diff --git a/packages/vm/src/engine/runtime.js b/packages/vm/src/engine/runtime.js index 039facee..66ddeaf7 100644 --- a/packages/vm/src/engine/runtime.js +++ b/packages/vm/src/engine/runtime.js @@ -7,12 +7,12 @@ import BlockType from '../extension-support/block-type'; import Profiler from './profiler.js'; import Sequencer from './sequencer.js'; import execute from './execute.js'; -import ScratchBlocksConstants from './scratch-blocks-constants.js'; +import ScratchBlocksConstants from './scratch-blocks-constants'; import TargetType from '../extension-support/target-type'; import Thread from './thread.js'; import log from '../util/log'; import maybeFormatMessage from '../util/maybe-format-message'; -import StageLayering from './stage-layering.js'; +import StageLayering from './stage-layering'; import Variable from './variable.js'; import xmlEscape from '../util/xml-escape'; import ScratchLinkWebSocket from '../util/scratch-link-websocket'; diff --git a/packages/vm/src/engine/target.js b/packages/vm/src/engine/target.js index e6fb1f84..68169664 100644 --- a/packages/vm/src/engine/target.js +++ b/packages/vm/src/engine/target.js @@ -1,7 +1,7 @@ import EventEmitter from 'events'; import Blocks from './blocks.js'; import Variable from '../engine/variable.js'; -import Comment from '../engine/comment.js'; +import Comment from '../engine/comment'; import uid from '../util/uid'; import {Map} from 'immutable'; import log from '../util/log'; diff --git a/packages/vm/src/extensions/scratch3_pen/index.js b/packages/vm/src/extensions/scratch3_pen/index.js index c2a7fbd3..a30bb867 100644 --- a/packages/vm/src/extensions/scratch3_pen/index.js +++ b/packages/vm/src/extensions/scratch3_pen/index.js @@ -8,7 +8,7 @@ import formatMessage from 'format-message'; import MathUtil from '../../util/math-util'; import RenderedTarget from '../../sprites/rendered-target.js'; import log from '../../util/log'; -import StageLayering from '../../engine/stage-layering.js'; +import StageLayering from '../../engine/stage-layering'; /** * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. diff --git a/packages/vm/src/io/video.js b/packages/vm/src/io/video.js index c97bd9c0..983e060e 100644 --- a/packages/vm/src/io/video.js +++ b/packages/vm/src/io/video.js @@ -1,4 +1,4 @@ -import StageLayering from '../engine/stage-layering.js'; +import StageLayering from '../engine/stage-layering'; class Video { constructor (runtime) { diff --git a/packages/vm/src/serialization/sb2.js b/packages/vm/src/serialization/sb2.js index 77338102..0528b004 100644 --- a/packages/vm/src/serialization/sb2.js +++ b/packages/vm/src/serialization/sb2.js @@ -19,10 +19,10 @@ import uid from '../util/uid'; import StringUtil from '../util/string-util'; import MathUtil from '../util/math-util'; import specMap from './sb2_specmap.js'; -import Comment from '../engine/comment.js'; +import Comment from '../engine/comment'; import Variable from '../engine/variable.js'; -import MonitorRecord from '../engine/monitor-record.js'; -import StageLayering from '../engine/stage-layering.js'; +import MonitorRecord from '../engine/monitor-record'; +import StageLayering from '../engine/stage-layering'; import {loadCostume} from '../import/load-costume.js'; import {loadSound} from '../import/load-sound.js'; import {deserializeCostume, deserializeSound} from './deserialize-assets.js'; diff --git a/packages/vm/src/serialization/sb3.js b/packages/vm/src/serialization/sb3.js index 7f8167fa..0a0b56ce 100644 --- a/packages/vm/src/serialization/sb3.js +++ b/packages/vm/src/serialization/sb3.js @@ -9,9 +9,9 @@ import vmPackage from '../../package.json'; import Blocks from '../engine/blocks.js'; import Sprite from '../sprites/sprite.js'; import Variable from '../engine/variable.js'; -import Comment from '../engine/comment.js'; -import MonitorRecord from '../engine/monitor-record.js'; -import StageLayering from '../engine/stage-layering.js'; +import Comment from '../engine/comment'; +import MonitorRecord from '../engine/monitor-record'; +import StageLayering from '../engine/stage-layering'; import log from '../util/log'; import uid from '../util/uid'; import MathUtil from '../util/math-util'; diff --git a/packages/vm/src/sprites/rendered-target.js b/packages/vm/src/sprites/rendered-target.js index 84781030..4b4be207 100644 --- a/packages/vm/src/sprites/rendered-target.js +++ b/packages/vm/src/sprites/rendered-target.js @@ -3,7 +3,7 @@ import StringUtil from '../util/string-util'; import Cast from '../util/cast'; import Clone from '../util/clone'; import Target from '../engine/target.js'; -import StageLayering from '../engine/stage-layering.js'; +import StageLayering from '../engine/stage-layering'; /** * Rendered target: instance of a sprite (clone), or the stage. diff --git a/packages/vm/src/sprites/sprite.js b/packages/vm/src/sprites/sprite.js index 49cdb5a5..1677b820 100644 --- a/packages/vm/src/sprites/sprite.js +++ b/packages/vm/src/sprites/sprite.js @@ -4,7 +4,7 @@ import {loadSoundFromAsset} from '../import/load-sound.js'; import {loadCostumeFromAsset} from '../import/load-costume.js'; import newBlockIds from '../util/new-block-ids'; import StringUtil from '../util/string-util'; -import StageLayering from '../engine/stage-layering.js'; +import StageLayering from '../engine/stage-layering'; class Sprite { /** diff --git a/packages/vm/test/unit/engine_runtime.js b/packages/vm/test/unit/engine_runtime.js index 99eb1cfa..5a76427e 100644 --- a/packages/vm/test/unit/engine_runtime.js +++ b/packages/vm/test/unit/engine_runtime.js @@ -3,7 +3,7 @@ import path from 'path'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/virtual-machine.js'; import Runtime from '../../src/engine/runtime.js'; -import MonitorRecord from '../../src/engine/monitor-record.js'; +import MonitorRecord from '../../src/engine/monitor-record'; import {Map} from 'immutable'; test('spec', t => { diff --git a/packages/vm/test/unit/extension_conversion.js b/packages/vm/test/unit/extension_conversion.js index f61e4297..f2e646f2 100644 --- a/packages/vm/test/unit/extension_conversion.js +++ b/packages/vm/test/unit/extension_conversion.js @@ -2,7 +2,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import ArgumentType from '../../src/extension-support/argument-type'; import BlockType from '../../src/extension-support/block-type'; import Runtime from '../../src/engine/runtime.js'; -import ScratchBlocksConstants from '../../src/engine/scratch-blocks-constants.js'; +import ScratchBlocksConstants from '../../src/engine/scratch-blocks-constants'; /** * @type {ExtensionMetadata} From f2151c15f19c15ec307a89dfcc1a67cabef9a473 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Sun, 10 May 2026 18:58:37 +0800 Subject: [PATCH 04/30] :wrench: chore(vm): migrate engine/variable Signed-off-by: SimonShiki --- packages/block/src/index.ts | 1 + packages/vm/src/engine/runtime.js | 2 +- packages/vm/src/engine/target.js | 2 +- .../src/engine/{variable.js => variable.ts} | 59 ++++++++++--------- packages/vm/src/io/cloud.js | 2 +- packages/vm/src/serialization/sb2.js | 2 +- packages/vm/src/serialization/sb2_specmap.js | 2 +- packages/vm/src/serialization/sb3.js | 2 +- packages/vm/src/virtual-machine.js | 2 +- .../broadcast_special_chars_sb2.js | 2 +- .../broadcast_special_chars_sb3.js | 2 +- packages/vm/test/integration/monitors_sb3.js | 2 +- .../integration/variable_special_chars_sb2.js | 2 +- .../integration/variable_special_chars_sb3.js | 2 +- packages/vm/test/unit/blocks_event.js | 2 +- packages/vm/test/unit/engine_blocks.js | 2 +- packages/vm/test/unit/engine_target.js | 2 +- packages/vm/test/unit/engine_variable.js | 2 +- packages/vm/test/unit/io_cloud.js | 2 +- packages/vm/test/unit/virtual-machine.js | 2 +- 20 files changed, 51 insertions(+), 45 deletions(-) rename packages/vm/src/engine/{variable.js => variable.ts} (61%) diff --git a/packages/block/src/index.ts b/packages/block/src/index.ts index ffc4675f..5eff15e2 100644 --- a/packages/block/src/index.ts +++ b/packages/block/src/index.ts @@ -234,6 +234,7 @@ export * as callbackRegistry from './callback_registry'; export * as constants from './constants'; export * as scratchBlocksUtils from './utils'; export type * as proceduresSerializer from './serialization/procedures'; +export type * as variableModel from './variable_model'; export {reportValue} from './report_value'; export {Colours} from './theme'; diff --git a/packages/vm/src/engine/runtime.js b/packages/vm/src/engine/runtime.js index 66ddeaf7..1131d945 100644 --- a/packages/vm/src/engine/runtime.js +++ b/packages/vm/src/engine/runtime.js @@ -13,7 +13,7 @@ import Thread from './thread.js'; import log from '../util/log'; import maybeFormatMessage from '../util/maybe-format-message'; import StageLayering from './stage-layering'; -import Variable from './variable.js'; +import Variable from './variable'; import xmlEscape from '../util/xml-escape'; import ScratchLinkWebSocket from '../util/scratch-link-websocket'; diff --git a/packages/vm/src/engine/target.js b/packages/vm/src/engine/target.js index 68169664..ddc89a3b 100644 --- a/packages/vm/src/engine/target.js +++ b/packages/vm/src/engine/target.js @@ -1,6 +1,6 @@ import EventEmitter from 'events'; import Blocks from './blocks.js'; -import Variable from '../engine/variable.js'; +import Variable from '../engine/variable'; import Comment from '../engine/comment'; import uid from '../util/uid'; import {Map} from 'immutable'; diff --git a/packages/vm/src/engine/variable.js b/packages/vm/src/engine/variable.ts similarity index 61% rename from packages/vm/src/engine/variable.js rename to packages/vm/src/engine/variable.ts index 081b0259..724a4293 100644 --- a/packages/vm/src/engine/variable.js +++ b/packages/vm/src/engine/variable.ts @@ -7,15 +7,34 @@ import uid from '../util/uid'; import xmlEscape from '../util/xml-escape'; +const enum VariableType { + SCALAR = '', + LIST = 'list', + BROADCAST_MESSAGE = 'broadcast_msg' +} + +import type * as ClipCCBlocks from 'clipcc-block'; + class Variable { /** - * @param {string} id Id of the variable. - * @param {string} name Name of the variable. - * @param {string} type Type of the variable, one of '' or 'list' - * @param {boolean} isCloud Whether the variable is stored in the cloud. - * @class + * Id of the variable. + */ + id: string; + /** + * Name of the variable. + */ + name: string; + /** + * Type of the variable. + */ + type: VariableType; + /** + * Whether the variable is stored in the cloud. */ - constructor (id, name, type, isCloud) { + isCloud: boolean; + value: any; + + constructor (id: string, name: string, type: VariableType, isCloud: boolean) { this.id = id || uid(); this.name = name; this.type = type; @@ -35,7 +54,7 @@ class Variable { } } - toXML (isLocal) { + toXML (isLocal: boolean): string { isLocal = (isLocal === true); return `${xmlEscape(this.name)}`; @@ -43,10 +62,10 @@ class Variable { /** * Serializes this VariableModel to JSON State. - * @param {boolean} isLocal Whether this variable is locally scoped. - * @returns {object} a JSON representation of this VariableModel. + * @param isLocal Whether this variable is locally scoped. + * @returns a JSON representation of this VariableModel. */ - toState (isLocal) { + toState (isLocal: boolean): ClipCCBlocks.variableModel.ScratchVariableState { isLocal = (isLocal === true); return { id: this.id, @@ -57,30 +76,16 @@ class Variable { }; } - /** - * Type representation for scalar variables. - * This is currently represented as '' - * for compatibility with blockly. - * @returns {string} - */ static get SCALAR_TYPE () { - return ''; + return VariableType.SCALAR; } - /** - * Type representation for list variables. - * @returns {string} - */ static get LIST_TYPE () { - return 'list'; + return VariableType.LIST; } - /** - * Type representation for list variables. - * @returns {string} - */ static get BROADCAST_MESSAGE_TYPE () { - return 'broadcast_msg'; + return VariableType.BROADCAST_MESSAGE; } } diff --git a/packages/vm/src/io/cloud.js b/packages/vm/src/io/cloud.js index 04979dd1..94d45cfd 100644 --- a/packages/vm/src/io/cloud.js +++ b/packages/vm/src/io/cloud.js @@ -1,4 +1,4 @@ -import Variable from '../engine/variable.js'; +import Variable from '../engine/variable'; import log from '../util/log'; class Cloud { diff --git a/packages/vm/src/serialization/sb2.js b/packages/vm/src/serialization/sb2.js index 0528b004..0cd3f41c 100644 --- a/packages/vm/src/serialization/sb2.js +++ b/packages/vm/src/serialization/sb2.js @@ -20,7 +20,7 @@ import StringUtil from '../util/string-util'; import MathUtil from '../util/math-util'; import specMap from './sb2_specmap.js'; import Comment from '../engine/comment'; -import Variable from '../engine/variable.js'; +import Variable from '../engine/variable'; import MonitorRecord from '../engine/monitor-record'; import StageLayering from '../engine/stage-layering'; import {loadCostume} from '../import/load-costume.js'; diff --git a/packages/vm/src/serialization/sb2_specmap.js b/packages/vm/src/serialization/sb2_specmap.js index b43c87b3..1373b50b 100644 --- a/packages/vm/src/serialization/sb2_specmap.js +++ b/packages/vm/src/serialization/sb2_specmap.js @@ -22,7 +22,7 @@ * Finally, I filled in the expected arguments as below. */ -import Variable from '../engine/variable.js'; +import Variable from '../engine/variable'; /** * @typedef {object} SB2SpecMap_blockInfo diff --git a/packages/vm/src/serialization/sb3.js b/packages/vm/src/serialization/sb3.js index 0a0b56ce..24b5bdd2 100644 --- a/packages/vm/src/serialization/sb3.js +++ b/packages/vm/src/serialization/sb3.js @@ -8,7 +8,7 @@ import vmPackage from '../../package.json'; import Blocks from '../engine/blocks.js'; import Sprite from '../sprites/sprite.js'; -import Variable from '../engine/variable.js'; +import Variable from '../engine/variable'; import Comment from '../engine/comment'; import MonitorRecord from '../engine/monitor-record'; import StageLayering from '../engine/stage-layering'; diff --git a/packages/vm/src/virtual-machine.js b/packages/vm/src/virtual-machine.js index 7302b1df..a72b1889 100644 --- a/packages/vm/src/virtual-machine.js +++ b/packages/vm/src/virtual-machine.js @@ -15,7 +15,7 @@ import MathUtil from './util/math-util'; import Runtime from './engine/runtime.js'; import StringUtil from './util/string-util'; import formatMessage from 'format-message'; -import Variable from './engine/variable.js'; +import Variable from './engine/variable'; import newBlockIds from './util/new-block-ids'; import {loadCostume} from './import/load-costume.js'; import {loadSound} from './import/load-sound.js'; diff --git a/packages/vm/test/integration/broadcast_special_chars_sb2.js b/packages/vm/test/integration/broadcast_special_chars_sb2.js index ba6a2ae2..d882dafe 100644 --- a/packages/vm/test/integration/broadcast_special_chars_sb2.js +++ b/packages/vm/test/integration/broadcast_special_chars_sb2.js @@ -3,7 +3,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index.js'; -import Variable from '../../src/engine/variable.js'; +import Variable from '../../src/engine/variable'; import StringUtil from '../../src/util/string-util'; import VariableUtil from '../../src/util/variable-util'; diff --git a/packages/vm/test/integration/broadcast_special_chars_sb3.js b/packages/vm/test/integration/broadcast_special_chars_sb3.js index d7ba06ae..acb3bfe1 100644 --- a/packages/vm/test/integration/broadcast_special_chars_sb3.js +++ b/packages/vm/test/integration/broadcast_special_chars_sb3.js @@ -3,7 +3,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index.js'; -import Variable from '../../src/engine/variable.js'; +import Variable from '../../src/engine/variable'; import StringUtil from '../../src/util/string-util'; import VariableUtil from '../../src/util/variable-util'; diff --git a/packages/vm/test/integration/monitors_sb3.js b/packages/vm/test/integration/monitors_sb3.js index 025a8582..715d2a29 100644 --- a/packages/vm/test/integration/monitors_sb3.js +++ b/packages/vm/test/integration/monitors_sb3.js @@ -3,7 +3,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index.js'; -import Variable from '../../src/engine/variable.js'; +import Variable from '../../src/engine/variable'; const projectUri = path.resolve(__dirname, '../fixtures/monitors.sb3'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/variable_special_chars_sb2.js b/packages/vm/test/integration/variable_special_chars_sb2.js index 2942f7cb..0faebc9e 100644 --- a/packages/vm/test/integration/variable_special_chars_sb2.js +++ b/packages/vm/test/integration/variable_special_chars_sb2.js @@ -3,7 +3,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index.js'; -import Variable from '../../src/engine/variable.js'; +import Variable from '../../src/engine/variable'; import StringUtil from '../../src/util/string-util'; import VariableUtil from '../../src/util/variable-util'; diff --git a/packages/vm/test/integration/variable_special_chars_sb3.js b/packages/vm/test/integration/variable_special_chars_sb3.js index e67272f2..3b327da7 100644 --- a/packages/vm/test/integration/variable_special_chars_sb3.js +++ b/packages/vm/test/integration/variable_special_chars_sb3.js @@ -3,7 +3,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index.js'; -import Variable from '../../src/engine/variable.js'; +import Variable from '../../src/engine/variable'; import StringUtil from '../../src/util/string-util'; import VariableUtil from '../../src/util/variable-util'; diff --git a/packages/vm/test/unit/blocks_event.js b/packages/vm/test/unit/blocks_event.js index e58242f5..5469b749 100644 --- a/packages/vm/test/unit/blocks_event.js +++ b/packages/vm/test/unit/blocks_event.js @@ -5,7 +5,7 @@ import Event from '../../src/blocks/scratch3_event.js'; import Runtime from '../../src/engine/runtime.js'; import Target from '../../src/engine/target.js'; import Thread from '../../src/engine/thread.js'; -import Variable from '../../src/engine/variable.js'; +import Variable from '../../src/engine/variable'; test('#760 - broadcastAndWait', t => { const broadcastAndWaitBlock = { diff --git a/packages/vm/test/unit/engine_blocks.js b/packages/vm/test/unit/engine_blocks.js index 153ef639..770c55ef 100644 --- a/packages/vm/test/unit/engine_blocks.js +++ b/packages/vm/test/unit/engine_blocks.js @@ -1,6 +1,6 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import Blocks from '../../src/engine/blocks.js'; -import Variable from '../../src/engine/variable.js'; +import Variable from '../../src/engine/variable'; import adapter from '../../src/engine/adapter.js'; import events from '../fixtures/events.json'; import Runtime from '../../src/engine/runtime.js'; diff --git a/packages/vm/test/unit/engine_target.js b/packages/vm/test/unit/engine_target.js index d3d0fb13..0c0a8ccf 100644 --- a/packages/vm/test/unit/engine_target.js +++ b/packages/vm/test/unit/engine_target.js @@ -1,6 +1,6 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import Target from '../../src/engine/target.js'; -import Variable from '../../src/engine/variable.js'; +import Variable from '../../src/engine/variable'; import adapter from '../../src/engine/adapter.js'; import Runtime from '../../src/engine/runtime.js'; import events from '../fixtures/events.json'; diff --git a/packages/vm/test/unit/engine_variable.js b/packages/vm/test/unit/engine_variable.js index 98717092..f7cc28e8 100644 --- a/packages/vm/test/unit/engine_variable.js +++ b/packages/vm/test/unit/engine_variable.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import Variable from '../../src/engine/variable.js'; +import Variable from '../../src/engine/variable'; import * as htmlparser from 'htmlparser2'; test('spec', t => { diff --git a/packages/vm/test/unit/io_cloud.js b/packages/vm/test/unit/io_cloud.js index 4400d893..41265ed6 100644 --- a/packages/vm/test/unit/io_cloud.js +++ b/packages/vm/test/unit/io_cloud.js @@ -1,7 +1,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import Cloud from '../../src/io/cloud.js'; import Target from '../../src/engine/target.js'; -import Variable from '../../src/engine/variable.js'; +import Variable from '../../src/engine/variable'; import Runtime from '../../src/engine/runtime.js'; test('spec', t => { diff --git a/packages/vm/test/unit/virtual-machine.js b/packages/vm/test/unit/virtual-machine.js index 1afe3c45..f5f592fa 100644 --- a/packages/vm/test/unit/virtual-machine.js +++ b/packages/vm/test/unit/virtual-machine.js @@ -1,7 +1,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import VirtualMachine from '../../src/virtual-machine.js'; import Sprite from '../../src/sprites/sprite.js'; -import Variable from '../../src/engine/variable.js'; +import Variable from '../../src/engine/variable'; import adapter from '../../src/engine/adapter.js'; import events from '../fixtures/events.json'; import Renderer from '../fixtures/fake-renderer.js'; From 1c6cdcdf8e5e649c3aea5f2c5859e865a8afd29e Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Sun, 10 May 2026 19:28:31 +0800 Subject: [PATCH 05/30] :wrench: chore(vm): migrate engine/adapter Signed-off-by: SimonShiki --- packages/block/src/index.ts | 1 + .../vm/src/engine/{adapter.js => adapter.ts} | 119 +++++++++--------- packages/vm/src/engine/blocks.js | 6 +- packages/vm/src/serialization/schema.ts | 49 ++++++++ packages/vm/test/unit/engine_adapter.js | 2 +- packages/vm/test/unit/engine_blocks.js | 2 +- packages/vm/test/unit/engine_target.js | 2 +- packages/vm/test/unit/virtual-machine.js | 2 +- 8 files changed, 119 insertions(+), 64 deletions(-) rename packages/vm/src/engine/{adapter.js => adapter.ts} (71%) diff --git a/packages/block/src/index.ts b/packages/block/src/index.ts index 5eff15e2..b9f6b5ff 100644 --- a/packages/block/src/index.ts +++ b/packages/block/src/index.ts @@ -238,6 +238,7 @@ export type * as variableModel from './variable_model'; export {reportValue} from './report_value'; export {Colours} from './theme'; +export {BlockDragEnd} from './events/block_drag_end'; export * as Theme from './theme'; export {glowStack} from './glow'; diff --git a/packages/vm/src/engine/adapter.js b/packages/vm/src/engine/adapter.ts similarity index 71% rename from packages/vm/src/engine/adapter.js rename to packages/vm/src/engine/adapter.ts index 9d373e17..b53514f7 100644 --- a/packages/vm/src/engine/adapter.js +++ b/packages/vm/src/engine/adapter.ts @@ -1,28 +1,32 @@ -import mutationAdapter from './mutation-adapter.js'; +import mutationAdapter from './mutation-adapter'; import * as html from 'htmlparser2'; +import type {DataNode, Element} from 'domhandler'; import uid from '../util/uid'; +import type * as ClipCCBlocks from 'clipcc-block'; +import type {VMBlock, VMField, VMMutation} from '../serialization/schema'; -/** - * @import * as Blockly from 'blockly'; - * @typedef {Blockly.serialization.blocks.State} BlocksState - */ +type BlockState = ClipCCBlocks.serialization.blocks.State; /** * Convert and an individual block DOM to the representation tree. * Based on Blockly's `domToBlockHeadless_`. - * @param {Element} blockDOM DOM tree for an individual block. - * @param {object} blocks Collection of blocks to add to. - * @param {boolean} isTopBlock Whether blocks at this level are "top blocks." - * @param {?string} parent Parent block ID. - * @returns {undefined} + * @param blockDOM DOM tree for an individual block. + * @param blocks Collection of blocks to add to. + * @param isTopBlock Whether blocks at this level are "top blocks." + * @param parent Parent block ID. */ -const domToBlock = function (blockDOM, blocks, isTopBlock, parent) { +const domToBlock = function ( + blockDOM: Element, + blocks: Record, + isTopBlock: boolean, + parent: string | null +): void { if (!blockDOM.attribs.id) { blockDOM.attribs.id = uid(); } // Block skeleton. - const block = { + const block: VMBlock = { id: blockDOM.attribs.id, // Block ID opcode: blockDOM.attribs.type, // For execution, "event_whengreenflag". inputs: {}, // Inputs to this block and the blocks they point to. @@ -31,8 +35,10 @@ const domToBlock = function (blockDOM, blocks, isTopBlock, parent) { topLevel: isTopBlock, // If this block starts a stack. parent: parent, // Parent block ID, if available. shadow: blockDOM.name === 'shadow', // If this represents a shadow/slot. - x: blockDOM.attribs.x, // X position of script, if top-level. - y: blockDOM.attribs.y // Y position of script, if top-level. + // X position of script, if top-level. + x: typeof blockDOM.attribs.x !== 'undefined' ? Number(blockDOM.attribs.x) : undefined, + // Y position of script, if top-level. + y: typeof blockDOM.attribs.y !== 'undefined' ? Number(blockDOM.attribs.y) : undefined }; // Add the block to the representation tree. @@ -40,21 +46,21 @@ const domToBlock = function (blockDOM, blocks, isTopBlock, parent) { // Process XML children and find enclosed blocks, fields, etc. for (let i = 0; i < blockDOM.children.length; i++) { - const xmlChild = blockDOM.children[i]; + const xmlChild = blockDOM.children[i] as Element; // Enclosed blocks and shadows - let childBlockNode = null; - let childShadowNode = null; + let childBlockNode: Element | null = null; + let childShadowNode: Element | null = null; for (let j = 0; j < xmlChild.children.length; j++) { const grandChildNode = xmlChild.children[j]; - if (!grandChildNode.name) { + if (!(grandChildNode as Element).name) { // Non-XML tag node. continue; } - const grandChildNodeName = grandChildNode.name.toLowerCase(); + const grandChildNodeName = (grandChildNode as Element).name.toLowerCase(); if (grandChildNodeName === 'block') { - childBlockNode = grandChildNode; + childBlockNode = grandChildNode as Element; } else if (grandChildNodeName === 'shadow') { - childShadowNode = grandChildNode; + childShadowNode = grandChildNode as Element; } } @@ -73,11 +79,9 @@ const domToBlock = function (blockDOM, blocks, isTopBlock, parent) { // Add id in case it is a variable field const fieldId = xmlChild.attribs.id; let fieldData = ''; - if (xmlChild.children.length > 0 && xmlChild.children[0].data) { - fieldData = xmlChild.children[0].data; + if (xmlChild.children.length > 0 && (xmlChild.children[0] as { data?: string }).data) { + fieldData = (xmlChild.children[0] as DataNode).data; } else { - // If the child of the field with a data property - // doesn't exist, set the data to an empty string. fieldData = ''; } block.fields[fieldName] = { @@ -100,7 +104,7 @@ const domToBlock = function (blockDOM, blocks, isTopBlock, parent) { case 'statement': { // Recursively generate block structure for input block. - domToBlock(childBlockNode, blocks, false, block.id); + domToBlock(childBlockNode!, blocks, false, block.id); if (childShadowNode && childBlockNode !== childShadowNode) { // Also generate the shadow block. domToBlock(childShadowNode, blocks, false, block.id); @@ -109,7 +113,7 @@ const domToBlock = function (blockDOM, blocks, isTopBlock, parent) { const inputName = xmlChild.attribs.name; block.inputs[inputName] = { name: inputName, - block: childBlockNode.attribs.id, + block: childBlockNode!.attribs.id, shadow: childShadowNode ? childShadowNode.attribs.id : null }; break; @@ -139,12 +143,12 @@ const domToBlock = function (blockDOM, blocks, isTopBlock, parent) { * Convert outer blocks DOM from a Blockly CREATE event * to a usable form for the Scratch runtime. * This structure is based on Blockly xml.js:`domToWorkspace` and `domToBlock`. - * @param {Element} blocksDOM DOM tree for this event. - * @returns {Array.} Usable list of blocks from this CREATE event. + * @param blocksDOM DOM tree for this event. + * @returns Usable list of blocks from this CREATE event. */ -const domToBlocks = function (blocksDOM) { +const domToBlocks = function (blocksDOM: Element[]): VMBlock[] { // At this level, there could be multiple blocks adjacent in the DOM tree. - const blocks = {}; + const blocks: Record = {}; for (let i = 0; i < blocksDOM.length; i++) { const block = blocksDOM[i]; if (!block.name || !block.attribs) { @@ -156,7 +160,7 @@ const domToBlocks = function (blocksDOM) { } } // Flatten blocks object into a list. - const blocksList = []; + const blocksList: VMBlock[] = []; for (const b in blocks) { if (!Object.prototype.hasOwnProperty.call(blocks, b)) continue; blocksList.push(blocks[b]); @@ -166,20 +170,19 @@ const domToBlocks = function (blocksDOM) { /** * Convert an individual block State to the representation tree. - * @param {object} blockState JSON State for an individual block. - * @param {object} blocks Collection of blocks to add to. - * @param {boolean} isTopBlock Whether blocks at this level are "top blocks." - * @param {?string} parent Parent block ID. - * @param {boolean} [isShadow] Whether this block is a shadow. - * @returns {undefined} + * @param blockState JSON State for an individual block. + * @param blocks Collection of blocks to add to. + * @param isTopBlock Whether blocks at this level are "top blocks." + * @param parent Parent block ID. + * @param isShadow Whether this block is a shadow. */ -const stateToBlock = function (blockState, blocks, isTopBlock, parent, isShadow) { +const stateToBlock = function (blockState: BlockState, blocks: Record, isTopBlock: boolean, parent: string | null, isShadow?: boolean): void { if (!blockState.id) { blockState.id = uid(); } // Block skeleton. - const block = { + const block: VMBlock = { id: blockState.id, // Block ID opcode: blockState.type, // For execution, "event_whengreenflag". inputs: {}, // Inputs to this block and the blocks they point to. @@ -200,7 +203,7 @@ const stateToBlock = function (blockState, blocks, isTopBlock, parent, isShadow) for (const fieldName in blockState.fields) { if (!Object.prototype.hasOwnProperty.call(blockState.fields, fieldName)) continue; const fieldData = blockState.fields[fieldName]; - const field = { + const field: VMField = { name: fieldName }; if (typeof fieldData === 'object' && fieldData !== null && fieldData.id) { @@ -221,16 +224,16 @@ const stateToBlock = function (blockState, blocks, isTopBlock, parent, isShadow) for (const inputName in blockState.inputs) { if (!Object.prototype.hasOwnProperty.call(blockState.inputs, inputName)) continue; const connection = blockState.inputs[inputName]; - let childBlockId = null; - let childShadowId = null; + let childBlockId: string | null = null; + let childShadowId: string | null = null; if (connection.block) { stateToBlock(connection.block, blocks, false, block.id, false); - childBlockId = connection.block.id; + childBlockId = connection.block.id!; } if (connection.shadow) { stateToBlock(connection.shadow, blocks, false, block.id, true); - childShadowId = connection.shadow.id; + childShadowId = connection.shadow.id!; } // Link this block's input to the child block. @@ -253,13 +256,13 @@ const stateToBlock = function (blockState, blocks, isTopBlock, parent, isShadow) const nextConnection = blockState.next; if (nextConnection.block) { stateToBlock(nextConnection.block, blocks, false, block.id, false); - block.next = nextConnection.block.id; + block.next = nextConnection.block.id!; } } // Process mutation if (blockState.extraState) { - block.mutation = blockState.extraState; + block.mutation = blockState.extraState as VMMutation; } // Process comments @@ -273,11 +276,11 @@ const stateToBlock = function (blockState, blocks, isTopBlock, parent, isShadow) /** * Blockly blocks JSON state to Scratch VM blocks representation. - * @param {BlocksState} blocksState The JSON state of the blocks to convert. - * @returns {Array.} Usable list of blocks from this CREATE event. + * @param blocksState The JSON state of the blocks to convert. + * @returns Usable list of blocks from this CREATE event. */ -const stateToBlocks = function (blocksState) { - const blocks = {}; +const stateToBlocks = function (blocksState: BlockState | BlockState[]): VMBlock[] { + const blocks: Record = {}; if (Array.isArray(blocksState)) { blocksState.forEach(blockState => { stateToBlock(blockState, blocks, true, null); @@ -287,7 +290,7 @@ const stateToBlocks = function (blocksState) { } // Flatten blocks object into a list. - const blocksList = []; + const blocksList: VMBlock[] = []; for (const b in blocks) { if (!Object.prototype.hasOwnProperty.call(blocks, b)) continue; blocksList.push(blocks[b]); @@ -295,22 +298,24 @@ const stateToBlocks = function (blocksState) { return blocksList; }; +type AdaptableEvents = (ClipCCBlocks.Events.BlockCreate | ClipCCBlocks.BlockDragEnd) & {xml?: { outerHTML: string }}; + /** * Adapter between block creation events and block representation which can be * used by the Scratch runtime. - * @param {object} e `Blockly.events.create` or `Blockly.events.endDrag` - * @returns {Array.} List of blocks from this CREATE event. + * @param e `Blockly.events.create` or `Blockly.events.endDrag` + * @returns List of blocks from this CREATE event. */ -const adapter = function (e) { +const adapter = function (e: AdaptableEvents): VMBlock[] | undefined { // Validate input if (typeof e !== 'object') return; // Prefer using JSON serialization - if (typeof e.json === 'object') { + if (e.json && typeof e.json === 'object') { return stateToBlocks(e.json); } if (typeof e.xml !== 'object') return; - return domToBlocks(html.parseDOM(e.xml.outerHTML, {decodeEntities: true})); + return domToBlocks(html.parseDOM(e.xml.outerHTML, {decodeEntities: true}) as Element[]); }; export default adapter; diff --git a/packages/vm/src/engine/blocks.js b/packages/vm/src/engine/blocks.js index aaea63ab..05703446 100644 --- a/packages/vm/src/engine/blocks.js +++ b/packages/vm/src/engine/blocks.js @@ -1,10 +1,10 @@ -import adapter from './adapter.js'; +import adapter from './adapter'; import xmlEscape from '../util/xml-escape'; -import MonitorRecord from './monitor-record.js'; +import MonitorRecord from './monitor-record'; import Clone from '../util/clone'; import {Map} from 'immutable'; import log from '../util/log'; -import Variable from './variable.js'; +import Variable from './variable'; import getMonitorIdForBlockWithArgs from '../util/get-monitor-id'; /** diff --git a/packages/vm/src/serialization/schema.ts b/packages/vm/src/serialization/schema.ts index 87822111..e515434f 100644 --- a/packages/vm/src/serialization/schema.ts +++ b/packages/vm/src/serialization/schema.ts @@ -156,3 +156,52 @@ export interface SB3Monitor { sliderMax?: number; isDiscrete?: boolean; } + +// --- VM runtime block presentation, used by engine/blocks and serialization/sb3. --- + +export interface VMBlock { + id: string; + opcode: string; + next: string | null; + parent: string | null; + inputs: Record; + fields: Record; + shadow: boolean; + topLevel: boolean; + x?: number; + y?: number; + mutation?: VMMutation; + comment?: string; + commentData?: unknown; + isMonitored?: boolean; + targetId?: string | null; +} + +export interface VMInput { + name: string; + block: string | null; + shadow: string | null; +} + +export interface VMField { + name: string; + value?: string; + id?: string; + variableType?: string; +} + +export interface VMMutation { + tagName?: string; + children?: VMMutation[]; + proccode?: string; + argumentids?: string; + argumentnames?: string; + argumentdefaults?: string; + warp?: string | boolean; + hasnext?: string | boolean; + return?: string | boolean; + global?: string | boolean; + generateshadows?: string | boolean; + blockInfo?: Record; + [key: string]: unknown; +} diff --git a/packages/vm/test/unit/engine_adapter.js b/packages/vm/test/unit/engine_adapter.js index 867b3613..ec42613c 100644 --- a/packages/vm/test/unit/engine_adapter.js +++ b/packages/vm/test/unit/engine_adapter.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import adapter from '../../src/engine/adapter.js'; +import adapter from '../../src/engine/adapter'; import events from '../fixtures/events.json'; test('spec', t => { diff --git a/packages/vm/test/unit/engine_blocks.js b/packages/vm/test/unit/engine_blocks.js index 770c55ef..f67f1816 100644 --- a/packages/vm/test/unit/engine_blocks.js +++ b/packages/vm/test/unit/engine_blocks.js @@ -1,7 +1,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import Blocks from '../../src/engine/blocks.js'; import Variable from '../../src/engine/variable'; -import adapter from '../../src/engine/adapter.js'; +import adapter from '../../src/engine/adapter'; import events from '../fixtures/events.json'; import Runtime from '../../src/engine/runtime.js'; diff --git a/packages/vm/test/unit/engine_target.js b/packages/vm/test/unit/engine_target.js index 0c0a8ccf..b03ebfb5 100644 --- a/packages/vm/test/unit/engine_target.js +++ b/packages/vm/test/unit/engine_target.js @@ -1,7 +1,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import Target from '../../src/engine/target.js'; import Variable from '../../src/engine/variable'; -import adapter from '../../src/engine/adapter.js'; +import adapter from '../../src/engine/adapter'; import Runtime from '../../src/engine/runtime.js'; import events from '../fixtures/events.json'; diff --git a/packages/vm/test/unit/virtual-machine.js b/packages/vm/test/unit/virtual-machine.js index f5f592fa..a12c3b73 100644 --- a/packages/vm/test/unit/virtual-machine.js +++ b/packages/vm/test/unit/virtual-machine.js @@ -2,7 +2,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import VirtualMachine from '../../src/virtual-machine.js'; import Sprite from '../../src/sprites/sprite.js'; import Variable from '../../src/engine/variable'; -import adapter from '../../src/engine/adapter.js'; +import adapter from '../../src/engine/adapter'; import events from '../fixtures/events.json'; import Renderer from '../fixtures/fake-renderer.js'; import Runtime from '../../src/engine/runtime.js'; From e456e218f1e32837d880656bdde7117ad77a4e0c Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Sun, 10 May 2026 19:29:50 +0800 Subject: [PATCH 06/30] :fire: chore(vm): remove mutation adapter Signed-off-by: SimonShiki --- packages/vm/src/engine/mutation-adapter.js | 48 ------------------- .../vm/test/unit/engine_mutation-adapter.js | 25 ---------- 2 files changed, 73 deletions(-) delete mode 100644 packages/vm/src/engine/mutation-adapter.js delete mode 100644 packages/vm/test/unit/engine_mutation-adapter.js diff --git a/packages/vm/src/engine/mutation-adapter.js b/packages/vm/src/engine/mutation-adapter.js deleted file mode 100644 index 9eb54873..00000000 --- a/packages/vm/src/engine/mutation-adapter.js +++ /dev/null @@ -1,48 +0,0 @@ -import * as html from 'htmlparser2'; -import decodeHtml from 'decode-html'; - -/** - * Convert a part of a mutation DOM to a mutation VM object, recursively. - * @param {object} dom DOM object for mutation tag. - * @returns {object} Object representing useful parts of this mutation. - */ -const mutatorTagToObject = function (dom) { - const obj = Object.create(null); - obj.tagName = dom.name; - obj.children = []; - for (const prop in dom.attribs) { - if (prop === 'xmlns') continue; - obj[prop] = decodeHtml(dom.attribs[prop]); - // Note: the capitalization of block info in the following lines is important. - // The lowercase is read in from xml which normalizes case. The VM uses camel case everywhere else. - if (prop === 'blockinfo') { - obj.blockInfo = JSON.parse(obj.blockinfo); - delete obj.blockinfo; - } - } - for (let i = 0; i < dom.children.length; i++) { - obj.children.push( - mutatorTagToObject(dom.children[i]) - ); - } - return obj; -}; - -/** - * Adapter between mutator XML or DOM and block representation which can be - * used by the Scratch runtime. - * @param {(object|string)} mutation Mutation XML string or DOM. - * @returns {object} Object representing the mutation. - */ -const mutationAdapter = function (mutation) { - let mutationParsed; - // Check if the mutation is already parsed; if not, parse it. - if (typeof mutation === 'object') { - mutationParsed = mutation; - } else { - mutationParsed = html.parseDOM(mutation)[0]; - } - return mutatorTagToObject(mutationParsed); -}; - -export default mutationAdapter; diff --git a/packages/vm/test/unit/engine_mutation-adapter.js b/packages/vm/test/unit/engine_mutation-adapter.js deleted file mode 100644 index 7eeae643..00000000 --- a/packages/vm/test/unit/engine_mutation-adapter.js +++ /dev/null @@ -1,25 +0,0 @@ -import {test} from '../fixtures/jest-tap-bridge.js'; -import mutationAdapter from '../../src/engine/mutation-adapter.js'; - -test('spec', t => { - t.type(mutationAdapter, 'function'); - t.end(); -}); - -test('convert DOM to Scratch object', t => { - const testStringRaw = '"arbitrary" & \'complicated\' test string'; - const testStringEscaped = '\\"arbitrary\\" & 'complicated' test string'; - const xml = ``; - const expectedMutation = { - tagName: 'mutation', - children: [], - blockInfo: { - text: testStringRaw - } - }; - - // TODO: do we want to test passing a DOM node to `mutationAdapter`? Node.js doesn't have built-in DOM support... - const mutationFromString = mutationAdapter(xml); - t.same(mutationFromString, expectedMutation); - t.end(); -}); From 30accb3e27f130af2b1eeac0235d84371f2e8523 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Sun, 10 May 2026 19:43:57 +0800 Subject: [PATCH 07/30] :wrench: chore(vm): migrate profiler Signed-off-by: SimonShiki --- .../src/engine/{profiler.js => profiler.ts} | 248 +++++++----------- packages/vm/src/engine/runtime.js | 2 +- 2 files changed, 92 insertions(+), 158 deletions(-) rename packages/vm/src/engine/{profiler.js => profiler.ts} (59%) diff --git a/packages/vm/src/engine/profiler.js b/packages/vm/src/engine/profiler.ts similarity index 59% rename from packages/vm/src/engine/profiler.js rename to packages/vm/src/engine/profiler.ts index a0cc4929..34630958 100644 --- a/packages/vm/src/engine/profiler.js +++ b/packages/vm/src/engine/profiler.ts @@ -15,193 +15,142 @@ /** * The next id returned for a new profile'd function. - * @type {number} */ let nextId = 0; /** * The mapping of names to ids. - * @constant {Record} */ -const profilerNames = {}; +const profilerNames: Record = {}; /** * The START event identifier in Profiler records. - * @constant {number} */ const START = 0; /** * The STOP event identifier in Profiler records. - * @constant {number} */ const STOP = 1; /** * The number of cells used in the records array by a START event. - * @constant {number} */ const START_SIZE = 4; /** * The number of cells used in the records array by a STOP event. - * @constant {number} */ const STOP_SIZE = 2; -/** - * Stored reference to Performance instance provided by the Browser. - * @constant {Performance} - */ -const performance = typeof window === 'object' && window.performance; - - -/** - * Callback handle called by Profiler for each frame it decodes from its - * records. - * @callback FrameCallback - * @param {ProfilerFrame} frame - */ +type FrameCallback = (frame: ProfilerFrame) => void; /** * A set of information about a frame of execution that was recorded. */ class ProfilerFrame { /** - * @param {number} depth Depth of the frame in the recorded stack. + * The numeric id of a record symbol like Runtime._step or + * blockFunction. */ - constructor (depth) { - /** - * The numeric id of a record symbol like Runtime._step or - * blockFunction. - * @type {number} - */ - this.id = -1; - - /** - * The amount of time spent inside the recorded frame and any deeper - * frames. - * @type {number} - */ - this.totalTime = 0; - - /** - * The amount of time spent only inside this record frame. Not - * including time in any deeper frames. - * @type {number} - */ - this.selfTime = 0; - - /** - * An arbitrary argument for the recorded frame. For example a block - * function might record its opcode as an argument. - * @type {*} - */ - this.arg = null; + id = -1; + /** + * The amount of time spent inside the recorded frame and any deeper + * frames. + */ + totalTime = 0; + /** + * The amount of time spent only inside this record frame. Not + * including time in any deeper frames. + */ + selfTime = 0; + /** + * An arbitrary argument for the recorded frame. For example a block + * function might record its opcode as an argument. + */ + arg: unknown = null; + count = 0; + constructor ( /** * The depth of the recorded frame. This can help compare recursive * funtions that are recorded. Each level of recursion with have a * different depth value. - * @type {number} - */ - this.depth = depth; - - /** - * A summarized count of the number of calls to this frame. - * @type {number} */ - this.count = 0; - } + public depth: number + ) {} } class Profiler { /** - * @param {FrameCallback} onFrame a handle called for each recorded frame. - * The passed frame value may not be stored as it'll be updated with later - * frame information. Any information that is further stored by the handler - * should make copies or reduce the information. + * A series of START and STOP values followed by arguments. After + * recording is complete the full set of records is reported back by + * stepping through the series to connect the relative START and STOP + * information. */ - constructor (onFrame = function () {}) { - /** - * A series of START and STOP values followed by arguments. After - * recording is complete the full set of records is reported back by - * stepping through the series to connect the relative START and STOP - * information. - * @type {Array.<*>} - */ - this.records = []; - - /** - * An array of frames incremented on demand instead as part of start - * and stop. - * @type {Array.} - */ - this.increments = []; - - /** - * An array of profiler frames separated by counter argument. Generally - * for Scratch these frames are separated by block function opcode. - * This tracks each time an opcode is called. - * @type {Array.} - */ - this.counters = []; + records: unknown[] = []; + /** + * An array of frames incremented on demand instead as part of start + * and stop. + */ + increments: ProfilerFrame[] = []; + /** + * An array of profiler frames separated by counter argument. Generally + * for Scratch these frames are separated by block function opcode. + * This tracks each time an opcode is called. + */ + counters: ProfilerFrame[] = []; + /** + * A frame with no id or argument. + */ + nullFrame: ProfilerFrame = new ProfilerFrame(-1); + /** + * A cache of ProfilerFrames to reuse when reporting the recorded + * frames in records. + */ + _stack: ProfilerFrame[] = [new ProfilerFrame(0)]; + /** + * A reference to the START record id constant. + */ + START = START; + /** + c * A reference to the STOP record id constant. + c */ + STOP = STOP; - /** - * A frame with no id or argument. - * @type {ProfilerFrame} - */ - this.nullFrame = new ProfilerFrame(-1); - - /** - * A cache of ProfilerFrames to reuse when reporting the recorded - * frames in records. - * @type {Array.} - */ - this._stack = [new ProfilerFrame(0)]; + static START = START; + static STOP = STOP; + constructor ( /** * A callback handle called with each decoded frame when reporting back * all the recorded times. - * @type {FrameCallback} - */ - this.onFrame = onFrame; - - /** - * A reference to the START record id constant. - * @constant {number} - */ - this.START = START; - - /** - * A reference to the STOP record id constant. - * @constant {number} - */ - this.STOP = STOP; - } + */ + public onFrame: FrameCallback = function () {} + ) {} /** * Start recording a frame of time for an id and optional argument. - * @param {number} id The id returned by idByName for a name symbol like + * @param id The id returned by idByName for a name symbol like * Runtime._step. - * @param {?*} arg An arbitrary argument value to store with the frame. + * @param arg An arbitrary argument value to store with the frame. */ - start (id, arg) { + start (id: number, arg: unknown): void { this.records.push(START, id, arg, performance.now()); } /** * Stop the current frame. */ - stop () { + stop (): void { this.records.push(STOP, performance.now()); } /** * Increment the number of times this symbol is called. - * @param {number} id The id returned by idByName for a name symbol. + * @param id The id returned by idByName for a name symbol. */ - increment (id) { + increment (id: number): void { if (!this.increments[id]) { this.increments[id] = new ProfilerFrame(-1); this.increments[id].id = id; @@ -212,13 +161,13 @@ class Profiler { /** * Find or create a ProfilerFrame-like object whose counter can be * incremented outside of the Profiler. - * @param {number} id The id returned by idByName for a name symbol. - * @param {*} arg The argument for a frame that identifies it in addition + * @param id The id returned by idByName for a name symbol. + * @param arg The argument for a frame that identifies it in addition * to the id. - * @returns {{count: number}} A ProfilerFrame-like whose count should be + * @returns A ProfilerFrame-like whose count should be * incremented for each call. */ - frame (id, arg) { + frame (id: number, arg: unknown): ProfilerFrame { for (let i = 0; i < this.counters.length; i++) { if (this.counters[i].id === id && this.counters[i].arg === arg) { return this.counters[i]; @@ -235,7 +184,7 @@ class Profiler { /** * Decode records and report all frames to `this.onFrame`. */ - reportFrames () { + reportFrames (): void { const stack = this._stack; let depth = 1; @@ -255,7 +204,7 @@ class Profiler { // Store id, arg, totalTime, and initialize selfTime. const frame = stack[depth++]; - frame.id = this.records[i + 1]; + frame.id = this.records[i + 1] as number; frame.arg = this.records[i + 2]; // totalTime is first set as the time recorded by this START // event. Once the STOP event is reached the stored start time @@ -266,7 +215,7 @@ class Profiler { // totalTime is used this way as a convenient member to store a // value between the two events without needing additional // members on the Frame or in a shadow map. - frame.totalTime = this.records[i + 3]; + frame.totalTime = this.records[i + 3] as number; // selfTime is decremented until we reach the STOP event for // this frame. totalTime will be added to it then to get the // time difference. @@ -274,7 +223,7 @@ class Profiler { i += START_SIZE; } else if (this.records[i] === STOP) { - const now = this.records[i + 1]; + const now = this.records[i + 1] as number; const frame = stack[--depth]; // totalTime is the difference between the start event time @@ -319,29 +268,28 @@ class Profiler { /** * Lookup or create an id for a frame name. - * @param {string} name The name to return an id for. - * @returns {number} The id for the passed name. + * @param name The name to return an id for. + * @returns The id for the passed name. */ - idByName (name) { + idByName (name: string): number { return Profiler.idByName(name); } /** * Reverse lookup the name from a given frame id. - * @param {number} id The id to search for. - * @returns {string} The name for the given id. + * @param id The id to search for. + * @returns The name for the given id. */ - nameById (id) { + nameById (id: number): string | null { return Profiler.nameById(id); } /** * Lookup or create an id for a frame name. - * @static - * @param {string} name The name to return an id for. - * @returns {number} The id for the passed name. + * @param name The name to return an id for. + * @returns The id for the passed name. */ - static idByName (name) { + static idByName (name: string): number { if (typeof profilerNames[name] !== 'number') { profilerNames[name] = nextId++; } @@ -350,11 +298,10 @@ class Profiler { /** * Reverse lookup the name from a given frame id. - * @static - * @param {number} id The id to search for. - * @returns {string} The name for the given id. + * @param id The id to search for. + * @returns The name for the given id. */ - static nameById (id) { + static nameById (id: number): string | null { for (const name in profilerNames) { if (profilerNames[name] === id) { return name; @@ -365,26 +312,13 @@ class Profiler { /** * Profiler is only available on platforms with the Performance API. - * @returns {boolean} Can the Profiler run in this browser? + * @returns Can the Profiler run in this browser? */ - static available () { + static available (): boolean { return ( typeof window === 'object' && typeof window.performance !== 'undefined'); } } - -/** - * A reference to the START record id constant. - * @constant {number} - */ -Profiler.START = START; - -/** - * A reference to the STOP record id constant. - * @constant {number} - */ -Profiler.STOP = STOP; - export default Profiler; diff --git a/packages/vm/src/engine/runtime.js b/packages/vm/src/engine/runtime.js index 1131d945..ea33ee9e 100644 --- a/packages/vm/src/engine/runtime.js +++ b/packages/vm/src/engine/runtime.js @@ -4,7 +4,7 @@ import ArgumentType from '../extension-support/argument-type'; import Blocks from './blocks.js'; import {getScripts as getCachedScriptsByOpcode} from './blocks-runtime-cache.js'; import BlockType from '../extension-support/block-type'; -import Profiler from './profiler.js'; +import Profiler from './profiler'; import Sequencer from './sequencer.js'; import execute from './execute.js'; import ScratchBlocksConstants from './scratch-blocks-constants'; From 0d5b4a176e3dd20a180cd7e29f8b255ec74a0a8d Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Sun, 10 May 2026 19:53:21 +0800 Subject: [PATCH 08/30] :rewind: revert: ":fire: chore(vm): remove mutation adapter" This reverts commit e456e218f1e32837d880656bdde7117ad77a4e0c. --- packages/vm/src/engine/mutation-adapter.js | 48 +++++++++++++++++++ .../vm/test/unit/engine_mutation-adapter.js | 25 ++++++++++ 2 files changed, 73 insertions(+) create mode 100644 packages/vm/src/engine/mutation-adapter.js create mode 100644 packages/vm/test/unit/engine_mutation-adapter.js diff --git a/packages/vm/src/engine/mutation-adapter.js b/packages/vm/src/engine/mutation-adapter.js new file mode 100644 index 00000000..9eb54873 --- /dev/null +++ b/packages/vm/src/engine/mutation-adapter.js @@ -0,0 +1,48 @@ +import * as html from 'htmlparser2'; +import decodeHtml from 'decode-html'; + +/** + * Convert a part of a mutation DOM to a mutation VM object, recursively. + * @param {object} dom DOM object for mutation tag. + * @returns {object} Object representing useful parts of this mutation. + */ +const mutatorTagToObject = function (dom) { + const obj = Object.create(null); + obj.tagName = dom.name; + obj.children = []; + for (const prop in dom.attribs) { + if (prop === 'xmlns') continue; + obj[prop] = decodeHtml(dom.attribs[prop]); + // Note: the capitalization of block info in the following lines is important. + // The lowercase is read in from xml which normalizes case. The VM uses camel case everywhere else. + if (prop === 'blockinfo') { + obj.blockInfo = JSON.parse(obj.blockinfo); + delete obj.blockinfo; + } + } + for (let i = 0; i < dom.children.length; i++) { + obj.children.push( + mutatorTagToObject(dom.children[i]) + ); + } + return obj; +}; + +/** + * Adapter between mutator XML or DOM and block representation which can be + * used by the Scratch runtime. + * @param {(object|string)} mutation Mutation XML string or DOM. + * @returns {object} Object representing the mutation. + */ +const mutationAdapter = function (mutation) { + let mutationParsed; + // Check if the mutation is already parsed; if not, parse it. + if (typeof mutation === 'object') { + mutationParsed = mutation; + } else { + mutationParsed = html.parseDOM(mutation)[0]; + } + return mutatorTagToObject(mutationParsed); +}; + +export default mutationAdapter; diff --git a/packages/vm/test/unit/engine_mutation-adapter.js b/packages/vm/test/unit/engine_mutation-adapter.js new file mode 100644 index 00000000..7eeae643 --- /dev/null +++ b/packages/vm/test/unit/engine_mutation-adapter.js @@ -0,0 +1,25 @@ +import {test} from '../fixtures/jest-tap-bridge.js'; +import mutationAdapter from '../../src/engine/mutation-adapter.js'; + +test('spec', t => { + t.type(mutationAdapter, 'function'); + t.end(); +}); + +test('convert DOM to Scratch object', t => { + const testStringRaw = '"arbitrary" & \'complicated\' test string'; + const testStringEscaped = '\\"arbitrary\\" & 'complicated' test string'; + const xml = ``; + const expectedMutation = { + tagName: 'mutation', + children: [], + blockInfo: { + text: testStringRaw + } + }; + + // TODO: do we want to test passing a DOM node to `mutationAdapter`? Node.js doesn't have built-in DOM support... + const mutationFromString = mutationAdapter(xml); + t.same(mutationFromString, expectedMutation); + t.end(); +}); From a50ef86d5850ad204866eb0adcb3ca551da914ed Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Sun, 10 May 2026 19:56:34 +0800 Subject: [PATCH 09/30] :wrench: chore(vm): migrate mutation adapter Signed-off-by: SimonShiki --- .../{mutation-adapter.js => mutation-adapter.ts} | 11 ++++++----- packages/vm/test/unit/engine_mutation-adapter.js | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) rename packages/vm/src/engine/{mutation-adapter.js => mutation-adapter.ts} (81%) diff --git a/packages/vm/src/engine/mutation-adapter.js b/packages/vm/src/engine/mutation-adapter.ts similarity index 81% rename from packages/vm/src/engine/mutation-adapter.js rename to packages/vm/src/engine/mutation-adapter.ts index 9eb54873..4447efef 100644 --- a/packages/vm/src/engine/mutation-adapter.js +++ b/packages/vm/src/engine/mutation-adapter.ts @@ -1,12 +1,13 @@ import * as html from 'htmlparser2'; import decodeHtml from 'decode-html'; +import type {Element} from 'domhandler'; /** * Convert a part of a mutation DOM to a mutation VM object, recursively. * @param {object} dom DOM object for mutation tag. * @returns {object} Object representing useful parts of this mutation. */ -const mutatorTagToObject = function (dom) { +const mutatorTagToObject = function (dom: Element) { const obj = Object.create(null); obj.tagName = dom.name; obj.children = []; @@ -22,7 +23,7 @@ const mutatorTagToObject = function (dom) { } for (let i = 0; i < dom.children.length; i++) { obj.children.push( - mutatorTagToObject(dom.children[i]) + mutatorTagToObject(dom.children[i] as Element) ); } return obj; @@ -31,10 +32,10 @@ const mutatorTagToObject = function (dom) { /** * Adapter between mutator XML or DOM and block representation which can be * used by the Scratch runtime. - * @param {(object|string)} mutation Mutation XML string or DOM. + * @param mutation Mutation XML string or DOM. * @returns {object} Object representing the mutation. */ -const mutationAdapter = function (mutation) { +const mutationAdapter = function (mutation: Element | string) { let mutationParsed; // Check if the mutation is already parsed; if not, parse it. if (typeof mutation === 'object') { @@ -42,7 +43,7 @@ const mutationAdapter = function (mutation) { } else { mutationParsed = html.parseDOM(mutation)[0]; } - return mutatorTagToObject(mutationParsed); + return mutatorTagToObject(mutationParsed as Element); }; export default mutationAdapter; diff --git a/packages/vm/test/unit/engine_mutation-adapter.js b/packages/vm/test/unit/engine_mutation-adapter.js index 7eeae643..3b6549e6 100644 --- a/packages/vm/test/unit/engine_mutation-adapter.js +++ b/packages/vm/test/unit/engine_mutation-adapter.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import mutationAdapter from '../../src/engine/mutation-adapter.js'; +import mutationAdapter from '../../src/engine/mutation-adapter'; test('spec', t => { t.type(mutationAdapter, 'function'); From 6381752a7a193fd095f6af166fbbea719a674b61 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Sun, 10 May 2026 19:58:52 +0800 Subject: [PATCH 10/30] :bug: fix(vm): dts generation strategy Signed-off-by: SimonShiki --- packages/vm/tsconfig.dts.json | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/vm/tsconfig.dts.json b/packages/vm/tsconfig.dts.json index 440768f2..04fc4e66 100644 --- a/packages/vm/tsconfig.dts.json +++ b/packages/vm/tsconfig.dts.json @@ -4,24 +4,21 @@ "./src/**/*" ], "exclude": [ - "node_modules" + "node_modules", + "./test/**/*" ], "compilerOptions": { - // Tells TypeScript to read JS files, as - // normally they are ignored as source files + "target": "esnext", + "module": "esnext", + "moduleResolution": "bundler", "allowJs": true, - // Generate d.ts files + "checkJs": false, "declaration": true, - // This compiler run should - // only output d.ts files "emitDeclarationOnly": true, - // Types should go into this directory. - // Removing this would place the .d.ts files - // next to the .js files - "outDir": "./dist/types/", - // go to js file when using IDE functions like - // "Go to Definition" in VSCode "declarationMap": true, + "esModuleInterop": true, + "jsx": "react", + "outDir": "./dist/types/", "skipLibCheck": true } } From 8e9c61928485514b26ba9da61f17bb3ebde44b91 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Sun, 10 May 2026 20:31:48 +0800 Subject: [PATCH 11/30] :wrench: chore(vm): migrate some io adapters Signed-off-by: SimonShiki --- packages/vm/src/engine/runtime.js | 20 ++++--- packages/vm/src/io/{clock.js => clock.ts} | 29 +++++----- .../vm/src/io/{joystick.js => joystick.ts} | 24 ++++---- .../vm/src/io/{keyboard.js => keyboard.ts} | 57 +++++++++---------- .../src/io/{mouseWheel.js => mouseWheel.ts} | 15 ++--- .../vm/src/io/{userData.js => userData.ts} | 13 ++--- 6 files changed, 83 insertions(+), 75 deletions(-) rename packages/vm/src/io/{clock.js => clock.ts} (64%) rename packages/vm/src/io/{joystick.js => joystick.ts} (65%) rename packages/vm/src/io/{keyboard.js => keyboard.ts} (73%) rename packages/vm/src/io/{mouseWheel.js => mouseWheel.ts} (64%) rename packages/vm/src/io/{userData.js => userData.ts} (51%) diff --git a/packages/vm/src/engine/runtime.js b/packages/vm/src/engine/runtime.js index ea33ee9e..348e3137 100644 --- a/packages/vm/src/engine/runtime.js +++ b/packages/vm/src/engine/runtime.js @@ -17,15 +17,15 @@ import Variable from './variable'; import xmlEscape from '../util/xml-escape'; import ScratchLinkWebSocket from '../util/scratch-link-websocket'; -import Clock from '../io/clock.js'; +import Clock from '../io/clock'; import Cloud from '../io/cloud.js'; -import Keyboard from '../io/keyboard.js'; +import Keyboard from '../io/keyboard'; import Mouse from '../io/mouse.js'; -import MouseWheel from '../io/mouseWheel.js'; -import UserData from '../io/userData.js'; +import MouseWheel from '../io/mouseWheel'; +import UserData from '../io/userData'; import Video from '../io/video.js'; -import Joystick from '../io/joystick.js'; +import Joystick from '../io/joystick'; import StringUtil from '../util/string-util'; import uid from '../util/uid'; import control from '../blocks/scratch3_control.js'; @@ -55,7 +55,7 @@ const defaultBlockPackages = { const defaultExtensionColors = ['#0FBD8C', '#0DA57A', '#0B8E69']; /** - * @typedef {import('./target')} Target + * @typedef {import('./target').default} Target * @typedef {import('clipcc-audio')} AudioEngine * @typedef {import('clipcc-render')} RenderWebGL * @typedef {import('clipcc-storage').ScratchStorage} ScratchStorage @@ -391,6 +391,12 @@ class Runtime extends EventEmitter { constructor () { super(); + /** + * Current time in milliseconds, used for determining elapsed time and for scheduling future tasks. + * @type {number} + */ + this.currentMSecs = 0; + /** * Target management and storage. * @type {Array.} @@ -2093,7 +2099,7 @@ class Runtime extends EventEmitter { /** * Start all relevant hats. * @param {!string} requestedHatOpcode Opcode of hats to start. - * @param {object=} optMatchFields Optionally, fields to match on the hat. + * @param {object= | null} optMatchFields Optionally, fields to match on the hat. * @param {Target=} optTarget Optionally, a target to restrict to. * @returns {Array.|undefined} List of threads started by this function. */ diff --git a/packages/vm/src/io/clock.js b/packages/vm/src/io/clock.ts similarity index 64% rename from packages/vm/src/io/clock.js rename to packages/vm/src/io/clock.ts index 6c0e2194..3d3aecb3 100644 --- a/packages/vm/src/io/clock.js +++ b/packages/vm/src/io/clock.ts @@ -1,37 +1,40 @@ import Timer from '../util/timer'; +import type Runtime from '../engine/runtime'; class Clock { - constructor (runtime) { - this._projectTimer = new Timer({now: () => runtime.currentMSecs}); - this._projectTimer.start(); - this._pausedTime = null; - this._paused = false; + _projectTimer: Timer; + _pausedTime: number | null = null; + _paused = false; + + constructor ( /** * Reference to the owning Runtime. - * @type {!Runtime} */ - this.runtime = runtime; + public runtime: Runtime + ) { + this._projectTimer = new Timer({now: () => runtime.currentMSecs}); + this._projectTimer.start(); } - projectTimer () { + projectTimer (): number { if (this._paused) { - return this._pausedTime / 1000; + return this._pausedTime! / 1000; } return this._projectTimer.timeElapsed() / 1000; } - pause () { + pause (): void { this._paused = true; this._pausedTime = this._projectTimer.timeElapsed(); } - resume () { + resume (): void { this._paused = false; - const dt = this._projectTimer.timeElapsed() - this._pausedTime; + const dt = this._projectTimer.timeElapsed() - this._pausedTime!; this._projectTimer.startTime += dt; } - resetProjectTimer () { + resetProjectTimer (): void { this._projectTimer.start(); } } diff --git a/packages/vm/src/io/joystick.js b/packages/vm/src/io/joystick.ts similarity index 65% rename from packages/vm/src/io/joystick.js rename to packages/vm/src/io/joystick.ts index 82fad58c..36f52865 100644 --- a/packages/vm/src/io/joystick.js +++ b/packages/vm/src/io/joystick.ts @@ -1,31 +1,33 @@ +import type Runtime from '../engine/runtime'; + class Joystick { - constructor (runtime) { - this._x = 0; - this._y = 0; - this._distance = 0; + _x = 0; + _y = 0; + _distance = 0; + + constructor ( /** * Reference to the owning Runtime. * Can be used, for example, to activate hats. - * @type {!Runtime} */ - this.runtime = runtime; - } + public runtime: Runtime + ) {} - postData (data) { + postData (data: Record): void { if (Object.prototype.hasOwnProperty.call(data, 'x')) this._x = data.x; if (Object.prototype.hasOwnProperty.call(data, 'y')) this._y = data.y; if (Object.prototype.hasOwnProperty.call(data, 'distance')) this._distance = data.distance; } - getX () { + getX (): number { return this._x; } - getY () { + getY (): number { return this._y; } - getDistance () { + getDistance (): number { return this._distance; } } diff --git a/packages/vm/src/io/keyboard.js b/packages/vm/src/io/keyboard.ts similarity index 73% rename from packages/vm/src/io/keyboard.js rename to packages/vm/src/io/keyboard.ts index 0b5a7991..63ef8459 100644 --- a/packages/vm/src/io/keyboard.js +++ b/packages/vm/src/io/keyboard.ts @@ -1,8 +1,8 @@ import Cast from '../util/cast'; +import type Runtime from '../engine/runtime'; /** * Names used internally for keys used in scratch, also known as "scratch keys". - * @enum {string} */ const KEY_NAME = { SPACE: 'space', @@ -11,40 +11,39 @@ const KEY_NAME = { RIGHT: 'right arrow', DOWN: 'down arrow', ENTER: 'enter' -}; +} as const; /** * An array of the names of scratch keys. - * @type {Array} */ -const KEY_NAME_LIST = Object.keys(KEY_NAME).map(name => KEY_NAME[name]); +const KEY_NAME_LIST: string[] = Object.keys(KEY_NAME).map(name => KEY_NAME[name as keyof typeof KEY_NAME]); class Keyboard { - constructor (runtime) { - /** - * List of currently pressed scratch keys. - * A scratch key is: - * A key you can press on a keyboard, excluding modifier keys. - * An uppercase string of length one; - * except for special key names for arrow keys and space (e.g. 'left arrow'). - * Can be a non-english unicode letter like: æ ø ש נ 手 廿. - * @type {Array.} - */ - this._keysPressed = []; + /** + * List of currently pressed scratch keys. + * A scratch key is: + * A key you can press on a keyboard, excluding modifier keys. + * An uppercase string of length one; + * except for special key names for arrow keys and space (e.g. 'left arrow'). + * Can be a non-english unicode letter like: æ ø ש נ 手 廿. + * @type {Array.} + */ + _keysPressed: string[] = []; + + constructor ( /** * Reference to the owning Runtime. * Can be used, for example, to activate hats. - * @type {!Runtime} */ - this.runtime = runtime; - } + public runtime: Runtime + ) {} /** * Convert from a keyboard event key name to a Scratch key name. - * @param {string} keyString the input key string. - * @returns {string} the corresponding Scratch key, or an empty string. + * @param keyString the input key string. + * @returns the corresponding Scratch key, or an empty string. */ - _keyStringToScratchKey (keyString) { + _keyStringToScratchKey (keyString: string): string { keyString = Cast.toString(keyString); // Convert space and arrow keys to their Scratch key names. switch (keyString) { @@ -68,10 +67,10 @@ class Keyboard { /** * Convert from a block argument to a Scratch key name. - * @param {string} keyArg the input arg. - * @returns {string} the corresponding Scratch key. + * @param keyArg the input arg. + * @returns the corresponding Scratch key. */ - _keyArgToScratchKey (keyArg) { + _keyArgToScratchKey (keyArg: string | number): string { // If a number was dropped in, try to convert from ASCII to Scratch key. if (typeof keyArg === 'number') { // Check for the ASCII range containing numbers, some punctuation, @@ -110,9 +109,9 @@ class Keyboard { /** * Keyboard DOM event handler. - * @param {object} data Data from DOM event. + * @param data Data from DOM event. */ - postData (data) { + postData (data: { key: string; isDown: boolean }): void { if (!data.key) return; const scratchKey = this._keyStringToScratchKey(data.key); if (scratchKey === '') return; @@ -131,10 +130,10 @@ class Keyboard { /** * Get key down state for a specified key. - * @param {any} keyArg key argument. - * @returns {boolean} Is the specified key down? + * @param keyArg key argument. + * @returns Is the specified key down? */ - getKeyIsDown (keyArg) { + getKeyIsDown (keyArg: string | number): boolean { if (keyArg === 'any') { return this._keysPressed.length > 0; } diff --git a/packages/vm/src/io/mouseWheel.js b/packages/vm/src/io/mouseWheel.ts similarity index 64% rename from packages/vm/src/io/mouseWheel.js rename to packages/vm/src/io/mouseWheel.ts index ddc7543d..827edde2 100644 --- a/packages/vm/src/io/mouseWheel.js +++ b/packages/vm/src/io/mouseWheel.ts @@ -1,18 +1,19 @@ +import type Runtime from '../engine/runtime'; + class MouseWheel { - constructor (runtime) { + constructor ( /** * Reference to the owning Runtime. - * @type {!Runtime} */ - this.runtime = runtime; - } + public runtime: Runtime + ) {} /** * Mouse wheel DOM event handler. - * @param {object} data Data from DOM event. + * @param data Data from DOM event. */ - postData (data) { - const matchFields = {}; + postData (data: { deltaY: number }): void { + const matchFields: Record = {}; if (data.deltaY < 0) { matchFields.KEY_OPTION = 'up arrow'; } else if (data.deltaY > 0) { diff --git a/packages/vm/src/io/userData.js b/packages/vm/src/io/userData.ts similarity index 51% rename from packages/vm/src/io/userData.js rename to packages/vm/src/io/userData.ts index 1f38b4db..90f80fd0 100644 --- a/packages/vm/src/io/userData.js +++ b/packages/vm/src/io/userData.ts @@ -1,22 +1,19 @@ class UserData { - constructor () { - this._username = ''; - } + _username = ''; /** * Handler for updating the username - * @param {object} data Data posted to this ioDevice. - * @property {!string} username The new username. + * @param data Data posted to this ioDevice. */ - postData (data) { + postData (data: {username: string}): void { this._username = data.username; } /** * Getter for username. Initially empty string, until set via postData. - * @returns {!string} The current username + * @returns The current username */ - getUsername () { + getUsername (): string { return this._username; } } From 13c08d1ff4fb1b5e5e985f455b9141c54cffefa9 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Sun, 10 May 2026 20:59:40 +0800 Subject: [PATCH 12/30] :wrench: chore(vm): migrate dispatchers Signed-off-by: SimonShiki --- packages/vm/package.json | 1 + packages/vm/src/dispatch/central-dispatch.js | 140 ----------- packages/vm/src/dispatch/central-dispatch.ts | 128 ++++++++++ packages/vm/src/dispatch/shared-dispatch.js | 230 ----------------- packages/vm/src/dispatch/shared-dispatch.ts | 233 ++++++++++++++++++ ...{worker-dispatch.js => worker-dispatch.ts} | 84 +++---- .../src/extension-support/define-messages.js | 18 -- .../src/extension-support/define-messages.ts | 20 ++ .../extension-support/extension-manager.js | 2 +- .../src/extension-support/extension-worker.js | 2 +- packages/vm/src/virtual-machine.js | 2 +- .../vm/test/fixtures/dispatch-test-worker.js | 2 +- .../vm/test/integration/internal-extension.js | 2 +- .../vm/test/integration/load-extensions.js | 2 +- packages/vm/test/integration/pen.js | 2 +- .../vm/test/integration/saythink-and-wait.js | 2 +- packages/vm/test/integration/sound.js | 2 +- packages/vm/test/unit/dispatch.js | 2 +- pnpm-lock.yaml | 3 + 19 files changed, 435 insertions(+), 442 deletions(-) delete mode 100644 packages/vm/src/dispatch/central-dispatch.js create mode 100644 packages/vm/src/dispatch/central-dispatch.ts delete mode 100644 packages/vm/src/dispatch/shared-dispatch.js create mode 100644 packages/vm/src/dispatch/shared-dispatch.ts rename packages/vm/src/dispatch/{worker-dispatch.js => worker-dispatch.ts} (50%) delete mode 100644 packages/vm/src/extension-support/define-messages.js create mode 100644 packages/vm/src/extension-support/define-messages.ts diff --git a/packages/vm/package.json b/packages/vm/package.json index 8a7f8cf4..6856559a 100644 --- a/packages/vm/package.json +++ b/packages/vm/package.json @@ -75,6 +75,7 @@ "clipcc-svg-renderer": "workspace:~", "codingclip-worker-loader": "^3.0.10", "copy-webpack-plugin": "^14.0.0", + "domhandler": "^5.0.3", "eslint": "^9.39.2", "eslint-config-clipcc": "workspace:*", "format-message-cli": "6.2.4", diff --git a/packages/vm/src/dispatch/central-dispatch.js b/packages/vm/src/dispatch/central-dispatch.js deleted file mode 100644 index cb1ff9d9..00000000 --- a/packages/vm/src/dispatch/central-dispatch.js +++ /dev/null @@ -1,140 +0,0 @@ -import SharedDispatch from './shared-dispatch.js'; -import log from '../util/log'; - -/** - * This class serves as the central broker for message dispatch. It expects to operate on the main thread / Window and - * it must be informed of any Worker threads which will participate in the messaging system. From any context in the - * messaging system, the dispatcher's "call" method can call any method on any "service" provided in any participating - * context. The dispatch system will forward function arguments and return values across worker boundaries as needed. - * @see {WorkerDispatch} - */ -class CentralDispatch extends SharedDispatch { - constructor () { - super(); - - /** - * Map of channel name to worker or local service provider. - * If the entry is a Worker, the service is provided by an object on that worker. - * Otherwise, the service is provided locally and methods on the service will be called directly. - * @see {setService} - * @type {Record} - */ - this.services = {}; - - /** - * The constructor we will use to recognize workers. - * @type {Function} - */ - this.workerClass = (typeof Worker === 'undefined' ? null : Worker); - - /** - * List of workers attached to this dispatcher. - * @type {Array} - */ - this.workers = []; - } - - /** - * Synchronously call a particular method on a particular service provided locally. - * Calling this function on a remote service will fail. - * @param {string} service - the name of the service. - * @param {string} method - the name of the method. - * @param {*} [args] - the arguments to be copied to the method, if any. - * @returns {*} - the return value of the service method. - */ - callSync (service, method, ...args) { - const {provider, isRemote} = this._getServiceProvider(service); - if (provider) { - if (isRemote) { - throw new Error(`Cannot use 'callSync' on remote provider for service ${service}.`); - } - - return provider[method](...args); - } - throw new Error(`Provider not found for service: ${service}`); - } - - /** - * Synchronously set a local object as the global provider of the specified service. - * WARNING: Any method on the provider can be called from any worker within the dispatch system. - * @param {string} service - a globally unique string identifying this service. Examples: 'vm', 'gui', 'extension9'. - * @param {object} provider - a local object which provides this service. - */ - setServiceSync (service, provider) { - if (Object.prototype.hasOwnProperty.call(this.services, service)) { - log.warn(`Central dispatch replacing existing service provider for ${service}`); - } - this.services[service] = provider; - } - - /** - * Set a local object as the global provider of the specified service. - * WARNING: Any method on the provider can be called from any worker within the dispatch system. - * @param {string} service - a globally unique string identifying this service. Examples: 'vm', 'gui', 'extension9'. - * @param {object} provider - a local object which provides this service. - * @returns {Promise} - a promise which will resolve once the service is registered. - */ - setService (service, provider) { - /** Return a promise for consistency with {@link WorkerDispatch#setService} */ - try { - this.setServiceSync(service, provider); - return Promise.resolve(); - } catch (e) { - return Promise.reject(e); - } - } - - /** - * Add a worker to the message dispatch system. The worker must implement a compatible message dispatch framework. - * The dispatcher will immediately attempt to "handshake" with the worker. - * @param {Worker} worker - the worker to add into the dispatch system. - */ - addWorker (worker) { - if (this.workers.indexOf(worker) === -1) { - this.workers.push(worker); - worker.onmessage = this._onMessage.bind(this, worker); - this._remoteCall(worker, 'dispatch', 'handshake').catch(e => { - log.error(`Could not handshake with worker: ${JSON.stringify(e)}`); - }); - } else { - log.warn('Central dispatch ignoring attempt to add duplicate worker'); - } - } - - /** - * Fetch the service provider object for a particular service name. - * @override - * @param {string} service - the name of the service to look up - * @returns {{provider:(object|Worker), isRemote:boolean}} - the means to contact the service, if found - * @protected - */ - _getServiceProvider (service) { - const provider = this.services[service]; - return provider && { - provider, - isRemote: Boolean(this.workerClass && provider instanceof this.workerClass) - }; - } - - /** - * Handle a call message sent to the dispatch service itself - * @override - * @param {Worker} worker - the worker which sent the message. - * @param {DispatchCallMessage} message - the message to be handled. - * @returns {Promise|undefined} - a promise for the results of this operation, if appropriate - * @protected - */ - _onDispatchMessage (worker, message) { - let promise; - switch (message.method) { - case 'setService': - promise = this.setService(message.args[0], worker); - break; - default: - log.error(`Central dispatch received message for unknown method: ${message.method}`); - } - return promise; - } -} - -export default new CentralDispatch(); diff --git a/packages/vm/src/dispatch/central-dispatch.ts b/packages/vm/src/dispatch/central-dispatch.ts new file mode 100644 index 00000000..a377791b --- /dev/null +++ b/packages/vm/src/dispatch/central-dispatch.ts @@ -0,0 +1,128 @@ +import { DispatchCallMessage, SharedDispatch, WorkerLike } from './shared-dispatch'; +import log from '../util/log'; + +/** + * This class serves as the central broker for message dispatch. It expects to operate on the main thread / Window and + * it must be informed of any Worker threads which will participate in the messaging system. From any context in the + * messaging system, the dispatcher's "call" method can call any method on any "service" provided in any participating + * context. The dispatch system will forward function arguments and return values across worker boundaries as needed. + */ +export class CentralDispatch extends SharedDispatch { + /** + * Map of channel name to worker or local service provider. + * If the entry is a Worker, the service is provided by an object on that worker. + * Otherwise, the service is provided locally and methods on the service will be called directly. + */ + private services: Record = {}; + + /** + * The constructor we will use to recognize workers. + */ + private workerClass = (typeof Worker === 'undefined' ? null : Worker); + + /** + * List of workers attached to this dispatcher. + */ + private workers: Worker[] = []; + + /** + * Synchronously call a particular method on a particular service provided locally. + * Calling this function on a remote service will fail. + * @param service The name of the service. + * @param method The name of the method. + * @param args The arguments to be copied to the method, if any. + * @returns The return value of the service method. + */ + callSync(service: string, method: string, ...args: any[]) { + const { provider, isRemote } = this.getServiceProvider(service); + if (provider) { + if (isRemote) { + throw new Error(`Cannot use 'callSync' on remote provider for service ${service}.`); + } + + return (provider as any)[method](...args); + } + throw new Error(`Provider not found for service: ${service}`); + } + + /** + * Synchronously set a local object as the global provider of the specified service. + * WARNING: Any method on the provider can be called from any worker within the dispatch system. + * @param service A globally unique string identifying this service. Examples: 'vm', 'gui', 'extension9'. + * @param provider A local object which provides this service. + */ + setServiceSync(service: string, provider: object) { + if (Object.prototype.hasOwnProperty.call(this.services, service)) { + log.warn(`Central dispatch replacing existing service provider for ${service}`); + } + this.services[service] = provider; + } + + /** + * Set a local object as the global provider of the specified service. + * WARNING: Any method on the provider can be called from any worker within the dispatch system. + * @param service A globally unique string identifying this service. Examples: 'vm', 'gui', 'extension9'. + * @param provider A local object which provides this service. + * @returns A promise which will resolve once the service is registered. + */ + setService(service: string, provider: object): Promise { + /** Return a promise for consistency with {@link WorkerDispatch#setService} */ + try { + this.setServiceSync(service, provider); + return Promise.resolve(); + } catch (e) { + return Promise.reject(e); + } + } + + /** + * Add a worker to the message dispatch system. The worker must implement a compatible message dispatch framework. + * The dispatcher will immediately attempt to "handshake" with the worker. + * @param worker The worker to add into the dispatch system. + */ + addWorker(worker: Worker) { + if (this.workers.indexOf(worker) === -1) { + this.workers.push(worker); + worker.onmessage = this.onMessage.bind(this, worker); + this.remoteCall(worker, 'dispatch', 'handshake').catch(e => { + log.error(`Could not handshake with worker: ${JSON.stringify(e)}`); + }); + } else { + log.warn('Central dispatch ignoring attempt to add duplicate worker'); + } + } + + /** + * Fetch the service provider object for a particular service name. + * @param service The name of the service to look up. + * @returns The means to contact the service, if found. + */ + protected override getServiceProvider(service: string) { + const provider = this.services[service]; + const isRemote = Boolean(this.workerClass && provider instanceof this.workerClass); + return { + provider, + isRemote + }; + } + + /** + * Handle a call message sent to the dispatch service itself. + * @param worker The worker which sent the message. + * @param message The message to be handled. + * @returns A promise for the results of this operation, if appropriate. + */ + protected override onDispatchMessage(worker: WorkerLike, message: DispatchCallMessage) { + let promise; + switch (message.method) { + case 'setService': + promise = this.setService(message.args[0], worker); + break; + default: + log.error(`Central dispatch received message for unknown method: ${message.method}`); + } + return promise; + } +} + +export default new CentralDispatch(); diff --git a/packages/vm/src/dispatch/shared-dispatch.js b/packages/vm/src/dispatch/shared-dispatch.js deleted file mode 100644 index d98fc2be..00000000 --- a/packages/vm/src/dispatch/shared-dispatch.js +++ /dev/null @@ -1,230 +0,0 @@ -import log from '../util/log'; - -/** - * @typedef {object} DispatchCallMessage - a message to the dispatch system representing a service method call - * @property {*} responseId - send a response message with this response ID. See {@link DispatchResponseMessage} - * @property {string} service - the name of the service to be called - * @property {string} method - the name of the method to be called - * @property {Array|undefined} args - the arguments to be passed to the method - */ - -/** - * @typedef {object} DispatchResponseMessage - a message to the dispatch system representing the results of a call - * @property {*} responseId - a copy of the response ID from the call which generated this response - * @property {*|undefined} error - if this is truthy, then it contains results from a failed call (such as an exception) - * @property {*|undefined} result - if error is not truthy, then this contains the return value of the call (if any) - */ - -/** - * @typedef {DispatchCallMessage|DispatchResponseMessage} DispatchMessage - * Any message to the dispatch system. - */ - -/** - * The SharedDispatch class is responsible for dispatch features shared by - * {@link CentralDispatch} and {@link WorkerDispatch}. - */ -class SharedDispatch { - constructor () { - /** - * List of callback registrations for promises waiting for a response from a call to a service on another - * worker. A callback registration is an array of [resolve,reject] Promise functions. - * Calls to local services don't enter this list. - * @type {Array.} - */ - this.callbacks = []; - - /** - * The next response ID to be used. - * @type {int} - */ - this.nextResponseId = 0; - } - - /** - * Call a particular method on a particular service, regardless of whether that service is provided locally or on - * a worker. If the service is provided by a worker, the `args` will be copied using the Structured Clone - * algorithm, except for any items which are also in the `transfer` list. Ownership of those items will be - * transferred to the worker, and they should not be used after this call. - * @example - * dispatcher.call('vm', 'setData', 'cat', 42); - * // this finds the worker for the 'vm' service, then on that worker calls: - * vm.setData('cat', 42); - * @param {string} service - the name of the service. - * @param {string} method - the name of the method. - * @param {*} [args] - the arguments to be copied to the method, if any. - * @returns {Promise} - a promise for the return value of the service method. - */ - call (service, method, ...args) { - return this.transferCall(service, method, null, ...args); - } - - /** - * Call a particular method on a particular service, regardless of whether that service is provided locally or on - * a worker. If the service is provided by a worker, the `args` will be copied using the Structured Clone - * algorithm, except for any items which are also in the `transfer` list. Ownership of those items will be - * transferred to the worker, and they should not be used after this call. - * @example - * dispatcher.transferCall('vm', 'setData', [myArrayBuffer], 'cat', myArrayBuffer); - * // this finds the worker for the 'vm' service, transfers `myArrayBuffer` to it, then on that worker calls: - * vm.setData('cat', myArrayBuffer); - * @param {string} service - the name of the service. - * @param {string} method - the name of the method. - * @param {Array} [transfer] - objects to be transferred instead of copied. Must be present in `args` to be useful. - * @param {*} [args] - the arguments to be copied to the method, if any. - * @returns {Promise} - a promise for the return value of the service method. - */ - transferCall (service, method, transfer, ...args) { - try { - const {provider, isRemote} = this._getServiceProvider(service); - if (provider) { - if (isRemote) { - return this._remoteTransferCall(provider, service, method, transfer, ...args); - } - - const result = provider[method](...args); - return Promise.resolve(result); - } - return Promise.reject(new Error(`Service not found: ${service}`)); - } catch (e) { - return Promise.reject(e); - } - } - - /** - * Check if a particular service lives on another worker. - * @param {string} service - the service to check. - * @returns {boolean} - true if the service is remote (calls must cross a Worker boundary), false otherwise. - * @private - */ - _isRemoteService (service) { - return this._getServiceProvider(service).isRemote; - } - - /** - * Like {@link call}, but force the call to be posted through a particular communication channel. - * @param {object} provider - send the call through this object's `postMessage` function. - * @param {string} service - the name of the service. - * @param {string} method - the name of the method. - * @param {*} [args] - the arguments to be copied to the method, if any. - * @returns {Promise} - a promise for the return value of the service method. - */ - _remoteCall (provider, service, method, ...args) { - return this._remoteTransferCall(provider, service, method, null, ...args); - } - - /** - * Like {@link transferCall}, but force the call to be posted through a particular communication channel. - * @param {object} provider - send the call through this object's `postMessage` function. - * @param {string} service - the name of the service. - * @param {string} method - the name of the method. - * @param {Array} [transfer] - objects to be transferred instead of copied. Must be present in `args` to be useful. - * @param {*} [args] - the arguments to be copied to the method, if any. - * @returns {Promise} - a promise for the return value of the service method. - */ - _remoteTransferCall (provider, service, method, transfer, ...args) { - return new Promise((resolve, reject) => { - const responseId = this._storeCallbacks(resolve, reject); - - args = JSON.parse(JSON.stringify(args)); - - if (transfer) { - provider.postMessage({service, method, responseId, args}, transfer); - } else { - provider.postMessage({service, method, responseId, args}); - } - }); - } - - /** - * Store callback functions pending a response message. - * @param {Function} resolve - function to call if the service method returns. - * @param {Function} reject - function to call if the service method throws. - * @returns {*} - a unique response ID for this set of callbacks. See {@link _deliverResponse}. - * @protected - */ - _storeCallbacks (resolve, reject) { - const responseId = this.nextResponseId++; - this.callbacks[responseId] = [resolve, reject]; - return responseId; - } - - /** - * Deliver call response from a worker. This should only be called as the result of a message from a worker. - * @param {int} responseId - the response ID of the callback set to call. - * @param {DispatchResponseMessage} message - the message containing the response value(s). - * @protected - */ - _deliverResponse (responseId, message) { - try { - const [resolve, reject] = this.callbacks[responseId]; - delete this.callbacks[responseId]; - if (message.error) { - reject(message.error); - } else { - resolve(message.result); - } - } catch (e) { - log.error(`Dispatch callback failed: ${JSON.stringify(e)}`); - } - } - - /** - * Handle a message event received from a connected worker. - * @param {Worker} worker - the worker which sent the message, or the global object if running in a worker. - * @param {MessageEvent} event - the message event to be handled. - * @protected - */ - _onMessage (worker, event) { - /** @type {DispatchMessage} */ - const message = event.data; - message.args = message.args || []; - let promise; - if (message.service) { - if (message.service === 'dispatch') { - promise = this._onDispatchMessage(worker, message); - } else { - promise = this.call(message.service, message.method, ...message.args); - } - } else if (typeof message.responseId === 'undefined') { - log.error(`Dispatch caught malformed message from a worker: ${JSON.stringify(event)}`); - } else { - this._deliverResponse(message.responseId, message); - } - if (promise) { - if (typeof message.responseId === 'undefined') { - log.error(`Dispatch message missing required response ID: ${JSON.stringify(event)}`); - } else { - promise.then( - result => worker.postMessage({responseId: message.responseId, result}), - error => worker.postMessage({responseId: message.responseId, error}) - ); - } - } - } - - /** - * Fetch the service provider object for a particular service name. - * @abstract - * @param {string} service - the name of the service to look up - * @returns {{provider:(object|Worker), isRemote:boolean}} - the means to contact the service, if found - * @protected - */ - _getServiceProvider (service) { - throw new Error(`Could not get provider for ${service}: _getServiceProvider not implemented`); - } - - /** - * Handle a call message sent to the dispatch service itself - * @abstract - * @param {Worker} worker - the worker which sent the message. - * @param {DispatchCallMessage} message - the message to be handled. - * @returns {Promise|undefined} - a promise for the results of this operation, if appropriate - * @private - */ - _onDispatchMessage (worker, message) { - throw new Error(`Unimplemented dispatch message handler cannot handle ${message.method} method`); - } -} - -export default SharedDispatch; diff --git a/packages/vm/src/dispatch/shared-dispatch.ts b/packages/vm/src/dispatch/shared-dispatch.ts new file mode 100644 index 00000000..bce237dd --- /dev/null +++ b/packages/vm/src/dispatch/shared-dispatch.ts @@ -0,0 +1,233 @@ +import log from '../util/log'; + +/** + * A message to the dispatch system representing a service method call. + */ +export interface DispatchCallMessage { + /** Send a response message with this response ID. */ + responseId: number; + /** The name of the service to be called. */ + service: string; + /** The name of the method to be called. */ + method: string; + /** The arguments to be passed to the method. */ + args: any[]; +} + +/** + * A message to the dispatch system representing the results of a call. + */ +export interface DispatchResponseMessage { + /** A copy of the response ID from the call which generated this response. */ + responseId: number; + /** If this is truthy, then it contains results from a failed call (such as an exception). */ + error?: any; + /** If error is not truthy, then this contains the return value of the call (if any). */ + result?: any; +} + +/** Any message to the dispatch system. */ +export type DispatchMessage = DispatchCallMessage | DispatchResponseMessage; + +function isDispatchCallMessage(obj: DispatchMessage): obj is DispatchCallMessage { + return 'service' in obj; +} + +export type Resolve = (value: any | PromiseLike) => void; +export type Reject = (reason?: any) => void; + +export type WorkerLike = Worker | { + postMessage(...args: any[]): void; +}; + +/** + * The SharedDispatch class is responsible for dispatch features shared by CentralDispatch and WorkerDispatch. + */ +export abstract class SharedDispatch { + /** + * List of callback registrations for promises waiting for a response from a call to a service on another + * worker. A callback registration is an array of [resolve,reject] Promise functions. + * Calls to local services don't enter this list. + */ + private callbacks: [Resolve, Reject][] = []; + + /** + * The next response ID to be used. + */ + private nextResponseId: number = 0; + + /** + * Call a particular method on a particular service, regardless of whether that service is provided locally or on + * a worker. If the service is provided by a worker, the `args` will be copied using the Structured Clone + * algorithm, except for any items which are also in the `transfer` list. Ownership of those items will be + * transferred to the worker, and they should not be used after this call. + * @example + * dispatcher.call('vm', 'setData', 'cat', 42); + * // this finds the worker for the 'vm' service, then on that worker calls: + * vm.setData('cat', 42); + * @param service The name of the service. + * @param method The name of the method. + * @param args The arguments to be copied to the method, if any. + * @returns A promise for the return value of the service method. + */ + call(service: string, method: string, ...args: any[]): Promise { + return this.transferCall(service, method, null, ...args); + } + + /** + * Call a particular method on a particular service, regardless of whether that service is provided locally or on + * a worker. If the service is provided by a worker, the `args` will be copied using the Structured Clone + * algorithm, except for any items which are also in the `transfer` list. Ownership of those items will be + * transferred to the worker, and they should not be used after this call. + * @example + * dispatcher.transferCall('vm', 'setData', [myArrayBuffer], 'cat', myArrayBuffer); + * // this finds the worker for the 'vm' service, transfers `myArrayBuffer` to it, then on that worker calls: + * vm.setData('cat', myArrayBuffer); + * @param service The name of the service. + * @param method The name of the method. + * @param transfer Objects to be transferred instead of copied. Must be present in `args` to be useful. + * @param args The arguments to be copied to the method, if any. + * @returns A promise for the return value of the service method. + */ + transferCall(service: string, method: string, transfer: object[] | null, ...args: any[]): Promise { + try { + const { provider, isRemote } = this.getServiceProvider(service); + if (provider) { + if (isRemote) { + return this.remoteTransferCall(provider as Worker, service, method, transfer, ...args); + } + + const result = (provider as any)[method](...args); + return Promise.resolve(result); + } + return Promise.reject(new Error(`Service not found: ${service}`)); + } catch (e) { + return Promise.reject(e); + } + } + + /** + * Check if a particular service lives on another worker. + * @param service The service to check. + * @returns True if the service is remote (calls must cross a Worker boundary), false otherwise. + */ + private isRemoteService(service: string): boolean { + return this.getServiceProvider(service).isRemote; + } + + /** + * Like `call`, but force the call to be posted through a particular communication channel. + * @param provider Send the call through this object's `postMessage` function. + * @param service The name of the service. + * @param method The name of the method. + * @param args The arguments to be copied to the method, if any. + * @returns A promise for the return value of the service method. + */ + protected remoteCall(provider: WorkerLike, service: string, method: string, ...args: any[]): Promise { + return this.remoteTransferCall(provider, service, method, null, ...args); + } + + /** + * Like `transferCall`, but force the call to be posted through a particular communication channel. + * @param provider Send the call through this object's `postMessage` function. + * @param service The name of the service. + * @param method The name of the method. + * @param transfer Objects to be transferred instead of copied. Must be present in `args` to be useful. + * @param args The arguments to be copied to the method, if any. + * @returns {Promise} - a promise for the return value of the service method. + */ + private remoteTransferCall(provider: WorkerLike, service: string, method: string, transfer: any[] | null, ...args: any[]) { + return new Promise((resolve, reject) => { + const responseId = this.storeCallbacks(resolve, reject); + + args = JSON.parse(JSON.stringify(args)); + + if (transfer) { + provider.postMessage({ service, method, responseId, args }, transfer); + } else { + provider.postMessage({ service, method, responseId, args }); + } + }); + } + + /** + * Store callback functions pending a response message. + * @param resolve Function to call if the service method returns. + * @param reject Function to call if the service method throws. + * @returns A unique response ID for this set of callbacks. + */ + protected storeCallbacks(resolve: Resolve, reject: Reject) { + const responseId = this.nextResponseId++; + this.callbacks[responseId] = [resolve, reject]; + return responseId; + } + + /** + * Deliver call response from a worker. This should only be called as the result of a message from a worker. + * @param responseId The response ID of the callback set to call. + * @param message The message containing the response value(s). + */ + protected deliverResponse(responseId: number, message: DispatchResponseMessage) { + try { + const [resolve, reject] = this.callbacks[responseId]; + delete this.callbacks[responseId]; + if (message.error) { + reject(message.error); + } else { + resolve(message.result); + } + } catch (e) { + log.error(`Dispatch callback failed: ${JSON.stringify(e)}`); + } + } + + /** + * Handle a message event received from a connected worker. + * @param worker The worker which sent the message, or the global object if running in a worker. + * @param event The message event to be handled. + */ + protected onMessage(worker: WorkerLike, event: MessageEvent) { + const message = event.data; + let promise; + if (isDispatchCallMessage(message) && message.service) { + message.args = message.args || []; + if (message.service === 'dispatch') { + promise = this.onDispatchMessage(worker, message); + } else { + promise = this.call(message.service, message.method, ...message.args); + } + } else if (typeof message.responseId === 'undefined') { + log.error(`Dispatch caught malformed message from a worker: ${JSON.stringify(event)}`); + } else { + this.deliverResponse(message.responseId, message); + } + if (promise) { + if (typeof message.responseId === 'undefined') { + log.error(`Dispatch message missing required response ID: ${JSON.stringify(event)}`); + } else { + promise.then( + result => worker.postMessage({ responseId: message.responseId, result }), + error => worker.postMessage({ responseId: message.responseId, error }) + ); + } + } + } + + /** + * Fetch the service provider object for a particular service name. + * @param service The name of the service to look up. + * @returns The means to contact the service, if found. + */ + protected abstract getServiceProvider(service: string): { + provider: Worker | object; + isRemote: boolean; + }; + + /** + * Handle a call message sent to the dispatch service itself + * @param worker The worker which sent the message. + * @param message The message to be handled. + * @returns A promise for the results of this operation, if appropriate + */ + protected abstract onDispatchMessage(worker: WorkerLike, message: DispatchCallMessage): Promise | undefined; +} diff --git a/packages/vm/src/dispatch/worker-dispatch.js b/packages/vm/src/dispatch/worker-dispatch.ts similarity index 50% rename from packages/vm/src/dispatch/worker-dispatch.js rename to packages/vm/src/dispatch/worker-dispatch.ts index 83533b83..64cc8ade 100644 --- a/packages/vm/src/dispatch/worker-dispatch.js +++ b/packages/vm/src/dispatch/worker-dispatch.ts @@ -1,4 +1,4 @@ -import SharedDispatch from './shared-dispatch.js'; +import {DispatchCallMessage, SharedDispatch, WorkerLike} from './shared-dispatch'; import log from '../util/log'; /** @@ -6,72 +6,70 @@ import log from '../util/log'; * From any context in the messaging system, the dispatcher's "call" method can call any method on any "service" * provided in any participating context. The dispatch system will forward function arguments and return values across * worker boundaries as needed. - * @see {CentralDispatch} */ -class WorkerDispatch extends SharedDispatch { - constructor () { +export class WorkerDispatch extends SharedDispatch { + /** + * This promise will be resolved when we have successfully connected to central dispatch. + */ + private connectionPromise: Promise; + + /** + * Called when successfully connected. + */ + private onConnect!: (value: void | PromiseLike) => void; + + /** + * Map of service name to local service provider. + * If a service is not listed here, it is assumed to be provided by another context (another Worker or the main + * thread). + */ + private services: Record = {}; + + constructor() { super(); - /** - * This promise will be resolved when we have successfully connected to central dispatch. - * @type {Promise} - * @see {waitForConnection} - * @private - */ - this._connectionPromise = new Promise(resolve => { - this._onConnect = resolve; + this.connectionPromise = new Promise((resolve) => { + this.onConnect = resolve; }); - /** - * Map of service name to local service provider. - * If a service is not listed here, it is assumed to be provided by another context (another Worker or the main - * thread). - * @see {setService} - * @type {object} - */ - this.services = {}; - - this._onMessage = this._onMessage.bind(this, self); if (typeof self !== 'undefined') { - self.onmessage = this._onMessage; + self.onmessage = this.onMessage.bind(this, self); } } /** - * @returns {Promise} a promise which will resolve upon connection to central dispatch. If you need to make a call + * @returns A promise which will resolve upon connection to central dispatch. If you need to make a call * immediately on "startup" you can attach a 'then' to this promise. * @example * dispatch.waitForConnection.then(() => { * dispatch.call('myService', 'hello'); * }) */ - get waitForConnection () { - return this._connectionPromise; + get waitForConnection() { + return this.connectionPromise; } /** * Set a local object as the global provider of the specified service. * WARNING: Any method on the provider can be called from any worker within the dispatch system. - * @param {string} service - a globally unique string identifying this service. Examples: 'vm', 'gui', 'extension9'. - * @param {object} provider - a local object which provides this service. - * @returns {Promise} - a promise which will resolve once the service is registered. + * @param service A globally unique string identifying this service. Examples: 'vm', 'gui', 'extension9'. + * @param provider A local object which provides this service. + * @returns A promise which will resolve once the service is registered. */ - setService (service, provider) { + setService(service: string, provider: object) { if (Object.prototype.hasOwnProperty.call(this.services, service)) { log.warn(`Worker dispatch replacing existing service provider for ${service}`); } this.services[service] = provider; - return this.waitForConnection.then(() => this._remoteCall(self, 'dispatch', 'setService', service)); + return this.waitForConnection.then(() => this.remoteCall(self, 'dispatch', 'setService', service)); } /** * Fetch the service provider object for a particular service name. - * @override - * @param {string} service - the name of the service to look up - * @returns {{provider:(object|Worker), isRemote:boolean}} - the means to contact the service, if found - * @protected + * @param service The name of the service to look up. + * @returns The means to contact the service, if found. */ - _getServiceProvider (service) { + protected override getServiceProvider(service: string) { // if we don't have a local service by this name, contact central dispatch by calling `postMessage` on self const provider = this.services[service]; return { @@ -81,18 +79,16 @@ class WorkerDispatch extends SharedDispatch { } /** - * Handle a call message sent to the dispatch service itself - * @override - * @param {Worker} worker - the worker which sent the message. - * @param {DispatchCallMessage} message - the message to be handled. - * @returns {Promise|undefined} - a promise for the results of this operation, if appropriate - * @protected + * Handle a call message sent to the dispatch service itself. + * @param worker The worker which sent the message. + * @param message The message to be handled. + * @returns A promise for the results of this operation, if appropriate. */ - _onDispatchMessage (worker, message) { + protected override onDispatchMessage(worker: WorkerLike, message: DispatchCallMessage) { let promise; switch (message.method) { case 'handshake': - promise = this._onConnect(); + promise = Promise.resolve(this.onConnect()); break; case 'terminate': // Don't close until next tick, after sending confirmation back diff --git a/packages/vm/src/extension-support/define-messages.js b/packages/vm/src/extension-support/define-messages.js deleted file mode 100644 index 988e2b40..00000000 --- a/packages/vm/src/extension-support/define-messages.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @typedef {object} MessageDescriptor - * @property {string} id - the translator-friendly unique ID of this message. - * @property {string} default - the message text in the default language (English). - * @property {string} [description] - a description of this message to help translators understand the context. - */ - -/** - * This is a hook for extracting messages from extension source files. - * This function simply returns the message descriptor map object that's passed in. - * @param {Record} messages - the messages to be defined - * @returns {Record} - the input, unprocessed - */ -const defineMessages = function (messages) { - return messages; -}; - -export default defineMessages; diff --git a/packages/vm/src/extension-support/define-messages.ts b/packages/vm/src/extension-support/define-messages.ts new file mode 100644 index 00000000..125dc6e8 --- /dev/null +++ b/packages/vm/src/extension-support/define-messages.ts @@ -0,0 +1,20 @@ +export interface MessageDescriptor { + /** the translator-friendly unique ID of this message */ + id: string; + /** the message text in the default language (English) */ + default: string; + /** a description of this message to help translators understand the context */ + description?: string; +} + +/** + * This is a hook for extracting messages from extension source files. + * This function simply returns the message descriptor map object that's passed in. + * @param messages the messages to be defined + * @returns the input, unprocessed + */ +const defineMessages = function (messages: Record): Record { + return messages; +}; + +export default defineMessages; diff --git a/packages/vm/src/extension-support/extension-manager.js b/packages/vm/src/extension-support/extension-manager.js index a64918b0..cb035c6f 100644 --- a/packages/vm/src/extension-support/extension-manager.js +++ b/packages/vm/src/extension-support/extension-manager.js @@ -1,4 +1,4 @@ -import dispatch from '../dispatch/central-dispatch.js'; +import dispatch from '../dispatch/central-dispatch'; import log from '../util/log'; import maybeFormatMessage from '../util/maybe-format-message'; import BlockType from './block-type'; diff --git a/packages/vm/src/extension-support/extension-worker.js b/packages/vm/src/extension-support/extension-worker.js index f7dce4e4..f1abfcb6 100644 --- a/packages/vm/src/extension-support/extension-worker.js +++ b/packages/vm/src/extension-support/extension-worker.js @@ -1,6 +1,6 @@ import ArgumentType from '../extension-support/argument-type'; import BlockType from '../extension-support/block-type'; -import dispatch from '../dispatch/worker-dispatch.js'; +import dispatch from '../dispatch/worker-dispatch'; import TargetType from './target-type'; class ExtensionWorker { diff --git a/packages/vm/src/virtual-machine.js b/packages/vm/src/virtual-machine.js index a72b1889..abd6f0de 100644 --- a/packages/vm/src/virtual-machine.js +++ b/packages/vm/src/virtual-machine.js @@ -8,7 +8,7 @@ if (typeof TextEncoder === 'undefined') { import EventEmitter from 'events'; import JSZip from 'jszip'; import {Buffer} from 'buffer'; -import centralDispatch from './dispatch/central-dispatch.js'; +import centralDispatch from './dispatch/central-dispatch'; import ExtensionManager from './extension-support/extension-manager.js'; import log from './util/log'; import MathUtil from './util/math-util'; diff --git a/packages/vm/test/fixtures/dispatch-test-worker.js b/packages/vm/test/fixtures/dispatch-test-worker.js index b4e76036..33e97245 100644 --- a/packages/vm/test/fixtures/dispatch-test-worker.js +++ b/packages/vm/test/fixtures/dispatch-test-worker.js @@ -1,4 +1,4 @@ -import dispatch from '../../src/dispatch/worker-dispatch.js'; +import dispatch from '../../src/dispatch/worker-dispatch'; import DispatchTestService from './dispatch-test-service.js'; import log from '../../src/util/log'; diff --git a/packages/vm/test/integration/internal-extension.js b/packages/vm/test/integration/internal-extension.js index dff1ee11..88498b9a 100644 --- a/packages/vm/test/integration/internal-extension.js +++ b/packages/vm/test/integration/internal-extension.js @@ -1,7 +1,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import Worker from 'tiny-worker'; import BlockType from '../../src/extension-support/block-type'; -import dispatch from '../../src/dispatch/central-dispatch.js'; +import dispatch from '../../src/dispatch/central-dispatch'; import VirtualMachine from '../../src/virtual-machine.js'; import Sprite from '../../src/sprites/sprite.js'; import RenderedTarget from '../../src/sprites/rendered-target.js'; diff --git a/packages/vm/test/integration/load-extensions.js b/packages/vm/test/integration/load-extensions.js index 0e18f412..dd70f037 100644 --- a/packages/vm/test/integration/load-extensions.js +++ b/packages/vm/test/integration/load-extensions.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import fs from 'fs'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import dispatch from '../../src/dispatch/central-dispatch.js'; +import dispatch from '../../src/dispatch/central-dispatch'; import VirtualMachine from '../../src/index.js'; /** diff --git a/packages/vm/test/integration/pen.js b/packages/vm/test/integration/pen.js index 8a174b1d..a1038fb3 100644 --- a/packages/vm/test/integration/pen.js +++ b/packages/vm/test/integration/pen.js @@ -3,7 +3,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import Scratch3PenBlocks from '../../src/extensions/scratch3_pen/index.js'; import VirtualMachine from '../../src/index.js'; -import dispatch from '../../src/dispatch/central-dispatch.js'; +import dispatch from '../../src/dispatch/central-dispatch'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; diff --git a/packages/vm/test/integration/saythink-and-wait.js b/packages/vm/test/integration/saythink-and-wait.js index 1fa961eb..2fb9dac9 100644 --- a/packages/vm/test/integration/saythink-and-wait.js +++ b/packages/vm/test/integration/saythink-and-wait.js @@ -4,7 +4,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index.js'; -import dispatch from '../../src/dispatch/central-dispatch.js'; +import dispatch from '../../src/dispatch/central-dispatch'; const uri = path.resolve(__dirname, '../fixtures/saythink-and-wait.sb2'); const project = readFileToBuffer(uri); diff --git a/packages/vm/test/integration/sound.js b/packages/vm/test/integration/sound.js index 2226c0bf..2fe116e6 100644 --- a/packages/vm/test/integration/sound.js +++ b/packages/vm/test/integration/sound.js @@ -4,7 +4,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index.js'; -import dispatch from '../../src/dispatch/central-dispatch.js'; +import dispatch from '../../src/dispatch/central-dispatch'; const uri = path.resolve(__dirname, '../fixtures/sound.sb2'); const project = readFileToBuffer(uri); diff --git a/packages/vm/test/unit/dispatch.js b/packages/vm/test/unit/dispatch.js index 207abe12..f1117171 100644 --- a/packages/vm/test/unit/dispatch.js +++ b/packages/vm/test/unit/dispatch.js @@ -1,6 +1,6 @@ import DispatchTestService from '../fixtures/dispatch-test-service.js'; import Worker from 'tiny-worker'; -import dispatch from '../../src/dispatch/central-dispatch.js'; +import dispatch from '../../src/dispatch/central-dispatch'; import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aae1217b..a1f2c52e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1277,6 +1277,9 @@ importers: copy-webpack-plugin: specifier: ^14.0.0 version: 14.0.0(webpack@5.105.4) + domhandler: + specifier: ^5.0.3 + version: 5.0.3 eslint: specifier: ^9.39.2 version: 9.39.2(jiti@2.6.1) From 468676564472b487f9290d9301ebdcc83f437267 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Sun, 10 May 2026 21:37:35 +0800 Subject: [PATCH 13/30] :bug: fix(vm): tests except worker-related issues Signed-off-by: SimonShiki --- packages/vm/package.json | 2 +- packages/vm/src/dispatch/shared-dispatch.ts | 4 ++-- .../extension-support/extension-manager.js | 2 +- packages/vm/test/unit/dispatch.js | 2 +- packages/vm/test/unit/io_clock.js | 2 +- packages/vm/test/unit/io_joystick.js | 2 +- packages/vm/test/unit/io_keyboard.js | 2 +- packages/vm/test/unit/io_mousewheel.js | 4 ++-- packages/vm/test/unit/io_userData.js | 2 +- packages/vm/tsconfig.dts.json | 24 ------------------- packages/vm/tsconfig.json | 10 ++++---- packages/vm/tsconfig.test.json | 6 ----- 12 files changed, 16 insertions(+), 46 deletions(-) delete mode 100644 packages/vm/tsconfig.dts.json delete mode 100644 packages/vm/tsconfig.test.json diff --git a/packages/vm/package.json b/packages/vm/package.json index 6856559a..24417ed7 100644 --- a/packages/vm/package.json +++ b/packages/vm/package.json @@ -17,7 +17,7 @@ "default": "./src/index.js" }, "scripts": { - "build:types": "tsc --project ./tsconfig.dts.json", + "build:types": "tsc", "build": "pnpm run build:types && webpack --progress --color --bail", "coverage": "jest --silent --coverage", "i18n:src": "mkdirp translations/core && format-message extract --out-file translations/core/en.json src/extensions/**/index.js", diff --git a/packages/vm/src/dispatch/shared-dispatch.ts b/packages/vm/src/dispatch/shared-dispatch.ts index bce237dd..d27717cb 100644 --- a/packages/vm/src/dispatch/shared-dispatch.ts +++ b/packages/vm/src/dispatch/shared-dispatch.ts @@ -111,7 +111,7 @@ export abstract class SharedDispatch { * @param service The service to check. * @returns True if the service is remote (calls must cross a Worker boundary), false otherwise. */ - private isRemoteService(service: string): boolean { + isRemoteService(service: string): boolean { return this.getServiceProvider(service).isRemote; } @@ -123,7 +123,7 @@ export abstract class SharedDispatch { * @param args The arguments to be copied to the method, if any. * @returns A promise for the return value of the service method. */ - protected remoteCall(provider: WorkerLike, service: string, method: string, ...args: any[]): Promise { + remoteCall(provider: WorkerLike, service: string, method: string, ...args: any[]): Promise { return this.remoteTransferCall(provider, service, method, null, ...args); } diff --git a/packages/vm/src/extension-support/extension-manager.js b/packages/vm/src/extension-support/extension-manager.js index cb035c6f..f636ba01 100644 --- a/packages/vm/src/extension-support/extension-manager.js +++ b/packages/vm/src/extension-support/extension-manager.js @@ -415,7 +415,7 @@ class ExtensionManager { args => args && args.mutation && args.mutation.blockInfo : () => blockInfo; const callBlockFunc = (() => { - if (dispatch._isRemoteService(serviceName)) { + if (dispatch.isRemoteService(serviceName)) { return (args, util, realBlockInfo) => dispatch.call(serviceName, funcName, args, util, realBlockInfo); } diff --git a/packages/vm/test/unit/dispatch.js b/packages/vm/test/unit/dispatch.js index f1117171..a3320f2d 100644 --- a/packages/vm/test/unit/dispatch.js +++ b/packages/vm/test/unit/dispatch.js @@ -59,7 +59,7 @@ test('remote', t => { return waitForWorker .then(() => runServiceTest('RemoteDispatchTest', t), e => t.fail(e)) - .then(() => dispatch._remoteCall(worker, 'dispatch', 'terminate'), e => t.fail(e)); + .then(() => dispatch.remoteCall(worker, 'dispatch', 'terminate'), e => t.fail(e)); }); test('local, sync', t => { diff --git a/packages/vm/test/unit/io_clock.js b/packages/vm/test/unit/io_clock.js index 9b08a4fe..2916eb75 100644 --- a/packages/vm/test/unit/io_clock.js +++ b/packages/vm/test/unit/io_clock.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import Clock from '../../src/io/clock.js'; +import Clock from '../../src/io/clock'; import Runtime from '../../src/engine/runtime.js'; test('spec', t => { diff --git a/packages/vm/test/unit/io_joystick.js b/packages/vm/test/unit/io_joystick.js index 50aeedc2..8c53e478 100644 --- a/packages/vm/test/unit/io_joystick.js +++ b/packages/vm/test/unit/io_joystick.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import Joystick from '../../src/io/joystick.js'; +import Joystick from '../../src/io/joystick'; import Runtime from '../../src/engine/runtime.js'; test('spec', t => { diff --git a/packages/vm/test/unit/io_keyboard.js b/packages/vm/test/unit/io_keyboard.js index 821370c7..63af5419 100644 --- a/packages/vm/test/unit/io_keyboard.js +++ b/packages/vm/test/unit/io_keyboard.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import Keyboard from '../../src/io/keyboard.js'; +import Keyboard from '../../src/io/keyboard'; import Runtime from '../../src/engine/runtime.js'; test('spec', t => { diff --git a/packages/vm/test/unit/io_mousewheel.js b/packages/vm/test/unit/io_mousewheel.js index b25cff6c..709d18c3 100644 --- a/packages/vm/test/unit/io_mousewheel.js +++ b/packages/vm/test/unit/io_mousewheel.js @@ -1,6 +1,6 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import MouseWheel from '../../src/io/mouseWheel.js'; -import Runtime from '../../src/engine/runtime.js'; +import MouseWheel from '../../src/io/mouseWheel'; +import Runtime from '../../src/engine/runtime'; test('spec', t => { const rt = new Runtime(); diff --git a/packages/vm/test/unit/io_userData.js b/packages/vm/test/unit/io_userData.js index 35e2b15a..470a9548 100644 --- a/packages/vm/test/unit/io_userData.js +++ b/packages/vm/test/unit/io_userData.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import UserData from '../../src/io/userData.js'; +import UserData from '../../src/io/userData'; test('spec', t => { const userData = new UserData(); diff --git a/packages/vm/tsconfig.dts.json b/packages/vm/tsconfig.dts.json deleted file mode 100644 index 04fc4e66..00000000 --- a/packages/vm/tsconfig.dts.json +++ /dev/null @@ -1,24 +0,0 @@ -// Reference: https://www.typescriptlang.org/docs/handbook/declaration-files/dts-from-js.html -{ - "include": [ - "./src/**/*" - ], - "exclude": [ - "node_modules", - "./test/**/*" - ], - "compilerOptions": { - "target": "esnext", - "module": "esnext", - "moduleResolution": "bundler", - "allowJs": true, - "checkJs": false, - "declaration": true, - "emitDeclarationOnly": true, - "declarationMap": true, - "esModuleInterop": true, - "jsx": "react", - "outDir": "./dist/types/", - "skipLibCheck": true - } -} diff --git a/packages/vm/tsconfig.json b/packages/vm/tsconfig.json index a94dc5ab..b48bdc4c 100644 --- a/packages/vm/tsconfig.json +++ b/packages/vm/tsconfig.json @@ -53,13 +53,13 @@ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + "declarationMap": true, /* Create sourcemaps for d.ts files. */ + "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./dist/", /* Specify an output folder for all emitted files. */ + "outDir": "./dist/types/", /* Specify an output folder for all emitted files. */ // "removeComments": true, /* Disable emitting comments. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ diff --git a/packages/vm/tsconfig.test.json b/packages/vm/tsconfig.test.json deleted file mode 100644 index fe5d727b..00000000 --- a/packages/vm/tsconfig.test.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "allowJs": false - } -} From 287d9836da0abf69792923658430b8412911a425 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Sun, 10 May 2026 21:58:41 +0800 Subject: [PATCH 14/30] :art: chore(vm): fix lint Signed-off-by: SimonShiki --- packages/vm/eslint.config.js | 6 +- packages/vm/src/blocks/category_prototype.ts | 1 + packages/vm/src/dispatch/central-dispatch.ts | 27 +++---- packages/vm/src/dispatch/shared-dispatch.ts | 34 ++++---- packages/vm/src/dispatch/worker-dispatch.ts | 12 +-- packages/vm/src/engine/adapter.ts | 10 ++- packages/vm/src/engine/blocks.js | 4 +- packages/vm/src/engine/profiler.ts | 6 +- packages/vm/src/engine/variable.ts | 3 +- .../extension-support/extension-metadata.ts | 1 + packages/vm/src/io/keyboard.ts | 2 + packages/vm/src/io/mouseWheel.ts | 2 + packages/vm/src/io/userData.ts | 1 + packages/vm/src/serialization/schema.ts | 1 + packages/vm/src/util/cast.ts | 6 +- packages/vm/src/util/color.ts | 78 +++++++++---------- packages/vm/src/util/fetch-with-timeout.ts | 6 +- packages/vm/src/util/jsonrpc.ts | 5 +- packages/vm/src/util/maybe-format-message.ts | 9 ++- packages/vm/src/util/task-queue.ts | 8 +- packages/vm/src/util/variable-util.ts | 13 +++- 21 files changed, 142 insertions(+), 93 deletions(-) diff --git a/packages/vm/eslint.config.js b/packages/vm/eslint.config.js index de024a92..efd6d8ce 100644 --- a/packages/vm/eslint.config.js +++ b/packages/vm/eslint.config.js @@ -1,14 +1,16 @@ const clipccConfig = require('eslint-config-clipcc'); const clipccNode = require('eslint-config-clipcc/node'); const clipccES6 = require('eslint-config-clipcc/es6'); +const clipccTS = require('eslint-config-clipcc/ts'); const globals = require('globals'); module.exports = [ ...clipccConfig, ...clipccNode, ...clipccES6, + ...clipccTS, { - files: ['src/**/*.js'], + files: ['src/**/*.{js,ts}'], languageOptions: { globals: { ...globals.browser @@ -28,7 +30,7 @@ module.exports = [ } }, { - files: ['test/**/*.js'], + files: ['test/**/*.{js,ts}'], languageOptions: { globals: { ...globals.browser, diff --git a/packages/vm/src/blocks/category_prototype.ts b/packages/vm/src/blocks/category_prototype.ts index 2db9dc94..e1d96a8b 100644 --- a/packages/vm/src/blocks/category_prototype.ts +++ b/packages/vm/src/blocks/category_prototype.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import type Runtime from '../engine/runtime'; import type BlockUtility from '../engine/block-utility'; import type {HatMetadata, MonitorBlockInfo} from '../engine/runtime'; diff --git a/packages/vm/src/dispatch/central-dispatch.ts b/packages/vm/src/dispatch/central-dispatch.ts index a377791b..6f753037 100644 --- a/packages/vm/src/dispatch/central-dispatch.ts +++ b/packages/vm/src/dispatch/central-dispatch.ts @@ -1,4 +1,5 @@ -import { DispatchCallMessage, SharedDispatch, WorkerLike } from './shared-dispatch'; +/* eslint-disable @typescript-eslint/no-explicit-any */ +import {DispatchCallMessage, SharedDispatch, WorkerLike} from './shared-dispatch'; import log from '../util/log'; /** @@ -33,8 +34,8 @@ export class CentralDispatch extends SharedDispatch { * @param args The arguments to be copied to the method, if any. * @returns The return value of the service method. */ - callSync(service: string, method: string, ...args: any[]) { - const { provider, isRemote } = this.getServiceProvider(service); + callSync (service: string, method: string, ...args: any[]) { + const {provider, isRemote} = this.getServiceProvider(service); if (provider) { if (isRemote) { throw new Error(`Cannot use 'callSync' on remote provider for service ${service}.`); @@ -51,7 +52,7 @@ export class CentralDispatch extends SharedDispatch { * @param service A globally unique string identifying this service. Examples: 'vm', 'gui', 'extension9'. * @param provider A local object which provides this service. */ - setServiceSync(service: string, provider: object) { + setServiceSync (service: string, provider: object) { if (Object.prototype.hasOwnProperty.call(this.services, service)) { log.warn(`Central dispatch replacing existing service provider for ${service}`); } @@ -65,7 +66,7 @@ export class CentralDispatch extends SharedDispatch { * @param provider A local object which provides this service. * @returns A promise which will resolve once the service is registered. */ - setService(service: string, provider: object): Promise { + setService (service: string, provider: object): Promise { /** Return a promise for consistency with {@link WorkerDispatch#setService} */ try { this.setServiceSync(service, provider); @@ -80,7 +81,7 @@ export class CentralDispatch extends SharedDispatch { * The dispatcher will immediately attempt to "handshake" with the worker. * @param worker The worker to add into the dispatch system. */ - addWorker(worker: Worker) { + addWorker (worker: Worker) { if (this.workers.indexOf(worker) === -1) { this.workers.push(worker); worker.onmessage = this.onMessage.bind(this, worker); @@ -97,7 +98,7 @@ export class CentralDispatch extends SharedDispatch { * @param service The name of the service to look up. * @returns The means to contact the service, if found. */ - protected override getServiceProvider(service: string) { + protected override getServiceProvider (service: string) { const provider = this.services[service]; const isRemote = Boolean(this.workerClass && provider instanceof this.workerClass); return { @@ -112,14 +113,14 @@ export class CentralDispatch extends SharedDispatch { * @param message The message to be handled. * @returns A promise for the results of this operation, if appropriate. */ - protected override onDispatchMessage(worker: WorkerLike, message: DispatchCallMessage) { + protected override onDispatchMessage (worker: WorkerLike, message: DispatchCallMessage) { let promise; switch (message.method) { - case 'setService': - promise = this.setService(message.args[0], worker); - break; - default: - log.error(`Central dispatch received message for unknown method: ${message.method}`); + case 'setService': + promise = this.setService(message.args[0], worker); + break; + default: + log.error(`Central dispatch received message for unknown method: ${message.method}`); } return promise; } diff --git a/packages/vm/src/dispatch/shared-dispatch.ts b/packages/vm/src/dispatch/shared-dispatch.ts index d27717cb..6a93fb28 100644 --- a/packages/vm/src/dispatch/shared-dispatch.ts +++ b/packages/vm/src/dispatch/shared-dispatch.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import log from '../util/log'; /** @@ -29,7 +30,7 @@ export interface DispatchResponseMessage { /** Any message to the dispatch system. */ export type DispatchMessage = DispatchCallMessage | DispatchResponseMessage; -function isDispatchCallMessage(obj: DispatchMessage): obj is DispatchCallMessage { +function isDispatchCallMessage (obj: DispatchMessage): obj is DispatchCallMessage { return 'service' in obj; } @@ -70,7 +71,7 @@ export abstract class SharedDispatch { * @param args The arguments to be copied to the method, if any. * @returns A promise for the return value of the service method. */ - call(service: string, method: string, ...args: any[]): Promise { + call (service: string, method: string, ...args: any[]): Promise { return this.transferCall(service, method, null, ...args); } @@ -89,9 +90,9 @@ export abstract class SharedDispatch { * @param args The arguments to be copied to the method, if any. * @returns A promise for the return value of the service method. */ - transferCall(service: string, method: string, transfer: object[] | null, ...args: any[]): Promise { + transferCall (service: string, method: string, transfer: object[] | null, ...args: any[]): Promise { try { - const { provider, isRemote } = this.getServiceProvider(service); + const {provider, isRemote} = this.getServiceProvider(service); if (provider) { if (isRemote) { return this.remoteTransferCall(provider as Worker, service, method, transfer, ...args); @@ -111,7 +112,7 @@ export abstract class SharedDispatch { * @param service The service to check. * @returns True if the service is remote (calls must cross a Worker boundary), false otherwise. */ - isRemoteService(service: string): boolean { + isRemoteService (service: string): boolean { return this.getServiceProvider(service).isRemote; } @@ -123,7 +124,7 @@ export abstract class SharedDispatch { * @param args The arguments to be copied to the method, if any. * @returns A promise for the return value of the service method. */ - remoteCall(provider: WorkerLike, service: string, method: string, ...args: any[]): Promise { + remoteCall (provider: WorkerLike, service: string, method: string, ...args: any[]): Promise { return this.remoteTransferCall(provider, service, method, null, ...args); } @@ -136,16 +137,21 @@ export abstract class SharedDispatch { * @param args The arguments to be copied to the method, if any. * @returns {Promise} - a promise for the return value of the service method. */ - private remoteTransferCall(provider: WorkerLike, service: string, method: string, transfer: any[] | null, ...args: any[]) { + private remoteTransferCall ( + provider: WorkerLike, + service: string, + method: string, + transfer: any[] | null, + ...args: any[]) { return new Promise((resolve, reject) => { const responseId = this.storeCallbacks(resolve, reject); args = JSON.parse(JSON.stringify(args)); if (transfer) { - provider.postMessage({ service, method, responseId, args }, transfer); + provider.postMessage({service, method, responseId, args}, transfer); } else { - provider.postMessage({ service, method, responseId, args }); + provider.postMessage({service, method, responseId, args}); } }); } @@ -156,7 +162,7 @@ export abstract class SharedDispatch { * @param reject Function to call if the service method throws. * @returns A unique response ID for this set of callbacks. */ - protected storeCallbacks(resolve: Resolve, reject: Reject) { + protected storeCallbacks (resolve: Resolve, reject: Reject) { const responseId = this.nextResponseId++; this.callbacks[responseId] = [resolve, reject]; return responseId; @@ -167,7 +173,7 @@ export abstract class SharedDispatch { * @param responseId The response ID of the callback set to call. * @param message The message containing the response value(s). */ - protected deliverResponse(responseId: number, message: DispatchResponseMessage) { + protected deliverResponse (responseId: number, message: DispatchResponseMessage) { try { const [resolve, reject] = this.callbacks[responseId]; delete this.callbacks[responseId]; @@ -186,7 +192,7 @@ export abstract class SharedDispatch { * @param worker The worker which sent the message, or the global object if running in a worker. * @param event The message event to be handled. */ - protected onMessage(worker: WorkerLike, event: MessageEvent) { + protected onMessage (worker: WorkerLike, event: MessageEvent) { const message = event.data; let promise; if (isDispatchCallMessage(message) && message.service) { @@ -206,8 +212,8 @@ export abstract class SharedDispatch { log.error(`Dispatch message missing required response ID: ${JSON.stringify(event)}`); } else { promise.then( - result => worker.postMessage({ responseId: message.responseId, result }), - error => worker.postMessage({ responseId: message.responseId, error }) + result => worker.postMessage({responseId: message.responseId, result}), + error => worker.postMessage({responseId: message.responseId, error}) ); } } diff --git a/packages/vm/src/dispatch/worker-dispatch.ts b/packages/vm/src/dispatch/worker-dispatch.ts index 64cc8ade..09d82654 100644 --- a/packages/vm/src/dispatch/worker-dispatch.ts +++ b/packages/vm/src/dispatch/worker-dispatch.ts @@ -25,10 +25,10 @@ export class WorkerDispatch extends SharedDispatch { */ private services: Record = {}; - constructor() { + constructor () { super(); - this.connectionPromise = new Promise((resolve) => { + this.connectionPromise = new Promise(resolve => { this.onConnect = resolve; }); @@ -45,7 +45,7 @@ export class WorkerDispatch extends SharedDispatch { * dispatch.call('myService', 'hello'); * }) */ - get waitForConnection() { + get waitForConnection () { return this.connectionPromise; } @@ -56,7 +56,7 @@ export class WorkerDispatch extends SharedDispatch { * @param provider A local object which provides this service. * @returns A promise which will resolve once the service is registered. */ - setService(service: string, provider: object) { + setService (service: string, provider: object) { if (Object.prototype.hasOwnProperty.call(this.services, service)) { log.warn(`Worker dispatch replacing existing service provider for ${service}`); } @@ -69,7 +69,7 @@ export class WorkerDispatch extends SharedDispatch { * @param service The name of the service to look up. * @returns The means to contact the service, if found. */ - protected override getServiceProvider(service: string) { + protected override getServiceProvider (service: string) { // if we don't have a local service by this name, contact central dispatch by calling `postMessage` on self const provider = this.services[service]; return { @@ -84,7 +84,7 @@ export class WorkerDispatch extends SharedDispatch { * @param message The message to be handled. * @returns A promise for the results of this operation, if appropriate. */ - protected override onDispatchMessage(worker: WorkerLike, message: DispatchCallMessage) { + protected override onDispatchMessage (worker: WorkerLike, message: DispatchCallMessage) { let promise; switch (message.method) { case 'handshake': diff --git a/packages/vm/src/engine/adapter.ts b/packages/vm/src/engine/adapter.ts index b53514f7..3bfe5444 100644 --- a/packages/vm/src/engine/adapter.ts +++ b/packages/vm/src/engine/adapter.ts @@ -36,8 +36,10 @@ const domToBlock = function ( parent: parent, // Parent block ID, if available. shadow: blockDOM.name === 'shadow', // If this represents a shadow/slot. // X position of script, if top-level. + // eslint-disable-next-line no-negated-condition x: typeof blockDOM.attribs.x !== 'undefined' ? Number(blockDOM.attribs.x) : undefined, // Y position of script, if top-level. + // eslint-disable-next-line no-negated-condition y: typeof blockDOM.attribs.y !== 'undefined' ? Number(blockDOM.attribs.y) : undefined }; @@ -176,7 +178,13 @@ const domToBlocks = function (blocksDOM: Element[]): VMBlock[] { * @param parent Parent block ID. * @param isShadow Whether this block is a shadow. */ -const stateToBlock = function (blockState: BlockState, blocks: Record, isTopBlock: boolean, parent: string | null, isShadow?: boolean): void { +const stateToBlock = function ( + blockState: BlockState, + blocks: Record, + isTopBlock: boolean, + parent: string | null, + isShadow?: boolean +): void { if (!blockState.id) { blockState.id = uid(); } diff --git a/packages/vm/src/engine/blocks.js b/packages/vm/src/engine/blocks.js index 05703446..67e1b7d8 100644 --- a/packages/vm/src/engine/blocks.js +++ b/packages/vm/src/engine/blocks.js @@ -1008,11 +1008,11 @@ class Blocks { /** * Returns a map of all references to variables or lists from blocks * in this block container. - * @param {Array} optBlocks Optional list of blocks to constrain the search to. + * @param {Array | null} optBlocks Optional list of blocks to constrain the search to. * This is useful for getting variable/list references for a stack of blocks instead * of all blocks on the workspace * @param {boolean=} optIncludeBroadcast Optional whether to include broadcast fields. - * @returns {object} A map of variable ID to a list of all variable references + * @returns A map of variable ID to a list of all variable references * for that ID. A variable reference contains the field referencing that variable * and also the type of the variable being referenced. */ diff --git a/packages/vm/src/engine/profiler.ts b/packages/vm/src/engine/profiler.ts index 34630958..906f23c1 100644 --- a/packages/vm/src/engine/profiler.ts +++ b/packages/vm/src/engine/profiler.ts @@ -114,8 +114,8 @@ class Profiler { */ START = START; /** - c * A reference to the STOP record id constant. - c */ + * A reference to the STOP record id constant. + */ STOP = STOP; static START = START; @@ -125,7 +125,7 @@ class Profiler { /** * A callback handle called with each decoded frame when reporting back * all the recorded times. - */ + */ public onFrame: FrameCallback = function () {} ) {} diff --git a/packages/vm/src/engine/variable.ts b/packages/vm/src/engine/variable.ts index 724a4293..8c8827c2 100644 --- a/packages/vm/src/engine/variable.ts +++ b/packages/vm/src/engine/variable.ts @@ -7,7 +7,7 @@ import uid from '../util/uid'; import xmlEscape from '../util/xml-escape'; -const enum VariableType { +export const enum VariableType { SCALAR = '', LIST = 'list', BROADCAST_MESSAGE = 'broadcast_msg' @@ -32,6 +32,7 @@ class Variable { * Whether the variable is stored in the cloud. */ isCloud: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any; constructor (id: string, name: string, type: VariableType, isCloud: boolean) { diff --git a/packages/vm/src/extension-support/extension-metadata.ts b/packages/vm/src/extension-support/extension-metadata.ts index 5a619a69..6fc4bbae 100644 --- a/packages/vm/src/extension-support/extension-metadata.ts +++ b/packages/vm/src/extension-support/extension-metadata.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import type ArgumentType from './argument-type'; import type BlockType from './block-type'; import type ReporterScope from './reporter-scope'; diff --git a/packages/vm/src/io/keyboard.ts b/packages/vm/src/io/keyboard.ts index 63ef8459..933a4bb7 100644 --- a/packages/vm/src/io/keyboard.ts +++ b/packages/vm/src/io/keyboard.ts @@ -110,6 +110,8 @@ class Keyboard { /** * Keyboard DOM event handler. * @param data Data from DOM event. + * @param data.key The key from the DOM event. + * @param data.isDown Whether the key is being pressed or released. */ postData (data: { key: string; isDown: boolean }): void { if (!data.key) return; diff --git a/packages/vm/src/io/mouseWheel.ts b/packages/vm/src/io/mouseWheel.ts index 827edde2..4e6901cf 100644 --- a/packages/vm/src/io/mouseWheel.ts +++ b/packages/vm/src/io/mouseWheel.ts @@ -11,6 +11,8 @@ class MouseWheel { /** * Mouse wheel DOM event handler. * @param data Data from DOM event. + * @param data.deltaY Amount of vertical scroll. Negative value indicates scrolling up, + * positive value indicates scrolling down. */ postData (data: { deltaY: number }): void { const matchFields: Record = {}; diff --git a/packages/vm/src/io/userData.ts b/packages/vm/src/io/userData.ts index 90f80fd0..96df6da4 100644 --- a/packages/vm/src/io/userData.ts +++ b/packages/vm/src/io/userData.ts @@ -4,6 +4,7 @@ class UserData { /** * Handler for updating the username * @param data Data posted to this ioDevice. + * @param data.username The username to set for this user data device. */ postData (data: {username: string}): void { this._username = data.username; diff --git a/packages/vm/src/serialization/schema.ts b/packages/vm/src/serialization/schema.ts index e515434f..ecb5c791 100644 --- a/packages/vm/src/serialization/schema.ts +++ b/packages/vm/src/serialization/schema.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ export interface SB3Project { targets: SB3Target[]; monitors?: SB3Monitor[]; diff --git a/packages/vm/src/util/cast.ts b/packages/vm/src/util/cast.ts index f7478bf8..fd6e883b 100644 --- a/packages/vm/src/util/cast.ts +++ b/packages/vm/src/util/cast.ts @@ -95,10 +95,10 @@ class Cast { const hexResult = Color.hexToRgb(value); // If the color wasn't *actually* a hex color, cast to black - if (!hexResult) { - color = {r: 0, g: 0, b: 0, a: 255}; - } else { + if (hexResult) { color = hexResult; + } else { + color = {r: 0, g: 0, b: 0, a: 255}; } } else { color = Color.decimalToRgb(Cast.toNumber(value)); diff --git a/packages/vm/src/util/color.ts b/packages/vm/src/util/color.ts index e48ec672..032d4fe6 100644 --- a/packages/vm/src/util/color.ts +++ b/packages/vm/src/util/color.ts @@ -13,11 +13,11 @@ export interface HSVObject { class Color { static get RGB_BLACK (): RGBObject { - return { r: 0, g: 0, b: 0 }; + return {r: 0, g: 0, b: 0}; } static get RGB_WHITE (): RGBObject { - return { r: 255, g: 255, b: 255 }; + return {r: 255, g: 255, b: 255}; } /** @@ -44,7 +44,7 @@ class Color { const r = (decimal >> 16) & 0xFF; const g = (decimal >> 8) & 0xFF; const b = decimal & 0xFF; - return { r: r, g: g, b: b, a: a > 0 ? a : 255 }; + return {r: r, g: g, b: b, a: a > 0 ? a : 255}; } /** @@ -84,10 +84,10 @@ class Color { } /** - * Convert a hex color (e.g., F00, #03F, #0033FF) to a decimal color number. - * @param hex Hex representation of the color. - * @returns Number representing the color. - */ + * Convert a hex color (e.g., F00, #03F, #0033FF) to a decimal color number. + * @param hex Hex representation of the color. + * @returns Number representing the color. + */ static hexToDecimal (hex: string): number { const rgb = Color.hexToRgb(hex); return rgb ? Color.rgbToDecimal(rgb) : 0; @@ -115,37 +115,37 @@ class Color { let b: number; switch (i) { - default: - case 0: - r = v; - g = t; - b = p; - break; - case 1: - r = q; - g = v; - b = p; - break; - case 2: - r = p; - g = v; - b = t; - break; - case 3: - r = p; - g = q; - b = v; - break; - case 4: - r = t; - g = p; - b = v; - break; - case 5: - r = v; - g = p; - b = q; - break; + default: + case 0: + r = v; + g = t; + b = p; + break; + case 1: + r = q; + g = v; + b = p; + break; + case 2: + r = p; + g = v; + b = t; + break; + case 3: + r = p; + g = q; + b = v; + break; + case 4: + r = t; + g = p; + b = v; + break; + case 5: + r = v; + g = p; + b = q; + break; } return { @@ -177,7 +177,7 @@ class Color { s = (v - x) / v; } - return { h: h, s: s, v: v }; + return {h: h, s: s, v: v}; } /** diff --git a/packages/vm/src/util/fetch-with-timeout.ts b/packages/vm/src/util/fetch-with-timeout.ts index 3eca57f9..8ccf0bfc 100644 --- a/packages/vm/src/util/fetch-with-timeout.ts +++ b/packages/vm/src/util/fetch-with-timeout.ts @@ -5,7 +5,11 @@ * @param timeout The amount of time before the request is canceled, in milliseconds * @returns The response from the server. */ -const fetchWithTimeout = (resource: RequestInfo | URL, init: RequestInit | null, timeout: number): Promise => { +const fetchWithTimeout = ( + resource: RequestInfo | URL, + init: RequestInit | null, + timeout: number +): Promise => { let timeoutID: ReturnType | null = null; // Not supported in Safari <11 const controller = window.AbortController ? new window.AbortController() : null; diff --git a/packages/vm/src/util/jsonrpc.ts b/packages/vm/src/util/jsonrpc.ts index 8312ad4f..7840a4f4 100644 --- a/packages/vm/src/util/jsonrpc.ts +++ b/packages/vm/src/util/jsonrpc.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ interface OpenRequest { resolve: (result: unknown) => void; reject: (error: Error) => void; @@ -57,11 +58,11 @@ class JSONRPC { * @param method - the method requested by the remote caller. * @param params - the parameters sent with the remote caller's request. */ - didReceiveCall (method: string, params: object): unknown { // eslint-disable-line no-unused-vars + didReceiveCall (method: string, params: object): unknown { throw new Error('Must override didReceiveCall'); } - _sendMessage (jsonMessageObject: object): void { // eslint-disable-line no-unused-vars + _sendMessage (jsonMessageObject: object): void { throw new Error('Must override _sendMessage'); } diff --git a/packages/vm/src/util/maybe-format-message.ts b/packages/vm/src/util/maybe-format-message.ts index c7c31575..4aee721e 100644 --- a/packages/vm/src/util/maybe-format-message.ts +++ b/packages/vm/src/util/maybe-format-message.ts @@ -1,4 +1,7 @@ -import formatMessage from 'format-message'; +import formatMessage, {Message} from 'format-message'; + +const isMessageObject = (maybeMessage: unknown): maybeMessage is Message => typeof maybeMessage === 'object' && + maybeMessage !== null && 'id' in maybeMessage && 'default' in maybeMessage; /** * Check if `maybeMessage` looks like a message object, and if so pass it to `formatMessage`. @@ -9,8 +12,8 @@ import formatMessage from 'format-message'; * @returns - the formatted message OR the original `maybeMessage` input. */ const maybeFormatMessage = function (maybeMessage: unknown, args?: Record, locale?: string): unknown { - if (maybeMessage && (maybeMessage as Record).id && (maybeMessage as Record).default) { - return formatMessage(maybeMessage as { id: string; default: string }, args, locale); + if (isMessageObject(maybeMessage)) { + return formatMessage(maybeMessage, args, locale); } return maybeMessage; }; diff --git a/packages/vm/src/util/task-queue.ts b/packages/vm/src/util/task-queue.ts index 70e3d332..0d3d7127 100644 --- a/packages/vm/src/util/task-queue.ts +++ b/packages/vm/src/util/task-queue.ts @@ -31,8 +31,14 @@ class TaskQueue { * @param maxTokens - the maximum number of tokens in the bucket (burst size). * @param refillRate - the number of tokens to be added per second (sustain rate). * @param options - optional settings for the new task queue instance. + * @param options.startingTokens - the initial number of tokens in the bucket (default: full bucket). + * @param options.maxTotalCost - the maximum total cost of all pending tasks (default: no limit). */ - constructor (maxTokens: number, refillRate: number, options: { startingTokens?: number; maxTotalCost?: number } = {}) { + constructor ( + maxTokens: number, + refillRate: number, + options: { startingTokens?: number; maxTotalCost?: number } = {} + ) { this._maxTokens = maxTokens; this._refillRate = refillRate; this._pendingTaskRecords = []; diff --git a/packages/vm/src/util/variable-util.ts b/packages/vm/src/util/variable-util.ts index 25764346..c6925078 100644 --- a/packages/vm/src/util/variable-util.ts +++ b/packages/vm/src/util/variable-util.ts @@ -1,5 +1,14 @@ +import type Target from '../engine/target'; +import type {VariableType} from '../engine/variable'; +import type {VMField} from '../serialization/schema'; + type VarRefMap = Record; +interface VarReference { + referencingField: VMField; + type: VariableType; +} + class VariableUtil { static _mergeVarRefObjects (accum: VarRefMap, obj2: VarRefMap): VarRefMap { for (const id in obj2) { @@ -21,7 +30,7 @@ class VariableUtil { * @returns An object with variable ids as the keys and a list of block fields referencing * the variable. */ - static getAllVarRefsForTargets (targets: Array<{ blocks: { getAllVariableAndListReferences: (a: null, b: boolean) => VarRefMap } }>, shouldIncludeBroadcast: boolean): VarRefMap { + static getAllVarRefsForTargets (targets: Target[], shouldIncludeBroadcast: boolean): VarRefMap { return targets .map(t => t.blocks.getAllVariableAndListReferences(null, shouldIncludeBroadcast)) .reduce(VariableUtil._mergeVarRefObjects, {}); @@ -36,7 +45,7 @@ class VariableUtil { * variable name in the references being updated should be replaced with this new name. * If this parameter is not provided or is '', no name change occurs. */ - static updateVariableIdentifiers (referencesToUpdate: Array<{ referencingField: { id: string; value: string } }>, newId: string, optNewName?: string): void { + static updateVariableIdentifiers(referencesToUpdate: VarReference[], newId: string, optNewName?: string): void { referencesToUpdate.map(ref => { ref.referencingField.id = newId; if (optNewName) { From d3e5f90c13e5a9400ebd477a3191c0c0e13650b9 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Sun, 10 May 2026 21:58:54 +0800 Subject: [PATCH 15/30] :art: chore(vm): fix lint Signed-off-by: SimonShiki --- packages/vm/src/util/variable-util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vm/src/util/variable-util.ts b/packages/vm/src/util/variable-util.ts index c6925078..fd08312b 100644 --- a/packages/vm/src/util/variable-util.ts +++ b/packages/vm/src/util/variable-util.ts @@ -45,7 +45,7 @@ class VariableUtil { * variable name in the references being updated should be replaced with this new name. * If this parameter is not provided or is '', no name change occurs. */ - static updateVariableIdentifiers(referencesToUpdate: VarReference[], newId: string, optNewName?: string): void { + static updateVariableIdentifiers (referencesToUpdate: VarReference[], newId: string, optNewName?: string): void { referencesToUpdate.map(ref => { ref.referencingField.id = newId; if (optNewName) { From 00508b1465cc67b0bc069bf0b258d4d5f6e3a6e3 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Sun, 10 May 2026 22:35:36 +0800 Subject: [PATCH 16/30] :bug: fix(vm): run ts worker in same process Signed-off-by: SimonShiki --- packages/vm/test/fixtures/fake-worker.js | 86 ++++++++++++++++++++++++ packages/vm/test/unit/dispatch.js | 8 +-- 2 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 packages/vm/test/fixtures/fake-worker.js diff --git a/packages/vm/test/fixtures/fake-worker.js b/packages/vm/test/fixtures/fake-worker.js new file mode 100644 index 00000000..fe11da43 --- /dev/null +++ b/packages/vm/test/fixtures/fake-worker.js @@ -0,0 +1,86 @@ +const path = require('path'); + +// A fake worker that runs in the same process, to avoid complex ts harness setup. +class FakeWorker { + constructor (scriptPath, _, options) { + const cwd = options && options.cwd ? options.cwd : process.cwd(); + const resolvedPath = path.resolve(cwd, scriptPath); + + this._onmessage = null; + this._onerror = null; + this._terminated = false; + + const workerSelf = { + postMessage: msg => { + if (this._terminated) return; + process.nextTick(() => { + if (this._onmessage) { + this._onmessage({data: msg}); + } + }); + }, + close: () => { + this._terminated = true; + }, + addEventListener: (event, fn) => { + workerSelf[`on${event}`] = fn; + }, + onmessage: null, + onerror: null + }; + + // Share the self reference so postMessage can find onmessage later + this._self = workerSelf; + + global.self = workerSelf; + global.postMessage = workerSelf.postMessage; + global.close = workerSelf.close; + global.addEventListener = workerSelf.addEventListener; + + try { + // eslint-disable-next-line global-require + require(resolvedPath); + } catch (err) { + process.nextTick(() => { + if (this._onerror) { + this._onerror(err); + } + }); + } + } + + postMessage (msg) { + if (this._terminated) return; + process.nextTick(() => { + if (this._self && this._self.onmessage) { + this._self.onmessage({data: msg}); + } + }); + } + + terminate () { + // no-op: the worker context self.close() handles teardown + } + + addEventListener (event, fn) { + this[`_on${event}`] = fn; + } + + get onmessage () { + return this._onmessage; + } + + set onmessage (fn) { + this._onmessage = fn; + } + + get onerror () { + return this._onerror; + } + + set onerror (fn) { + this._onerror = fn; + } +} + +module.exports = FakeWorker; diff --git a/packages/vm/test/unit/dispatch.js b/packages/vm/test/unit/dispatch.js index a3320f2d..985ed048 100644 --- a/packages/vm/test/unit/dispatch.js +++ b/packages/vm/test/unit/dispatch.js @@ -1,11 +1,9 @@ import DispatchTestService from '../fixtures/dispatch-test-service.js'; -import Worker from 'tiny-worker'; +import Worker from '../fixtures/fake-worker'; import dispatch from '../../src/dispatch/central-dispatch'; import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; - -// By default Central Dispatch works with the Worker class built into the browser. Tell it to use TinyWorker instead. dispatch.workerClass = Worker; const runServiceTest = function (serviceName, t) { @@ -48,8 +46,8 @@ test('local', t => { test('remote', t => { const fixturesDir = path.resolve(__dirname, '../fixtures'); - const shimPath = path.resolve(fixturesDir, 'dispatch-test-worker-shim.js'); - const worker = new Worker(shimPath, null, {cwd: fixturesDir}); + const workerPath = path.resolve(fixturesDir, 'dispatch-test-worker.js'); + const worker = new Worker(workerPath, null, {cwd: fixturesDir}); dispatch.addWorker(worker); const waitForWorker = new Promise(resolve => { From 37f73cae026a143a2e238f3e969a179136e6b7cc Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Mon, 11 May 2026 09:02:39 +0800 Subject: [PATCH 17/30] :wrench: chore(vm): migrate scratch3_control category Signed-off-by: SimonShiki --- packages/vm/src/blocks/category_prototype.ts | 8 +-- ...cratch3_control.js => scratch3_control.ts} | 55 ++++++++-------- packages/vm/src/engine/block-utility.js | 6 +- packages/vm/src/engine/runtime.js | 65 ++++++++++--------- packages/vm/src/sprites/rendered-target.js | 9 +++ 5 files changed, 77 insertions(+), 66 deletions(-) rename packages/vm/src/blocks/{scratch3_control.js => scratch3_control.ts} (80%) diff --git a/packages/vm/src/blocks/category_prototype.ts b/packages/vm/src/blocks/category_prototype.ts index e1d96a8b..01bd874e 100644 --- a/packages/vm/src/blocks/category_prototype.ts +++ b/packages/vm/src/blocks/category_prototype.ts @@ -1,15 +1,15 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type Runtime from '../engine/runtime'; import type BlockUtility from '../engine/block-utility'; import type {HatMetadata, MonitorBlockInfo} from '../engine/runtime'; -export type BlockFunction = (args: { +export type BlockArgs = { [argName: string]: any; mutation?: Record; -}, util: BlockUtility) => any; +} + +export type BlockFunction = (args: BlockArgs, util: BlockUtility) => any; export interface CategoryPrototype { - new(runtime: Runtime): void; /** * Retrieve the block primitives implemented by this package. * @returns {Record} Mapping of opcode to Function. diff --git a/packages/vm/src/blocks/scratch3_control.js b/packages/vm/src/blocks/scratch3_control.ts similarity index 80% rename from packages/vm/src/blocks/scratch3_control.js rename to packages/vm/src/blocks/scratch3_control.ts index d94eb06f..b4e0e423 100644 --- a/packages/vm/src/blocks/scratch3_control.js +++ b/packages/vm/src/blocks/scratch3_control.ts @@ -1,25 +1,26 @@ import Cast from '../util/cast'; +import type {BlockArgs, CategoryPrototype} from './category_prototype'; +import type Runtime from '../engine/runtime'; +import type RenderedTarget from '../sprites/rendered-target'; +import type BlockUtility from '../engine/block-utility'; -class Scratch3ControlBlocks { - constructor (runtime) { +class Scratch3ControlBlocks implements CategoryPrototype { + /** + * The "counter" block value. For compatibility with 2.0. + */ + private _counter = 0; + constructor ( /** * The runtime instantiating this block package. - * @type {Runtime} */ - this.runtime = runtime; - - /** - * The "counter" block value. For compatibility with 2.0. - * @type {number} - */ - this._counter = 0; - + public runtime: Runtime + ) { this.runtime.on('RUNTIME_DISPOSED', this.clearCounter.bind(this)); } /** * Retrieve the block primitives implemented by this package. - * @returns {Record} Mapping of opcode to Function. + * @returns Mapping of opcode to Function. */ getPrimitives () { return { @@ -50,7 +51,7 @@ class Scratch3ControlBlocks { }; } - repeat (args, util) { + repeat (args: BlockArgs, util: BlockUtility) { const times = Math.round(Cast.toNumber(args.TIMES)); // Initialize loop if (typeof util.stackFrame.loopCounter === 'undefined') { @@ -67,7 +68,7 @@ class Scratch3ControlBlocks { } } - repeatUntil (args, util) { + repeatUntil (args: BlockArgs, util: BlockUtility) { const condition = Cast.toBoolean(args.CONDITION); // If the condition is false (repeat UNTIL), start the branch. if (!condition) { @@ -75,7 +76,7 @@ class Scratch3ControlBlocks { } } - repeatWhile (args, util) { + repeatWhile (args: BlockArgs, util: BlockUtility) { const condition = Cast.toBoolean(args.CONDITION); // If the condition is true (repeat WHILE), start the branch. if (condition) { @@ -83,7 +84,7 @@ class Scratch3ControlBlocks { } } - forEach (args, util) { + forEach (args: BlockArgs, util: BlockUtility) { const variable = util.target.lookupOrCreateVariable( args.VARIABLE.id, args.VARIABLE.name); @@ -98,18 +99,18 @@ class Scratch3ControlBlocks { } } - waitUntil (args, util) { + waitUntil (args: BlockArgs, util: BlockUtility) { const condition = Cast.toBoolean(args.CONDITION); if (!condition) { util.yield(); } } - forever (args, util) { + forever (args: BlockArgs, util: BlockUtility) { util.startBranch(1, true); } - wait (args, util) { + wait (args: BlockArgs, util: BlockUtility) { if (util.stackTimerNeedsInit()) { const duration = Math.max(0, 1000 * Cast.toNumber(args.DURATION)); @@ -121,14 +122,14 @@ class Scratch3ControlBlocks { } } - if (args, util) { + if (args: BlockArgs, util: BlockUtility) { const condition = Cast.toBoolean(args.CONDITION); if (condition) { util.startBranch(1, false); } } - ifElse (args, util) { + ifElse (args: BlockArgs, util: BlockUtility) { const condition = Cast.toBoolean(args.CONDITION); if (condition) { util.startBranch(1, false); @@ -137,7 +138,7 @@ class Scratch3ControlBlocks { } } - stop (args, util) { + stop (args: BlockArgs, util: BlockUtility) { const option = args.STOP_OPTION; if (option === 'all') { util.stopAll(); @@ -149,12 +150,12 @@ class Scratch3ControlBlocks { } } - createClone (args, util) { + createClone (args: BlockArgs, util: BlockUtility) { // Cast argument to string args.CLONE_OPTION = Cast.toString(args.CLONE_OPTION); // Set clone target - let cloneTarget; + let cloneTarget: RenderedTarget | undefined; if (args.CLONE_OPTION === '_myself_') { cloneTarget = util.target; } else { @@ -174,8 +175,8 @@ class Scratch3ControlBlocks { } } - deleteClone (args, util) { - if (util.target.isOriginal) return; + deleteClone (args: BlockArgs, util: BlockUtility) { + if (!util.target.isOriginal) return; this.runtime.disposeTarget(util.target); this.runtime.stopForTarget(util.target); } @@ -192,7 +193,7 @@ class Scratch3ControlBlocks { this._counter++; } - allAtOnce (args, util) { + allAtOnce (args: BlockArgs, util: BlockUtility) { // Since the "all at once" block is implemented for compatiblity with // Scratch 2.0 projects, it behaves the same way it did in 2.0, which // is to simply run the contained script (like "if 1 = 1"). diff --git a/packages/vm/src/engine/block-utility.js b/packages/vm/src/engine/block-utility.js index 92952d03..2747dc11 100644 --- a/packages/vm/src/engine/block-utility.js +++ b/packages/vm/src/engine/block-utility.js @@ -8,7 +8,7 @@ import Timer from '../util/timer'; */ /** - * @typedef {import('./target')} Target + * @typedef {import('../sprites/rendered-target').default} RenderedTarget * @typedef {import('./sequencer')} Sequencer * @typedef {import('./runtime')} Runtime * @typedef {{now: () => number | undefined}} NowObj @@ -45,7 +45,7 @@ class BlockUtility { /** * The target the primitive is working on. - * @type {Target} + * @type {RenderedTarget} */ get target () { return this.thread.target; @@ -73,7 +73,7 @@ class BlockUtility { /** * The stack frame used by loop and other blocks to track internal state. - * @type {object} + * @type {Record} */ get stackFrame () { const frame = this.thread.peekStackFrame(); diff --git a/packages/vm/src/engine/runtime.js b/packages/vm/src/engine/runtime.js index 348e3137..30add31e 100644 --- a/packages/vm/src/engine/runtime.js +++ b/packages/vm/src/engine/runtime.js @@ -55,7 +55,7 @@ const defaultBlockPackages = { const defaultExtensionColors = ['#0FBD8C', '#0DA57A', '#0B8E69']; /** - * @typedef {import('./target').default} Target + * @typedef {import('../sprites/rendered-target').default} RenderedTarget * @typedef {import('clipcc-audio')} AudioEngine * @typedef {import('clipcc-render')} RenderWebGL * @typedef {import('clipcc-storage').ScratchStorage} ScratchStorage @@ -80,7 +80,7 @@ const defaultExtensionColors = ['#0FBD8C', '#0DA57A', '#0B8E69']; /** * @callback ScriptCallback * @param {string} script - * @param {Target} target + * @param {RenderedTarget} target * @returns {void} */ @@ -399,13 +399,13 @@ class Runtime extends EventEmitter { /** * Target management and storage. - * @type {Array.} + * @type {Array.} */ this.targets = []; /** * Targets in reverse order of execution. Shares its order with drawables. - * @type {Array.} + * @type {Array.} */ this.executableTargets = []; @@ -435,7 +435,7 @@ class Runtime extends EventEmitter { /** * Currently known editing target for the VM. - * @type {?Target} + * @type {?RenderedTarget} */ this._editingTarget = null; @@ -1088,7 +1088,8 @@ class Runtime extends EventEmitter { /** * Create a context ("args") object for use with `formatMessage` on messages which might be target-specific. - * @param {Target} [target] - the target to use as context. If a target is not provided, default to the current + * @param {RenderedTarget} [target] - the target to use as context. + * If a target is not provided, default to the current * editing target or the stage. */ makeMessageContextForTarget (target) { @@ -1668,7 +1669,7 @@ class Runtime extends EventEmitter { /** * Get scratch-blocks XML for each extension category. - * @param {Target|undefined} target - the active editing target, if any. + * @param {RenderedTarget|undefined} target - the active editing target, if any. * @returns {Array} Scratch-blocks XML for each category of extension blocks. */ getBlocksXML (target) { @@ -1928,7 +1929,7 @@ class Runtime extends EventEmitter { /** * Create a thread and push it to the list of threads. * @param {!string} id ID of block that starts the stack. - * @param {!Target} target Target to run thread on. + * @param {!RenderedTarget} target Target to run thread on. * @param {{stackClick?: boolean, updateMonitor?: boolean}|undefined} opts Optional arguments. * @param {?boolean} opts.stackClick true if the script was activated by clicking on the stack * @param {?boolean} opts.updateMonitor true if the script should update a monitor value @@ -2011,8 +2012,8 @@ class Runtime extends EventEmitter { /** * Toggle a script. * @param {!string} topBlockId ID of block that starts the script. - * @param {{target?: Target, stackClick?: boolean}|undefined} opts Optional arguments to toggle the script. - * @param {?Target} opts.target Target to run the script on. If not supplied, uses the editing target. + * @param {{target?: RenderedTarget, stackClick?: boolean}|undefined} opts Optional arguments to toggle the script. + * @param {?RenderedTarget} opts.target Target to run the script on. If not supplied, uses the editing target. * @param {?boolean} opts.stackClick true if the user activated the stack by clicking, false if not. This * determines whether we show a visual report when turning on the script. */ @@ -2044,7 +2045,7 @@ class Runtime extends EventEmitter { /** * Enqueue a script that when finished will update the monitor for the block. * @param {!string} topBlockId ID of block that starts the script. - * @param {?Target} optTarget target Target to run script on. If not supplied, uses editing target. + * @param {?RenderedTarget} optTarget target Target to run script on. If not supplied, uses editing target. */ addMonitorScript (topBlockId, optTarget) { if (!optTarget) optTarget = this._editingTarget; @@ -2065,7 +2066,7 @@ class Runtime extends EventEmitter { * - the top block ID of the script. * - the target that owns the script. * @param {ScriptCallback} f Function to call for each script. - * @param {Target=} optTarget Optionally, a target to restrict to. + * @param {RenderedTarget=} optTarget Optionally, a target to restrict to. */ allScriptsDo (f, optTarget) { let targets = this.executableTargets; @@ -2100,7 +2101,7 @@ class Runtime extends EventEmitter { * Start all relevant hats. * @param {!string} requestedHatOpcode Opcode of hats to start. * @param {object= | null} optMatchFields Optionally, fields to match on the hat. - * @param {Target=} optTarget Optionally, a target to restrict to. + * @param {RenderedTarget=} optTarget Optionally, a target to restrict to. * @returns {Array.|undefined} List of threads started by this function. */ startHats (requestedHatOpcode, @@ -2222,7 +2223,7 @@ class Runtime extends EventEmitter { * Add a target to the runtime. This tracks the sprite pane * ordering of the target. The target still needs to be put * into the correct execution order after calling this function. - * @param {Target} target target to add + * @param {RenderedTarget} target target to add */ addTarget (target) { this.targets.push(target); @@ -2235,7 +2236,7 @@ class Runtime extends EventEmitter { * A positve number will make the target execute earlier. A negative number * will make the target execute later in the order. * - * @param {Target} executableTarget target to move + * @param {RenderedTarget} executableTarget target to move * @param {number} delta number of positions to move target by * @returns {number} new position in execution order */ @@ -2263,7 +2264,7 @@ class Runtime extends EventEmitter { * Infinity will set the target to execute first. 0 will set the target to * execute last (before the stage). * - * @param {Target} executableTarget target to move + * @param {RenderedTarget} executableTarget target to move * @param {number} newIndex position in execution order to place the target * @returns {number} new position in the execution order */ @@ -2274,7 +2275,7 @@ class Runtime extends EventEmitter { /** * Remove a target from the execution set. - * @param {Target} executableTarget target to remove + * @param {RenderedTarget} executableTarget target to remove */ removeExecutable (executableTarget) { const oldIndex = this.executableTargets.indexOf(executableTarget); @@ -2285,7 +2286,7 @@ class Runtime extends EventEmitter { /** * Dispose of a target. - * @param {!Target} disposingTarget Target to dispose of. + * @param {!RenderedTarget} disposingTarget Target to dispose of. */ disposeTarget (disposingTarget) { this.targets = this.targets.filter(target => { @@ -2299,7 +2300,7 @@ class Runtime extends EventEmitter { /** * Stop any threads acting on the target. - * @param {!Target} target Target to stop threads for. + * @param {!RenderedTarget} target Target to stop threads for. * @param {Thread=} optThreadException Optional thread to skip. */ stopForTarget (target, optThreadException) { @@ -2456,7 +2457,7 @@ class Runtime extends EventEmitter { /** * Set the current editing target known by the runtime. - * @param {!Target} editingTarget New editing target. + * @param {!RenderedTarget} editingTarget New editing target. */ setEditingTarget (editingTarget) { const oldEditingTarget = this._editingTarget; @@ -2714,7 +2715,7 @@ class Runtime extends EventEmitter { /** * Get a target by its id. * @param {string} targetId Id of target to find. - * @returns {Target|undefined} The target, if found. + * @returns {RenderedTarget|undefined} The target, if found. */ getTargetById (targetId) { for (let i = 0; i < this.targets.length; i++) { @@ -2728,7 +2729,7 @@ class Runtime extends EventEmitter { /** * Get the first original (non-clone-block-created) sprite given a name. * @param {string} spriteName Name of sprite to look for. - * @returns {Target|undefined} Target representing a sprite of the given name. + * @returns {RenderedTarget|undefined} Target representing a sprite of the given name. */ getSpriteTargetByName (spriteName) { for (let i = 0; i < this.targets.length; i++) { @@ -2745,7 +2746,7 @@ class Runtime extends EventEmitter { /** * Get a target by its drawable id. * @param {number} drawableID drawable id of target to find - * @returns {Target|undefined} The target, if found. + * @returns {RenderedTarget|undefined} The target, if found. */ getTargetByDrawableId (drawableID) { for (let i = 0; i < this.targets.length; i++) { @@ -2786,8 +2787,8 @@ class Runtime extends EventEmitter { /** * Report that a new target has been created, possibly by cloning an existing target. - * @param {Target} newTarget - the newly created target. - * @param {Target} [sourceTarget] - the target used as a source for the new clone, if any. + * @param {RenderedTarget} newTarget - the newly created target. + * @param {RenderedTarget} [sourceTarget] - the target used as a source for the new clone, if any. * @fires Runtime#targetWasCreated */ fireTargetWasCreated (newTarget, sourceTarget) { @@ -2796,7 +2797,7 @@ class Runtime extends EventEmitter { /** * Report that a clone target is being removed. - * @param {Target} target - the target being removed + * @param {RenderedTarget} target - the target being removed * @fires Runtime#targetWasRemoved */ fireTargetWasRemoved (target) { @@ -2805,7 +2806,7 @@ class Runtime extends EventEmitter { /** * Get a target representing the Scratch stage, if one exists. - * @returns {Target|undefined} The target, if found. + * @returns {RenderedTarget|undefined} The target, if found. */ getTargetForStage () { for (let i = 0; i < this.targets.length; i++) { @@ -2818,7 +2819,7 @@ class Runtime extends EventEmitter { /** * Get the editing target. - * @returns {?Target} The editing target. + * @returns {?RenderedTarget} The editing target. */ getEditingTarget () { return this._editingTarget; @@ -2901,7 +2902,7 @@ class Runtime extends EventEmitter { /** * Get the global procedure definition for a given name. * @param {?string} name Name of procedure to query. - * @returns {[?Target, ?string]} ID of procedure definition. + * @returns {[?RenderedTarget, ?string]} ID of procedure definition. */ getProcedureDefinition (name) { for (const target of this.targets) { @@ -2924,7 +2925,7 @@ class Runtime extends EventEmitter { /** * Emit a targets update at the end of the step if the provided target is * the original sprite - * @param {!Target} target Target requesting the targets update + * @param {!RenderedTarget} target Target requesting the targets update */ requestTargetsUpdate (target) { if (!target.isOriginal) return; @@ -2999,8 +3000,8 @@ class Runtime extends EventEmitter { * Event fired after a new target has been created, possibly by cloning an existing target. * * @event Runtime#targetWasCreated - * @param {Target} newTarget - the newly created target. - * @param {Target} [sourceTarget] - the target used as a source for the new clone, if any. + * @param {RenderedTarget} newTarget - the newly created target. + * @param {RenderedTarget} [sourceTarget] - the target used as a source for the new clone, if any. */ export default Runtime; diff --git a/packages/vm/src/sprites/rendered-target.js b/packages/vm/src/sprites/rendered-target.js index 4b4be207..51ea0d64 100644 --- a/packages/vm/src/sprites/rendered-target.js +++ b/packages/vm/src/sprites/rendered-target.js @@ -1115,4 +1115,13 @@ class RenderedTarget extends Target { } } +/** + * Whether a given target is a RenderedTarget, i.e., has drawable properties and can be rendered. + * @param {Target} target Target to check. + * @returns {target is RenderedTarget} True if the target is a RenderedTarget. + */ +export const isRenderedTarget = function (target) { + return 'drawableID' in target; +}; + export default RenderedTarget; From b5a85fb4890a794f0e1306efc4754982be64aecd Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Mon, 11 May 2026 10:04:26 +0800 Subject: [PATCH 18/30] :wrench: chore(vm): migrate some categories Signed-off-by: SimonShiki --- .../{scratch3_data.js => scratch3_data.ts} | 79 +++++++++-------- .../{scratch3_event.js => scratch3_event.ts} | 32 ++++--- ...{scratch3_motion.js => scratch3_motion.ts} | 59 +++++++------ ...ch3_operators.js => scratch3_operators.ts} | 85 ++++++++++--------- ...3_procedures.js => scratch3_procedures.ts} | 26 +++--- packages/vm/src/engine/block-utility.js | 6 +- packages/vm/src/engine/runtime.js | 12 +-- packages/vm/src/engine/target.js | 2 +- packages/vm/src/engine/thread.js | 12 +++ packages/vm/src/sprites/rendered-target.js | 8 +- packages/vm/src/types/global.d.ts | 5 ++ packages/vm/src/util/cast.ts | 10 +-- packages/vm/test/unit/blocks_control.js | 2 +- packages/vm/test/unit/blocks_data.js | 2 +- packages/vm/test/unit/blocks_data_infinity.js | 2 +- packages/vm/test/unit/blocks_event.js | 2 +- packages/vm/test/unit/blocks_motion.js | 4 +- packages/vm/test/unit/blocks_operators.js | 2 +- .../vm/test/unit/blocks_operators_infinity.js | 2 +- packages/vm/test/unit/blocks_procedures.js | 2 +- 20 files changed, 198 insertions(+), 156 deletions(-) rename packages/vm/src/blocks/{scratch3_data.js => scratch3_data.ts} (79%) rename packages/vm/src/blocks/{scratch3_event.js => scratch3_event.ts} (81%) rename packages/vm/src/blocks/{scratch3_motion.js => scratch3_motion.ts} (85%) rename packages/vm/src/blocks/{scratch3_operators.js => scratch3_operators.ts} (81%) rename packages/vm/src/blocks/{scratch3_procedures.js => scratch3_procedures.ts} (77%) diff --git a/packages/vm/src/blocks/scratch3_data.js b/packages/vm/src/blocks/scratch3_data.ts similarity index 79% rename from packages/vm/src/blocks/scratch3_data.js rename to packages/vm/src/blocks/scratch3_data.ts index adbc6d4c..6f396690 100644 --- a/packages/vm/src/blocks/scratch3_data.js +++ b/packages/vm/src/blocks/scratch3_data.ts @@ -1,17 +1,25 @@ import Cast from '../util/cast'; +import type {BlockArgs, CategoryPrototype} from './category_prototype'; +import type Runtime from '../engine/runtime'; +import type BlockUtility from '../engine/block-utility'; +import type Variable from '../engine/variable'; -class Scratch3DataBlocks { - constructor (runtime) { +interface ManagedVariable extends Variable { + _monitorUpToDate?: boolean; +} + +class Scratch3DataBlocks implements CategoryPrototype { + constructor ( /** * The runtime instantiating this block package. - * @type {Runtime} */ - this.runtime = runtime; + public runtime: Runtime + ) { } /** * Retrieve the block primitives implemented by this package. - * @returns {Record} Mapping of opcode to Function. + * @returns Mapping of opcode to Function. */ getPrimitives () { return { @@ -35,13 +43,13 @@ class Scratch3DataBlocks { }; } - getVariable (args, util) { + getVariable (args: BlockArgs, util: BlockUtility) { const variable = util.target.lookupOrCreateVariable( args.VARIABLE.id, args.VARIABLE.name); return variable.value; } - setVariableTo (args, util) { + setVariableTo (args: BlockArgs, util: BlockUtility) { const variable = util.target.lookupOrCreateVariable( args.VARIABLE.id, args.VARIABLE.name); variable.value = args.VALUE; @@ -51,7 +59,7 @@ class Scratch3DataBlocks { } } - changeVariableBy (args, util) { + changeVariableBy (args: BlockArgs, util: BlockUtility) { const variable = util.target.lookupOrCreateVariable( args.VARIABLE.id, args.VARIABLE.name); const castedValue = Cast.toNumber(variable.value); @@ -64,38 +72,38 @@ class Scratch3DataBlocks { } } - changeMonitorVisibility (id, visible) { + changeMonitorVisibility (id: string, visible: boolean) { // Send the monitor blocks an event like the flyout checkbox event. // This both updates the monitor state and changes the isMonitored block flag. this.runtime.monitorBlocks.changeBlock({ id: id, // Monitor blocks for variables are the variable ID. element: 'checkbox', // Mimic checkbox event from flyout. value: visible - }, this.runtime); + }); } - showVariable (args) { + showVariable (args: BlockArgs) { this.changeMonitorVisibility(args.VARIABLE.id, true); } - hideVariable (args) { + hideVariable (args: BlockArgs) { this.changeMonitorVisibility(args.VARIABLE.id, false); } - showList (args) { + showList (args: BlockArgs) { this.changeMonitorVisibility(args.LIST.id, true); } - hideList (args) { + hideList (args: BlockArgs) { this.changeMonitorVisibility(args.LIST.id, false); } - getListContents (args, util) { + getListContents (args: BlockArgs, util: BlockUtility) { const list = util.target.lookupOrCreateList( - args.LIST.id, args.LIST.name); + args.LIST.id, args.LIST.name) as ManagedVariable; // If block is running for monitors, return copy of list as an array if changed. - if (util.thread.updateMonitor) { + if (util.thread!.updateMonitor) { // Return original list value if up-to-date, which doesn't trigger monitor update. if (list._monitorUpToDate) return list.value; // If value changed, reset the flag and return a copy to trigger monitor update. @@ -123,18 +131,18 @@ class Scratch3DataBlocks { } - addToList (args, util) { + addToList (args: BlockArgs, util: BlockUtility) { const list = util.target.lookupOrCreateList( - args.LIST.id, args.LIST.name); + args.LIST.id, args.LIST.name) as ManagedVariable; if (list.value.length < this.LIST_ITEM_LIMIT) { list.value.push(args.ITEM); list._monitorUpToDate = false; } } - deleteOfList (args, util) { + deleteOfList (args: BlockArgs, util: BlockUtility) { const list = util.target.lookupOrCreateList( - args.LIST.id, args.LIST.name); + args.LIST.id, args.LIST.name) as ManagedVariable; const index = Cast.toListIndex(args.INDEX, list.value.length, true); if (index === Cast.LIST_INVALID) { return; @@ -146,24 +154,24 @@ class Scratch3DataBlocks { list._monitorUpToDate = false; } - deleteAllOfList (args, util) { + deleteAllOfList (args: BlockArgs, util: BlockUtility) { const list = util.target.lookupOrCreateList( args.LIST.id, args.LIST.name); list.value = []; return; } - insertAtList (args, util) { + insertAtList (args: BlockArgs, util: BlockUtility) { const item = args.ITEM; const list = util.target.lookupOrCreateList( - args.LIST.id, args.LIST.name); + args.LIST.id, args.LIST.name) as ManagedVariable; const index = Cast.toListIndex(args.INDEX, list.value.length + 1, false); if (index === Cast.LIST_INVALID) { return; } const listLimit = this.LIST_ITEM_LIMIT; - if (index > listLimit) return; - list.value.splice(index - 1, 0, item); + if ((index as number) > listLimit) return; + list.value.splice((index as number) - 1, 0, item); if (list.value.length > listLimit) { // If inserting caused the list to grow larger than the limit, // remove the last element in the list @@ -172,29 +180,29 @@ class Scratch3DataBlocks { list._monitorUpToDate = false; } - replaceItemOfList (args, util) { + replaceItemOfList (args: BlockArgs, util: BlockUtility) { const item = args.ITEM; const list = util.target.lookupOrCreateList( - args.LIST.id, args.LIST.name); + args.LIST.id, args.LIST.name) as ManagedVariable; const index = Cast.toListIndex(args.INDEX, list.value.length, false); if (index === Cast.LIST_INVALID) { return; } - list.value[index - 1] = item; + list.value[(index as number) - 1] = item; list._monitorUpToDate = false; } - getItemOfList (args, util) { + getItemOfList (args: BlockArgs, util: BlockUtility) { const list = util.target.lookupOrCreateList( args.LIST.id, args.LIST.name); const index = Cast.toListIndex(args.INDEX, list.value.length, false); if (index === Cast.LIST_INVALID) { return ''; } - return list.value[index - 1]; + return list.value[(index as number) - 1]; } - getItemNumOfList (args, util) { + getItemNumOfList (args: BlockArgs, util: BlockUtility) { const item = args.ITEM; const list = util.target.lookupOrCreateList( args.LIST.id, args.LIST.name); @@ -222,13 +230,13 @@ class Scratch3DataBlocks { return 0; } - lengthOfList (args, util) { + lengthOfList (args: BlockArgs, util: BlockUtility) { const list = util.target.lookupOrCreateList( args.LIST.id, args.LIST.name); return list.value.length; } - listContainsItem (args, util) { + listContainsItem (args: BlockArgs, util: BlockUtility) { const item = args.ITEM; const list = util.target.lookupOrCreateList( args.LIST.id, args.LIST.name); @@ -247,9 +255,8 @@ class Scratch3DataBlocks { /** * Type representation for list variables. - * @returns {number} */ - get LIST_ITEM_LIMIT () { + get LIST_ITEM_LIMIT (): number { return this.runtime.limitOptions.unlimitedListLength ? Infinity : 200000; } diff --git a/packages/vm/src/blocks/scratch3_event.js b/packages/vm/src/blocks/scratch3_event.ts similarity index 81% rename from packages/vm/src/blocks/scratch3_event.js rename to packages/vm/src/blocks/scratch3_event.ts index d3fe56f9..bfa99a95 100644 --- a/packages/vm/src/blocks/scratch3_event.js +++ b/packages/vm/src/blocks/scratch3_event.ts @@ -1,13 +1,16 @@ import Cast from '../util/cast'; +import type {BlockArgs, CategoryPrototype} from './category_prototype'; +import type Runtime from '../engine/runtime'; +import type BlockUtility from '../engine/block-utility'; +import type Thread from '../engine/thread'; -class Scratch3EventBlocks { - constructor (runtime) { +class Scratch3EventBlocks implements CategoryPrototype { + constructor ( /** * The runtime instantiating this block package. - * @type {Runtime} */ - this.runtime = runtime; - + public runtime: Runtime + ) { this.runtime.on('KEY_PRESSED', key => { this.runtime.startHats('event_whenkeypressed', { KEY_OPTION: key @@ -20,7 +23,7 @@ class Scratch3EventBlocks { /** * Retrieve the block primitives implemented by this package. - * @returns {Record} Mapping of opcode to Function. + * @returns Mapping of opcode to Function. */ getPrimitives () { return { @@ -62,11 +65,11 @@ class Scratch3EventBlocks { }; } - touchingObject (args, util) { + touchingObject (args: BlockArgs, util: BlockUtility) { return util.target.isTouchingObject(args.TOUCHINGOBJECTMENU); } - hatGreaterThanPredicate (args, util) { + hatGreaterThanPredicate (args: BlockArgs, util: BlockUtility) { const option = Cast.toString(args.WHENGREATERTHANMENU).toLowerCase(); const value = Cast.toNumber(args.VALUE); switch (option) { @@ -78,8 +81,8 @@ class Scratch3EventBlocks { return false; } - broadcast (args, util) { - const broadcastVar = util.runtime.getTargetForStage().lookupBroadcastMsg( + broadcast (args: BlockArgs, util: BlockUtility) { + const broadcastVar = util.runtime.getTargetForStage()?.lookupBroadcastMsg( args.BROADCAST_OPTION.id, args.BROADCAST_OPTION.name); if (broadcastVar) { const broadcastOption = broadcastVar.name; @@ -89,9 +92,9 @@ class Scratch3EventBlocks { } } - broadcastAndWait (args, util) { + broadcastAndWait (args: BlockArgs, util: BlockUtility) { if (!util.stackFrame.broadcastVar) { - util.stackFrame.broadcastVar = util.runtime.getTargetForStage().lookupBroadcastMsg( + util.stackFrame.broadcastVar = util.runtime.getTargetForStage()?.lookupBroadcastMsg( args.BROADCAST_OPTION.id, args.BROADCAST_OPTION.name); } if (util.stackFrame.broadcastVar) { @@ -110,19 +113,20 @@ class Scratch3EventBlocks { } } // We've run before; check if the wait is still going on. + // eslint-disable-next-line @typescript-eslint/no-this-alias const instance = this; // Scratch 2 considers threads to be waiting if they are still in // runtime.threads. Threads that have run all their blocks, or are // marked done but still in runtime.threads are still considered to // be waiting. - const waiting = util.stackFrame.startedThreads + const waiting = (util.stackFrame.startedThreads as Thread[]) .some(thread => instance.runtime.threads.indexOf(thread) !== -1); if (waiting) { // If all threads are waiting for the next tick or later yield // for a tick as well. Otherwise yield until the next loop of // the threads. if ( - util.stackFrame.startedThreads + (util.stackFrame.startedThreads as Thread[]) .every(thread => instance.runtime.isWaitingThread(thread)) ) { util.yieldTick(); diff --git a/packages/vm/src/blocks/scratch3_motion.js b/packages/vm/src/blocks/scratch3_motion.ts similarity index 85% rename from packages/vm/src/blocks/scratch3_motion.js rename to packages/vm/src/blocks/scratch3_motion.ts index 19a9362b..0f00fa7c 100644 --- a/packages/vm/src/blocks/scratch3_motion.js +++ b/packages/vm/src/blocks/scratch3_motion.ts @@ -1,19 +1,22 @@ import Cast from '../util/cast'; import MathUtil from '../util/math-util'; import Timer from '../util/timer'; +import type {BlockArgs, CategoryPrototype} from './category_prototype'; +import type Runtime from '../engine/runtime'; +import type BlockUtility from '../engine/block-utility'; -class Scratch3MotionBlocks { - constructor (runtime) { +class Scratch3MotionBlocks implements CategoryPrototype { + constructor ( /** * The runtime instantiating this block package. - * @type {Runtime} */ - this.runtime = runtime; + public runtime: Runtime + ) { } /** * Retrieve the block primitives implemented by this package. - * @returns {Record} Mapping of opcode to Function. + * @returns Mapping of opcode to Function. */ getPrimitives () { return { @@ -48,20 +51,20 @@ class Scratch3MotionBlocks { return { motion_xposition: { isSpriteSpecific: true, - getId: targetId => `${targetId}_xposition` + getId: (targetId?: string) => `${targetId}_xposition` }, motion_yposition: { isSpriteSpecific: true, - getId: targetId => `${targetId}_yposition` + getId: (targetId?: string) => `${targetId}_yposition` }, motion_direction: { isSpriteSpecific: true, - getId: targetId => `${targetId}_direction` + getId: (targetId?: string) => `${targetId}_direction` } }; } - moveSteps (args, util) { + moveSteps (args: BlockArgs, util: BlockUtility) { const steps = Cast.toNumber(args.STEPS); const radians = MathUtil.degToRad(90 - util.target.direction); const dx = steps * Math.cos(radians); @@ -69,13 +72,13 @@ class Scratch3MotionBlocks { util.target.setXY(util.target.x + dx, util.target.y + dy); } - goToXY (args, util) { + goToXY (args: BlockArgs, util: BlockUtility) { const x = Cast.toNumber(args.X); const y = Cast.toNumber(args.Y); util.target.setXY(x, y); } - getTargetXY (targetName, util) { + getTargetXY (targetName: string, util: BlockUtility): [number, number] | undefined { let targetX = 0; let targetY = 0; if (targetName === '_mouse_') { @@ -100,29 +103,29 @@ class Scratch3MotionBlocks { return [targetX, targetY]; } - goTo (args, util) { + goTo (args: BlockArgs, util: BlockUtility) { const targetXY = this.getTargetXY(args.TO, util); if (targetXY) { util.target.setXY(targetXY[0], targetXY[1]); } } - turnRight (args, util) { + turnRight (args: BlockArgs, util: BlockUtility) { const degrees = Cast.toNumber(args.DEGREES); util.target.setDirection(util.target.direction + degrees); } - turnLeft (args, util) { + turnLeft (args: BlockArgs, util: BlockUtility) { const degrees = Cast.toNumber(args.DEGREES); util.target.setDirection(util.target.direction - degrees); } - pointInDirection (args, util) { + pointInDirection (args: BlockArgs, util: BlockUtility) { const direction = Cast.toNumber(args.DIRECTION); util.target.setDirection(direction); } - pointTowards (args, util) { + pointTowards (args: BlockArgs, util: BlockUtility) { let targetX = 0; let targetY = 0; if (args.TOWARDS === '_mouse_') { @@ -150,7 +153,7 @@ class Scratch3MotionBlocks { util.target.setDirection(direction); } - glide (args, util) { + glide (args: BlockArgs, util: BlockUtility) { if (util.stackFrame.timer) { const timeElapsed = util.stackFrame.timer.timeElapsed(); if (timeElapsed < util.stackFrame.duration * 1000) { @@ -185,14 +188,14 @@ class Scratch3MotionBlocks { } } - glideTo (args, util) { + glideTo (args: BlockArgs, util: BlockUtility) { const targetXY = this.getTargetXY(args.TO, util); if (targetXY) { this.glide({SECS: args.SECS, X: targetXY[0], Y: targetXY[1]}, util); } } - ifOnEdgeBounce (args, util) { + ifOnEdgeBounce (args: BlockArgs, util: BlockUtility) { const bounds = util.target.getBounds(); if (!bounds) { return; @@ -248,44 +251,44 @@ class Scratch3MotionBlocks { util.target.setXY(fencedPosition[0], fencedPosition[1]); } - setRotationStyle (args, util) { + setRotationStyle (args: BlockArgs, util: BlockUtility) { util.target.setRotationStyle(args.STYLE); } - changeX (args, util) { + changeX (args: BlockArgs, util: BlockUtility) { const dx = Cast.toNumber(args.DX); util.target.setXY(util.target.x + dx, util.target.y); } - setX (args, util) { + setX (args: BlockArgs, util: BlockUtility) { const x = Cast.toNumber(args.X); util.target.setXY(x, util.target.y); } - changeY (args, util) { + changeY (args: BlockArgs, util: BlockUtility) { const dy = Cast.toNumber(args.DY); util.target.setXY(util.target.x, util.target.y + dy); } - setY (args, util) { + setY (args: BlockArgs, util: BlockUtility) { const y = Cast.toNumber(args.Y); util.target.setXY(util.target.x, y); } - getX (args, util) { + getX (args: BlockArgs, util: BlockUtility) { return this.limitPrecision(util.target.x); } - getY (args, util) { + getY (args: BlockArgs, util: BlockUtility) { return this.limitPrecision(util.target.y); } - getDirection (args, util) { + getDirection (args: BlockArgs, util: BlockUtility) { return util.target.direction; } // This corresponds to snapToInteger in Scratch 2 - limitPrecision (coordinate) { + limitPrecision (coordinate: number): number { const rounded = Math.round(coordinate); const delta = coordinate - rounded; const limitedCoord = (Math.abs(delta) < 1e-9) ? rounded : coordinate; diff --git a/packages/vm/src/blocks/scratch3_operators.js b/packages/vm/src/blocks/scratch3_operators.ts similarity index 81% rename from packages/vm/src/blocks/scratch3_operators.js rename to packages/vm/src/blocks/scratch3_operators.ts index 538b88b9..05bbd561 100644 --- a/packages/vm/src/blocks/scratch3_operators.js +++ b/packages/vm/src/blocks/scratch3_operators.ts @@ -1,18 +1,21 @@ import Cast from '../util/cast'; import MathUtil from '../util/math-util'; +import type {BlockArgs, CategoryPrototype} from './category_prototype'; +import type Runtime from '../engine/runtime'; +import type BlockUtility from '../engine/block-utility'; -class Scratch3OperatorsBlocks { - constructor (runtime) { +class Scratch3OperatorsBlocks implements CategoryPrototype { + constructor ( /** * The runtime instantiating this block package. - * @type {Runtime} */ - this.runtime = runtime; + public runtime: Runtime + ) { } /** * Retrieve the block primitives implemented by this package. - * @returns {Record} Mapping of opcode to Function. + * @returns Mapping of opcode to Function. */ getPrimitives () { return { @@ -55,7 +58,7 @@ class Scratch3OperatorsBlocks { /** * Retrieve the block execution orders specified by this package. * The last thing to execute should be the block's self. - * @returns {Record>} Mapping of opcode to execution orders. + * @returns Mapping of opcode to execution orders. */ getOrders () { return { @@ -64,39 +67,39 @@ class Scratch3OperatorsBlocks { }; } - add (args) { + add (args: BlockArgs) { return Cast.toNumber(args.NUM1) + Cast.toNumber(args.NUM2); } - subtract (args) { + subtract (args: BlockArgs) { return Cast.toNumber(args.NUM1) - Cast.toNumber(args.NUM2); } - multiply (args) { + multiply (args: BlockArgs) { return Cast.toNumber(args.NUM1) * Cast.toNumber(args.NUM2); } - divide (args) { + divide (args: BlockArgs) { return Cast.toNumber(args.NUM1) / Cast.toNumber(args.NUM2); } - lt (args) { + lt (args: BlockArgs) { return Cast.compare(args.OPERAND1, args.OPERAND2) < 0; } - equals (args) { + equals (args: BlockArgs) { return Cast.compare(args.OPERAND1, args.OPERAND2) === 0; } - gt (args) { + gt (args: BlockArgs) { return Cast.compare(args.OPERAND1, args.OPERAND2) > 0; } - and (args) { + and (args: BlockArgs) { return Cast.toBoolean(args.OPERAND1) && Cast.toBoolean(args.OPERAND2); } - andTemp (args, util) { + andTemp (args: BlockArgs, util: BlockUtility) { if (!Cast.toBoolean(args.OPERAND1)) { util.skipToOpcode = 'operator_and'; return false; @@ -104,11 +107,11 @@ class Scratch3OperatorsBlocks { util.skipToOpcode = true; } - or (args) { + or (args: BlockArgs) { return Cast.toBoolean(args.OPERAND1) || Cast.toBoolean(args.OPERAND2); } - orTemp (args, util) { + orTemp (args: BlockArgs, util: BlockUtility) { if (Cast.toBoolean(args.OPERAND1)) { util.skipToOpcode = 'operator_or'; return true; @@ -116,11 +119,11 @@ class Scratch3OperatorsBlocks { util.skipToOpcode = true; } - not (args) { + not (args: BlockArgs) { return !Cast.toBoolean(args.OPERAND); } - random (args) { + random (args: BlockArgs) { const nFrom = Cast.toNumber(args.FROM); const nTo = Cast.toNumber(args.TO); const low = nFrom <= nTo ? nFrom : nTo; @@ -133,20 +136,20 @@ class Scratch3OperatorsBlocks { return (Math.random() * (high - low)) + low; } - join (args) { + join (args: BlockArgs) { return Cast.toString(args.STRING1) + Cast.toString(args.STRING2); } - joinMultiple (args) { + joinMultiple (args: BlockArgs) { let result = ''; - const ids = args.mutation.argumentids; + const ids = args.mutation!.argumentids; for (const id of ids) { result += Cast.toString(args[id]); } return result; } - letterOf (args) { + letterOf (args: BlockArgs) { const index = Cast.toNumber(args.LETTER) - 1; const str = Cast.toString(args.STRING); // Out of bounds? @@ -156,18 +159,18 @@ class Scratch3OperatorsBlocks { return str.charAt(index); } - length (args) { + length (args: BlockArgs) { return Cast.toString(args.STRING).length; } - contains (args) { - const format = function (string) { + contains (args: BlockArgs) { + const format = function (string: string) { return Cast.toString(string).toLowerCase(); }; return format(args.STRING1).includes(format(args.STRING2)); } - mod (args) { + mod (args: BlockArgs) { const n = Cast.toNumber(args.NUM1); const modulus = Cast.toNumber(args.NUM2); let result = n % modulus; @@ -176,11 +179,11 @@ class Scratch3OperatorsBlocks { return result; } - round (args) { + round (args: BlockArgs) { return Math.round(Cast.toNumber(args.NUM)); } - mathop (args) { + mathop (args: BlockArgs) { const operator = Cast.toString(args.OPERATOR).toLowerCase(); const n = Cast.toNumber(args.NUM); switch (operator) { @@ -202,51 +205,51 @@ class Scratch3OperatorsBlocks { return 0; } - power (args) { + power (args: BlockArgs) { return Math.pow(Cast.toNumber(args.NUM1), Cast.toNumber(args.NUM2)); } - bitand (args) { + bitand (args: BlockArgs) { return Cast.toNumber(args.NUM1) & Cast.toNumber(args.NUM2); } - bitor (args) { + bitor (args: BlockArgs) { return Cast.toNumber(args.NUM1) | Cast.toNumber(args.NUM2); } - bitxor (args) { + bitxor (args: BlockArgs) { return Cast.toNumber(args.NUM1) ^ Cast.toNumber(args.NUM2); } - bitlsh (args) { + bitlsh (args: BlockArgs) { return Cast.toNumber(args.NUM1) << Cast.toNumber(args.NUM2); } - bitrsh (args) { + bitrsh (args: BlockArgs) { return Cast.toNumber(args.NUM1) >> Cast.toNumber(args.NUM2); } - bitursh (args) { + bitursh (args: BlockArgs) { return Cast.toNumber(args.NUM1) >>> Cast.toNumber(args.NUM2); } - bitnot (args) { + bitnot (args: BlockArgs) { return ~Cast.toNumber(args.NUM1); } - ge (args) { + ge (args: BlockArgs) { return Cast.compare(args.OPERAND1, args.OPERAND2) >= 0; } - le (args) { + le (args: BlockArgs) { return Cast.compare(args.OPERAND1, args.OPERAND2) <= 0; } - nequals (args) { + nequals (args: BlockArgs) { return Cast.compare(args.OPERAND1, args.OPERAND2) !== 0; } - indexOf (args) { + indexOf (args: BlockArgs) { const {STRING, SUBSTRING, POS} = args; let index = Cast.toString(STRING).indexOf(Cast.toString(SUBSTRING)); if (index === -1) return -1; diff --git a/packages/vm/src/blocks/scratch3_procedures.js b/packages/vm/src/blocks/scratch3_procedures.ts similarity index 77% rename from packages/vm/src/blocks/scratch3_procedures.js rename to packages/vm/src/blocks/scratch3_procedures.ts index 52ff78ac..37d0707d 100644 --- a/packages/vm/src/blocks/scratch3_procedures.js +++ b/packages/vm/src/blocks/scratch3_procedures.ts @@ -1,15 +1,19 @@ -class Scratch3ProcedureBlocks { - constructor (runtime) { +import type {BlockArgs, CategoryPrototype} from './category_prototype'; +import type Runtime from '../engine/runtime'; +import type BlockUtility from '../engine/block-utility'; + +class Scratch3ProcedureBlocks implements CategoryPrototype { + constructor ( /** * The runtime instantiating this block package. - * @type {Runtime} */ - this.runtime = runtime; + public runtime: Runtime + ) { } /** * Retrieve the block primitives implemented by this package. - * @returns {Record} Mapping of opcode to Function. + * @returns Mapping of opcode to Function. */ getPrimitives () { return { @@ -26,8 +30,8 @@ class Scratch3ProcedureBlocks { // No-op: execute the blocks. } - call (args, util) { - const procedureCode = args.mutation.proccode; + call (args: BlockArgs, util: BlockUtility) { + const procedureCode = args.mutation!.proccode; const paramNamesIdsAndDefaults = util.getProcedureParamNamesIdsAndDefaults(procedureCode); // If null, procedure could not be found, which can happen if custom @@ -55,12 +59,12 @@ class Scratch3ProcedureBlocks { util.startProcedure(procedureCode); } - return (args, util) { - util.thread.pushReportedValue(args.VALUE); + return (args: BlockArgs, util: BlockUtility) { + util.thread!.pushReportedValue(args.VALUE); util.stopThisScript(); } - argumentReporterStringNumber (args, util) { + argumentReporterStringNumber (args: BlockArgs, util: BlockUtility) { const value = util.getParam(args.VALUE); if (value === null) { // When the parameter is not found in the most recent procedure @@ -70,7 +74,7 @@ class Scratch3ProcedureBlocks { return value; } - argumentReporterBoolean (args, util) { + argumentReporterBoolean (args: BlockArgs, util: BlockUtility) { const value = util.getParam(args.VALUE); if (value === null) { // When the parameter is not found in the most recent procedure diff --git a/packages/vm/src/engine/block-utility.js b/packages/vm/src/engine/block-utility.js index 2747dc11..a97189d6 100644 --- a/packages/vm/src/engine/block-utility.js +++ b/packages/vm/src/engine/block-utility.js @@ -9,8 +9,8 @@ import Timer from '../util/timer'; /** * @typedef {import('../sprites/rendered-target').default} RenderedTarget - * @typedef {import('./sequencer')} Sequencer - * @typedef {import('./runtime')} Runtime + * @typedef {import('./sequencer').default} Sequencer + * @typedef {import('./runtime').default} Runtime * @typedef {{now: () => number | undefined}} NowObj */ @@ -247,7 +247,7 @@ class BlockUtility { * Query a named IO device. * @param {string} device The name of like the device, like keyboard. * @param {string} func The name of the device's function to query. - * @param {Array.<*>} args Arguments to pass to the device's function. + * @param {Array.<*>} [args] Arguments to pass to the device's function. * @returns {*} The expected output for the device's function. */ ioQuery (device, func, args) { diff --git a/packages/vm/src/engine/runtime.js b/packages/vm/src/engine/runtime.js index 30add31e..e38ff651 100644 --- a/packages/vm/src/engine/runtime.js +++ b/packages/vm/src/engine/runtime.js @@ -28,15 +28,15 @@ import Video from '../io/video.js'; import Joystick from '../io/joystick'; import StringUtil from '../util/string-util'; import uid from '../util/uid'; -import control from '../blocks/scratch3_control.js'; -import event from '../blocks/scratch3_event.js'; +import control from '../blocks/scratch3_control'; +import event from '../blocks/scratch3_event'; import looks from '../blocks/scratch3_looks.js'; -import motion from '../blocks/scratch3_motion.js'; -import operators from '../blocks/scratch3_operators.js'; +import motion from '../blocks/scratch3_motion'; +import operators from '../blocks/scratch3_operators'; import sound from '../blocks/scratch3_sound.js'; import sensing from '../blocks/scratch3_sensing.js'; -import data from '../blocks/scratch3_data.js'; -import procedures from '../blocks/scratch3_procedures.js'; +import data from '../blocks/scratch3_data'; +import procedures from '../blocks/scratch3_procedures'; const defaultBlockPackages = { diff --git a/packages/vm/src/engine/target.js b/packages/vm/src/engine/target.js index ddc89a3b..7dc97920 100644 --- a/packages/vm/src/engine/target.js +++ b/packages/vm/src/engine/target.js @@ -9,7 +9,7 @@ import StringUtil from '../util/string-util'; import VariableUtil from '../util/variable-util'; /** - * @typedef {import('./runtime')} Runtime + * @typedef {import('./runtime').default} Runtime */ /** diff --git a/packages/vm/src/engine/thread.js b/packages/vm/src/engine/thread.js index 35c805e6..6f694737 100644 --- a/packages/vm/src/engine/thread.js +++ b/packages/vm/src/engine/thread.js @@ -202,6 +202,18 @@ class Thread { */ this.warpTimer = null; + /** + * true if the script was activated by clicking on the stack + * @type {boolean} + */ + this.stackClick = false; + + /** + * true if the script should update a monitor value + * @type {boolean} + */ + this.updateMonitor = false; + this.justReported = null; /** diff --git a/packages/vm/src/sprites/rendered-target.js b/packages/vm/src/sprites/rendered-target.js index 51ea0d64..00e126d1 100644 --- a/packages/vm/src/sprites/rendered-target.js +++ b/packages/vm/src/sprites/rendered-target.js @@ -5,6 +5,10 @@ import Clone from '../util/clone'; import Target from '../engine/target.js'; import StageLayering from '../engine/stage-layering'; +/** + * @typedef {import('../engine/runtime').default} Runtime + */ + /** * Rendered target: instance of a sprite (clone), or the stage. */ @@ -261,7 +265,7 @@ class RenderedTarget extends Target { * Set the X and Y coordinates. * @param {!number} x New X coordinate, in Scratch coordinates. * @param {!number} y New Y coordinate, in Scratch coordinates. - * @param {?boolean} force Force setting X/Y, in case of dragging + * @param {?boolean} [force] Force setting X/Y, in case of dragging */ setXY (x, y, force) { if (this.isStage) return; @@ -717,7 +721,7 @@ class RenderedTarget extends Target { /** * Return the rendered target's tight bounding box. * Includes top, left, bottom, right attributes in Scratch coordinates. - * @returns {?object} Tight bounding box, or null. + * @returns Tight bounding box, or null. */ getBounds () { if (this.renderer) { diff --git a/packages/vm/src/types/global.d.ts b/packages/vm/src/types/global.d.ts index ad9aab6d..a6c71188 100644 --- a/packages/vm/src/types/global.d.ts +++ b/packages/vm/src/types/global.d.ts @@ -6,3 +6,8 @@ declare module 'decode-html' { declare global { type int = number; } + +/** + * Compile-time injected clipcc global metadata + */ +declare const clipcc: { VERSION?: string }; diff --git a/packages/vm/src/util/cast.ts b/packages/vm/src/util/cast.ts index fd6e883b..34b55c03 100644 --- a/packages/vm/src/util/cast.ts +++ b/packages/vm/src/util/cast.ts @@ -176,12 +176,12 @@ class Cast { return false; } - static get LIST_INVALID (): string { - return 'INVALID'; + static get LIST_INVALID () { + return 'INVALID' as const; } - static get LIST_ALL (): string { - return 'ALL'; + static get LIST_ALL () { + return 'ALL' as const; } /** @@ -194,7 +194,7 @@ class Cast { * @param acceptAll Whether it should accept "all" or not. * @returns 1-based index for list, LIST_ALL, or LIST_INVALID. */ - static toListIndex (index: unknown, length: number, acceptAll: boolean): number | string { + static toListIndex (index: unknown, length: number, acceptAll: boolean) { if (typeof index !== 'number') { if (index === 'all') { return acceptAll ? Cast.LIST_ALL : Cast.LIST_INVALID; diff --git a/packages/vm/test/unit/blocks_control.js b/packages/vm/test/unit/blocks_control.js index 056b319a..6d2d6cea 100644 --- a/packages/vm/test/unit/blocks_control.js +++ b/packages/vm/test/unit/blocks_control.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import Control from '../../src/blocks/scratch3_control.js'; +import Control from '../../src/blocks/scratch3_control'; import Runtime from '../../src/engine/runtime.js'; import BlockUtility from '../../src/engine/block-utility.js'; diff --git a/packages/vm/test/unit/blocks_data.js b/packages/vm/test/unit/blocks_data.js index 017b1715..17df218a 100644 --- a/packages/vm/test/unit/blocks_data.js +++ b/packages/vm/test/unit/blocks_data.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import Data from '../../src/blocks/scratch3_data.js'; +import Data from '../../src/blocks/scratch3_data'; const blocks = new Data(); diff --git a/packages/vm/test/unit/blocks_data_infinity.js b/packages/vm/test/unit/blocks_data_infinity.js index a95325b4..8621c3fb 100644 --- a/packages/vm/test/unit/blocks_data_infinity.js +++ b/packages/vm/test/unit/blocks_data_infinity.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import Data from '../../src/blocks/scratch3_data.js'; +import Data from '../../src/blocks/scratch3_data'; const blocks = new Data(); diff --git a/packages/vm/test/unit/blocks_event.js b/packages/vm/test/unit/blocks_event.js index 5469b749..83b02224 100644 --- a/packages/vm/test/unit/blocks_event.js +++ b/packages/vm/test/unit/blocks_event.js @@ -1,7 +1,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import Blocks from '../../src/engine/blocks.js'; import BlockUtility from '../../src/engine/block-utility.js'; -import Event from '../../src/blocks/scratch3_event.js'; +import Event from '../../src/blocks/scratch3_event'; import Runtime from '../../src/engine/runtime.js'; import Target from '../../src/engine/target.js'; import Thread from '../../src/engine/thread.js'; diff --git a/packages/vm/test/unit/blocks_motion.js b/packages/vm/test/unit/blocks_motion.js index 7c017172..ec25dbeb 100644 --- a/packages/vm/test/unit/blocks_motion.js +++ b/packages/vm/test/unit/blocks_motion.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import Motion from '../../src/blocks/scratch3_motion.js'; +import Motion from '../../src/blocks/scratch3_motion'; import Runtime from '../../src/engine/runtime.js'; import Sprite from '../../src/sprites/sprite.js'; import RenderedTarget from '../../src/sprites/rendered-target.js'; @@ -34,7 +34,7 @@ test('Costumed stage has correct size', t => { const target = new RenderedTarget(sprite, rt); const util = {target}; vm.setStageSize(640, 640); - + motion.goToXY({X: 640, Y: 640}, util); t.equal(motion.getX({}, util), 640); diff --git a/packages/vm/test/unit/blocks_operators.js b/packages/vm/test/unit/blocks_operators.js index 790fa309..62fc3f8b 100644 --- a/packages/vm/test/unit/blocks_operators.js +++ b/packages/vm/test/unit/blocks_operators.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import Operators from '../../src/blocks/scratch3_operators.js'; +import Operators from '../../src/blocks/scratch3_operators'; const blocks = new Operators(null); diff --git a/packages/vm/test/unit/blocks_operators_infinity.js b/packages/vm/test/unit/blocks_operators_infinity.js index acd60921..9a311aa5 100644 --- a/packages/vm/test/unit/blocks_operators_infinity.js +++ b/packages/vm/test/unit/blocks_operators_infinity.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import Operators from '../../src/blocks/scratch3_operators.js'; +import Operators from '../../src/blocks/scratch3_operators'; const blocks = new Operators(null); diff --git a/packages/vm/test/unit/blocks_procedures.js b/packages/vm/test/unit/blocks_procedures.js index 88b29876..cf5c748e 100644 --- a/packages/vm/test/unit/blocks_procedures.js +++ b/packages/vm/test/unit/blocks_procedures.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import Procedures from '../../src/blocks/scratch3_procedures.js'; +import Procedures from '../../src/blocks/scratch3_procedures'; const blocks = new Procedures(null); From 1032c0361e5f16b6c5baf0cd06c2fc6b6ffca0c8 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Mon, 11 May 2026 10:53:00 +0800 Subject: [PATCH 19/30] :wrench: chore(vm): migrate left categories Signed-off-by: SimonShiki --- packages/render/src/RenderWebGL.js | 2 +- .../{scratch3_looks.js => scratch3_looks.ts} | 234 ++++++++++-------- ...cratch3_sensing.js => scratch3_sensing.ts} | 137 +++++----- .../{scratch3_sound.js => scratch3_sound.ts} | 120 +++++---- packages/vm/src/engine/runtime.js | 8 +- packages/vm/src/sprites/rendered-target.js | 6 +- packages/vm/src/util/get-monitor-id.ts | 7 +- packages/vm/test/unit/blocks_looks.js | 2 +- packages/vm/test/unit/blocks_sensing.js | 2 +- packages/vm/test/unit/blocks_sounds.js | 2 +- 10 files changed, 281 insertions(+), 239 deletions(-) rename packages/vm/src/blocks/{scratch3_looks.js => scratch3_looks.ts} (74%) rename packages/vm/src/blocks/{scratch3_sensing.js => scratch3_sensing.ts} (81%) rename packages/vm/src/blocks/{scratch3_sound.js => scratch3_sound.ts} (74%) diff --git a/packages/render/src/RenderWebGL.js b/packages/render/src/RenderWebGL.js index 5816a7e0..fe127004 100644 --- a/packages/render/src/RenderWebGL.js +++ b/packages/render/src/RenderWebGL.js @@ -1391,7 +1391,7 @@ class RenderWebGL extends EventEmitter { * Return drawable pixel data and color at a given scratch position * @param {int} scratchX The scratch x coordinate of the picking location. * @param {int} scratchY The scratch y coordinate of the picking location. - * @returns {?ColorExtraction} Data about the picked color + * @returns Data about the picked color */ extractColorInScratchCoordinate (scratchX, scratchY) { this._doExitDrawRegion(); diff --git a/packages/vm/src/blocks/scratch3_looks.js b/packages/vm/src/blocks/scratch3_looks.ts similarity index 74% rename from packages/vm/src/blocks/scratch3_looks.js rename to packages/vm/src/blocks/scratch3_looks.ts index 18d86db2..dc65b7b5 100644 --- a/packages/vm/src/blocks/scratch3_looks.js +++ b/packages/vm/src/blocks/scratch3_looks.ts @@ -5,25 +5,47 @@ import uid from '../util/uid'; import StageLayering from '../engine/stage-layering'; import getMonitorIdForBlockWithArgs from '../util/get-monitor-id'; import MathUtil from '../util/math-util'; +import type {BlockArgs, CategoryPrototype} from './category_prototype'; +import type Runtime from '../engine/runtime'; +import type BlockUtility from '../engine/block-utility'; +import type Target from '../engine/target.js'; +import type {MonitorBlockInfo} from '../engine/runtime'; +import type Thread from '../engine/thread'; + +interface Bounds { + left: number; + right: number; + top: number; + bottom: number; +} /** - * @typedef {object} BubbleState - the bubble state associated with a particular target. - * @property {boolean} onSpriteRight - tracks whether the bubble is right or left of the sprite. - * @property {?int} drawableId - the ID of the associated bubble Drawable, null if none. - * @property {string} text - the text of the bubble. - * @property {string} type - the type of the bubble, "say" or "think" - * @property {?string} usageId - ID indicating the most recent usage of the say/think bubble. - * Used for comparison when determining whether to clear a say/think bubble. + * The bubble state associated with a particular target. */ +interface BubbleState { + onSpriteRight: boolean; + /** The ID of the associated bubble Drawable, null if none. */ + drawableId: number | null; + skinId: number | null; + text: string; + /** The type of the bubble, "say" or "think" */ + type: string; + /** + * ID indicating the most recent usage of the say/think bubble. + * Used for comparison when determining whether to clear a say/think bubble. + */ + usageId: string | null; +} -class Scratch3LooksBlocks { - constructor (runtime) { +class Scratch3LooksBlocks implements CategoryPrototype { + private _bubbleTimeout: ReturnType | null = null; + + constructor ( /** * The runtime instantiating this block package. - * @type {Runtime} */ - this.runtime = runtime; - + public runtime: Runtime + ) { this._onTargetChanged = this._onTargetChanged.bind(this); this._onResetBubbles = this._onResetBubbles.bind(this); this._onTargetWillExit = this._onTargetWillExit.bind(this); @@ -39,9 +61,8 @@ class Scratch3LooksBlocks { /** * The default bubble state, to be used when a target has no existing bubble state. - * @type {BubbleState} */ - static get DEFAULT_BUBBLE_STATE () { + static get DEFAULT_BUBBLE_STATE (): BubbleState { return { drawableId: null, onSpriteRight: true, @@ -54,17 +75,15 @@ class Scratch3LooksBlocks { /** * The key to load & store a target's bubble-related state. - * @type {string} */ - static get STATE_KEY () { + static get STATE_KEY (): string { return 'Scratch.looks'; } /** * Event name for a text bubble being created or updated. - * @returns {string} */ - static get SAY_OR_THINK () { + static get SAY_OR_THINK (): string { // There are currently many places in the codebase which explicitly refer to this event by the string 'SAY', // so keep this as the string 'SAY' for now rather than changing it to 'SAY_OR_THINK' and breaking things. return 'SAY'; @@ -72,34 +91,31 @@ class Scratch3LooksBlocks { /** * Limit for say bubble string. - * @returns {number} */ - static get SAY_BUBBLE_LIMIT () { + static get SAY_BUBBLE_LIMIT (): number { return 330; } /** * Limit for ghost effect - * @returns {object} */ - static get EFFECT_GHOST_LIMIT (){ + static get EFFECT_GHOST_LIMIT (): {min: number, max: number} { return {min: 0, max: 100}; } /** * Limit for brightness effect - * @returns {object} */ - static get EFFECT_BRIGHTNESS_LIMIT (){ + static get EFFECT_BRIGHTNESS_LIMIT (): {min: number, max: number} { return {min: -100, max: 100}; } /** - * @param {Target} target - collect bubble state for this target. Probably, but not necessarily, a RenderedTarget. - * @returns {BubbleState} the mutable bubble state associated with that target. This will be created if necessary. - * @private + * Collect bubble state for this target. Probably, but not necessarily, a RenderedTarget. + * @param target The target to collect bubble state for. + * @returns the mutable bubble state associated with that target. This will be created if necessary. */ - _getBubbleState (target) { + _getBubbleState (target: Target): BubbleState { let bubbleState = target.getCustomState(Scratch3LooksBlocks.STATE_KEY); if (!bubbleState) { bubbleState = Clone.simple(Scratch3LooksBlocks.DEFAULT_BUBBLE_STATE); @@ -110,10 +126,9 @@ class Scratch3LooksBlocks { /** * Handle a target which has moved. - * @param {RenderedTarget} target - the target which has moved. - * @private + * @param target The target which has moved, which may require its bubble to be repositioned */ - _onTargetChanged (target) { + _onTargetChanged (target: RenderedTarget) { const bubbleState = this._getBubbleState(target); if (bubbleState.drawableId) { this._positionBubble(target); @@ -122,14 +137,13 @@ class Scratch3LooksBlocks { /** * Handle a target which is exiting. - * @param {RenderedTarget} target - the target. - * @private + * @param target The target which is exiting */ - _onTargetWillExit (target) { + _onTargetWillExit (target: RenderedTarget) { const bubbleState = this._getBubbleState(target); - if (bubbleState.drawableId && bubbleState.skinId) { - this.runtime.renderer.destroyDrawable(bubbleState.drawableId, StageLayering.SPRITE_LAYER); - this.runtime.renderer.destroySkin(bubbleState.skinId); + if (bubbleState.drawableId !== null && bubbleState.skinId !== null) { + this.runtime.renderer!.destroyDrawable(bubbleState.drawableId, StageLayering.SPRITE_LAYER); + this.runtime.renderer!.destroySkin(bubbleState.skinId); bubbleState.drawableId = null; bubbleState.skinId = null; this.runtime.requestRedraw(); @@ -139,7 +153,6 @@ class Scratch3LooksBlocks { /** * Handle project start/stop by clearing all visible bubbles. - * @private */ _onResetBubbles () { for (let n = 0; n < this.runtime.targets.length; n++) { @@ -147,21 +160,21 @@ class Scratch3LooksBlocks { bubbleState.text = ''; this._onTargetWillExit(this.runtime.targets[n]); } - clearTimeout(this._bubbleTimeout); + clearTimeout(this._bubbleTimeout!); } /** * Position the bubble of a target. If it doesn't fit on the specified side, flip and rerender. - * @param {!Target} target Target whose bubble needs positioning. - * @private + * @param target The target which has moved, which may require its bubble to be repositioned */ - _positionBubble (target) { + _positionBubble (target: RenderedTarget) { if (!target.visible) return; const bubbleState = this._getBubbleState(target); - const [bubbleWidth, bubbleHeight] = this.runtime.renderer.getCurrentSkinSize(bubbleState.drawableId); - let targetBounds; + if (bubbleState.drawableId === null) return; + const [bubbleWidth, bubbleHeight] = this.runtime.renderer!.getCurrentSkinSize(bubbleState.drawableId); + let targetBounds: Bounds; try { - targetBounds = target.getBoundsForBubble(); + targetBounds = target.getBoundsForBubble() as Bounds; } catch { // Bounds calculation could fail (e.g. on empty costumes), in that case // use the x/y position of the target. @@ -172,7 +185,7 @@ class Scratch3LooksBlocks { bottom: target.y }; } - const stageSize = this.runtime.renderer.getNativeSize(); + const stageSize = this.runtime.renderer!.getNativeSize(); const stageBounds = { left: -stageSize[0] / 2, right: stageSize[0] / 2, @@ -188,7 +201,7 @@ class Scratch3LooksBlocks { bubbleState.onSpriteRight = true; this._renderBubble(target); } else { - this.runtime.renderer.updateDrawablePosition(bubbleState.drawableId, [ + this.runtime.renderer!.updateDrawablePosition(bubbleState.drawableId, [ bubbleState.onSpriteRight ? ( Math.max( stageBounds.left, // Bubble should not extend past left edge of stage @@ -211,11 +224,9 @@ class Scratch3LooksBlocks { * Create a visible bubble for a target. If a bubble exists for the target, * just set it to visible and update the type/text. Otherwise create a new * bubble and update the relevant custom state. - * @param {!Target} target Target who needs a bubble. - * @returns {undefined} Early return if text is empty string. - * @private + * @param target The target to create/update a bubble for. */ - _renderBubble (target) { + _renderBubble (target: RenderedTarget) { if (!this.runtime.renderer) return; const bubbleState = this._getBubbleState(target); @@ -228,12 +239,12 @@ class Scratch3LooksBlocks { } if (bubbleState.skinId) { - this.runtime.renderer.updateTextSkin(bubbleState.skinId, type, text, onSpriteRight, [0, 0]); + this.runtime.renderer.updateTextSkin(bubbleState.skinId, type, text, onSpriteRight); } else { target.addListener(RenderedTarget.EVENT_TARGET_VISUAL_CHANGE, this._onTargetChanged); - bubbleState.drawableId = this.runtime.renderer.createDrawable(StageLayering.SPRITE_LAYER); - bubbleState.skinId = this.runtime.renderer.createTextSkin(type, text, bubbleState.onSpriteRight, [0, 0]); - this.runtime.renderer.updateDrawableSkinId(bubbleState.drawableId, bubbleState.skinId); + bubbleState.drawableId = this.runtime.renderer.createDrawable(StageLayering.SPRITE_LAYER) as number; + bubbleState.skinId = this.runtime.renderer.createTextSkin(type, text, bubbleState.onSpriteRight); + this.runtime.renderer.updateDrawableSkinId(bubbleState.drawableId!, bubbleState.skinId!); } this._positionBubble(target); @@ -241,11 +252,10 @@ class Scratch3LooksBlocks { /** * Properly format text for a text bubble. - * @param {string} text The text to be formatted - * @returns {string} The formatted text - * @private + * @param text The text to be formatted + * @returns The formatted text */ - _formatBubbleText (text) { + _formatBubbleText (text: string | number): string { if (text === '') return text; // Non-integers should be rounded to 2 decimal places (no more, no less), unless they're small enough that @@ -265,12 +275,11 @@ class Scratch3LooksBlocks { /** * The entry point for say/think blocks. Clears existing bubble if the text is empty. * Set the bubble custom state and then call _renderBubble. - * @param {!Target} target Target that say/think blocks are being called on. - * @param {!string} type Either "say" or "think" - * @param {!string} text The text for the bubble, empty string clears the bubble. - * @private + * @param target Target that say/think blocks are being called on. + * @param type Either "say" or "think" + * @param text The text for the bubble, empty string clears the bubble. */ - _updateBubble (target, type, text) { + _updateBubble (target: RenderedTarget, type: string, text: string | number) { const bubbleState = this._getBubbleState(target); bubbleState.type = type; bubbleState.text = this._formatBubbleText(text); @@ -280,7 +289,7 @@ class Scratch3LooksBlocks { /** * Retrieve the block primitives implemented by this package. - * @returns {Record} Mapping of opcode to Function. + * @returns Mapping of opcode to Function. */ getPrimitives () { return { @@ -319,24 +328,26 @@ class Scratch3LooksBlocks { }, looks_costumenumbername: { isSpriteSpecific: true, - getId: (targetId, fields) => getMonitorIdForBlockWithArgs(`${targetId}_costumenumbername`, fields) + getId: (targetId, fields) => + getMonitorIdForBlockWithArgs(`${targetId}_costumenumbername`, fields!) }, looks_backdropnumbername: { - getId: (_, fields) => getMonitorIdForBlockWithArgs('backdropnumbername', fields) + getId: (_, fields) => + getMonitorIdForBlockWithArgs('backdropnumbername', fields!) } - }; + } as Record; } - say (args, util) { + say (args: BlockArgs, util: BlockUtility) { // @TODO in 2.0 calling say/think resets the right/left bias of the bubble this.runtime.emit(Scratch3LooksBlocks.SAY_OR_THINK, util.target, 'say', args.MESSAGE); } - sayforsecs (args, util) { + sayforsecs (args: BlockArgs, util: BlockUtility) { this.say(args, util); const target = util.target; const usageId = this._getBubbleState(target).usageId; - return new Promise(resolve => { + return new Promise(resolve => { this._bubbleTimeout = setTimeout(() => { this._bubbleTimeout = null; // Clear say bubble if it hasn't been changed and proceed. @@ -348,15 +359,15 @@ class Scratch3LooksBlocks { }); } - think (args, util) { + think (args: BlockArgs, util: BlockUtility) { this.runtime.emit(Scratch3LooksBlocks.SAY_OR_THINK, util.target, 'think', args.MESSAGE); } - thinkforsecs (args, util) { + thinkforsecs (args: BlockArgs, util: BlockUtility) { this.think(args, util); const target = util.target; const usageId = this._getBubbleState(target).usageId; - return new Promise(resolve => { + return new Promise(resolve => { this._bubbleTimeout = setTimeout(() => { this._bubbleTimeout = null; // Clear think bubble if it hasn't been changed and proceed. @@ -368,12 +379,12 @@ class Scratch3LooksBlocks { }); } - show (args, util) { + show (args: BlockArgs, util: BlockUtility) { util.target.setVisible(true); this._renderBubble(util.target); } - hide (args, util) { + hide (args: BlockArgs, util: BlockUtility) { util.target.setVisible(false); this._renderBubble(util.target); } @@ -381,12 +392,16 @@ class Scratch3LooksBlocks { /** * Utility function to set the costume of a target. * Matches the behavior of Scratch 2.0 for different types of arguments. - * @param {!Target} target Target to set costume to. - * @param {any} requestedCostume Costume requested, e.g., 0, 'name', etc. - * @param {boolean=} optZeroIndex Set to zero-index the requestedCostume. - * @returns {Array.} Any threads started by this switch. + * @param target Target to set costume to. + * @param requestedCostume Costume requested, e.g., 0, 'name', etc. + * @param optZeroIndex Set to zero-index the requestedCostume. + * @returns Any threads started by this switch. */ - _setCostume (target, requestedCostume, optZeroIndex) { + _setCostume ( + target: RenderedTarget, + requestedCostume: number | 'next costume' | 'previous costume', + optZeroIndex?: boolean + ) { if (typeof requestedCostume === 'number') { // Numbers should be treated as costume indices, always target.setCostume(optZeroIndex ? requestedCostume : requestedCostume - 1); @@ -415,12 +430,16 @@ class Scratch3LooksBlocks { /** * Utility function to set the backdrop of a target. * Matches the behavior of Scratch 2.0 for different types of arguments. - * @param {!Target} stage Target to set backdrop to. - * @param {any} requestedBackdrop Backdrop requested, e.g., 0, 'name', etc. - * @param {boolean=} optZeroIndex Set to zero-index the requestedBackdrop. - * @returns {Array.} Any threads started by this switch. + * @param stage Target to set backdrop to. + * @param requestedBackdrop Backdrop requested, e.g., 0, 'name', etc. + * @param optZeroIndex Set to zero-index the requestedBackdrop. + * @returns Any threads started by this switch. */ - _setBackdrop (stage, requestedBackdrop, optZeroIndex) { + _setBackdrop ( + stage: RenderedTarget, + requestedBackdrop: number | 'next backdrop' | 'previous backdrop' | 'random backdrop', + optZeroIndex?: boolean + ) { if (typeof requestedBackdrop === 'number') { // Numbers should be treated as backdrop indices, always stage.setCostume(optZeroIndex ? requestedBackdrop : requestedBackdrop - 1); @@ -461,27 +480,27 @@ class Scratch3LooksBlocks { }); } - switchCostume (args, util) { + switchCostume (args: BlockArgs, util: BlockUtility) { this._setCostume(util.target, args.COSTUME); } - nextCostume (args, util) { + nextCostume (args: BlockArgs, util: BlockUtility) { this._setCostume( util.target, util.target.currentCostume + 1, true ); } - switchBackdrop (args) { - this._setBackdrop(this.runtime.getTargetForStage(), args.BACKDROP); + switchBackdrop (args: BlockArgs) { + this._setBackdrop(this.runtime.getTargetForStage()!, args.BACKDROP); } - switchBackdropAndWait (args, util) { + switchBackdropAndWait (args: BlockArgs, util: BlockUtility) { // Have we run before, starting threads? if (!util.stackFrame.startedThreads) { // No - switch the backdrop. util.stackFrame.startedThreads = ( this._setBackdrop( - this.runtime.getTargetForStage(), + this.runtime.getTargetForStage()!, args.BACKDROP ) ); @@ -491,20 +510,21 @@ class Scratch3LooksBlocks { } } // We've run before; check if the wait is still going on. + // eslint-disable-next-line @typescript-eslint/no-this-alias const instance = this; // Scratch 2 considers threads to be waiting if they are still in // runtime.threads. Threads that have run all their blocks, or are // marked done but still in runtime.threads are still considered to // be waiting. const waiting = util.stackFrame.startedThreads - .some(thread => instance.runtime.threads.indexOf(thread) !== -1); + .some((thread: Thread) => instance.runtime.threads.indexOf(thread) !== -1); if (waiting) { // If all threads are waiting for the next tick or later yield // for a tick as well. Otherwise yield until the next loop of // the threads. if ( util.stackFrame.startedThreads - .every(thread => instance.runtime.isWaitingThread(thread)) + .every((thread: Thread) => instance.runtime.isWaitingThread(thread)) ) { util.yieldTick(); } else { @@ -514,13 +534,13 @@ class Scratch3LooksBlocks { } nextBackdrop () { - const stage = this.runtime.getTargetForStage(); + const stage = this.runtime.getTargetForStage()!; this._setBackdrop( stage, stage.currentCostume + 1, true ); } - clampEffect (effect, value) { + clampEffect (effect: string, value: number): number { let clampedValue = value; switch (effect) { case 'ghost': @@ -537,7 +557,7 @@ class Scratch3LooksBlocks { return clampedValue; } - changeEffect (args, util) { + changeEffect (args: BlockArgs, util: BlockUtility) { const effect = Cast.toString(args.EFFECT).toLowerCase(); const change = Cast.toNumber(args.CHANGE); if (!Object.prototype.hasOwnProperty.call(util.target.effects, effect)) return; @@ -546,28 +566,28 @@ class Scratch3LooksBlocks { util.target.setEffect(effect, newValue); } - setEffect (args, util) { + setEffect (args: BlockArgs, util: BlockUtility) { const effect = Cast.toString(args.EFFECT).toLowerCase(); let value = Cast.toNumber(args.VALUE); value = this.clampEffect(effect, value); util.target.setEffect(effect, value); } - clearEffects (args, util) { + clearEffects (args: BlockArgs, util: BlockUtility) { util.target.clearEffects(); } - changeSize (args, util) { + changeSize (args: BlockArgs, util: BlockUtility) { const change = Cast.toNumber(args.CHANGE); util.target.setSize(util.target.size + change); } - setSize (args, util) { + setSize (args: BlockArgs, util: BlockUtility) { const size = Cast.toNumber(args.SIZE); util.target.setSize(size); } - goToFrontBack (args, util) { + goToFrontBack (args: BlockArgs, util: BlockUtility) { if (!util.target.isStage) { if (args.FRONT_BACK === 'front') { util.target.goToFront(); @@ -577,7 +597,7 @@ class Scratch3LooksBlocks { } } - goForwardBackwardLayers (args, util) { + goForwardBackwardLayers (args: BlockArgs, util: BlockUtility) { if (!util.target.isStage) { if (args.FORWARD_BACKWARD === 'forward') { util.target.goForwardLayers(Cast.toNumber(args.NUM)); @@ -587,12 +607,12 @@ class Scratch3LooksBlocks { } } - getSize (args, util) { + getSize (args: BlockArgs, util: BlockUtility) { return util.target.size; } - getBackdropNumberName (args) { - const stage = this.runtime.getTargetForStage(); + getBackdropNumberName (args: BlockArgs) { + const stage = this.runtime.getTargetForStage()!; if (args.NUMBER_NAME === 'number') { return stage.currentCostume + 1; } @@ -600,7 +620,7 @@ class Scratch3LooksBlocks { return stage.getCostumes()[stage.currentCostume].name; } - getCostumeNumberName (args, util) { + getCostumeNumberName (args: BlockArgs, util: BlockUtility) { if (args.NUMBER_NAME === 'number') { return util.target.currentCostume + 1; } diff --git a/packages/vm/src/blocks/scratch3_sensing.js b/packages/vm/src/blocks/scratch3_sensing.ts similarity index 81% rename from packages/vm/src/blocks/scratch3_sensing.js rename to packages/vm/src/blocks/scratch3_sensing.ts index 8bd241ad..54ca77c2 100644 --- a/packages/vm/src/blocks/scratch3_sensing.js +++ b/packages/vm/src/blocks/scratch3_sensing.ts @@ -3,45 +3,44 @@ import Color from '../util/color'; import MathUtil from '../util/math-util'; import Timer from '../util/timer'; import getMonitorIdForBlockWithArgs from '../util/get-monitor-id'; +import type {BlockArgs, CategoryPrototype} from './category_prototype'; +import type Runtime from '../engine/runtime'; +import type BlockUtility from '../engine/block-utility'; +import type {MonitorBlockInfo} from '../engine/runtime'; +import type RenderedTarget from '../sprites/rendered-target'; -class Scratch3SensingBlocks { - constructor (runtime) { - /** - * The runtime instantiating this block package. - * @type {Runtime} - */ - this.runtime = runtime; +class Scratch3SensingBlocks implements CategoryPrototype { + /** + * The "answer" block value. + */ + private _answer = ''; - /** - * The "answer" block value. - * @type {string} - */ - this._answer = ''; + /** + * The timer utility. + */ + private _timer: Timer = new Timer(); - /** - * The timer utility. - * @type {Timer} - */ - this._timer = new Timer(); + /** + * The stored microphone loudness measurement. + */ + private _cachedLoudness = -1; - /** - * The stored microphone loudness measurement. - * @type {number} - */ - this._cachedLoudness = -1; + /** + * The time of the most recent microphone loudness measurement. + */ + private _cachedLoudnessTimestamp = 0; - /** - * The time of the most recent microphone loudness measurement. - * @type {number} - */ - this._cachedLoudnessTimestamp = 0; + /** + * The list of queued questions and respective `resolve` callbacks. + */ + private _questionList: Array<[string, () => void, RenderedTarget, boolean, boolean]> = []; + constructor ( /** - * The list of queued questions and respective `resolve` callbacks. - * @type {!Array} + * The runtime instantiating this block package. */ - this._questionList = []; - + public runtime: Runtime + ) { this.runtime.on('ANSWER', this._onAnswer.bind(this)); this.runtime.on('PROJECT_START', this._resetAnswer.bind(this)); this.runtime.on('PROJECT_STOP_ALL', this._clearAllQuestions.bind(this)); @@ -51,7 +50,7 @@ class Scratch3SensingBlocks { /** * Retrieve the block primitives implemented by this package. - * @returns {Record} Mapping of opcode to Function. + * @returns Mapping of opcode to Function. */ getPrimitives () { return { @@ -80,7 +79,6 @@ class Scratch3SensingBlocks { sensing_username: this.getUsername, sensing_userid: () => {}, // legacy no-op block, sensing_operatingsystem: this.getOS, - // eslint-disable-next-line no-undef sensing_clipcc_version: () => clipcc.VERSION || 'unknown', // defined by WebpackDefinePlugin sensing_turnonturbomode: () => { this.setTurboMode(true); @@ -110,16 +108,17 @@ class Scratch3SensingBlocks { // This is different from the default toolbox xml id in order to support // importing multiple monitors from the same opcode from sb2 files, // something that is not currently supported in scratch 3. - getId: (_, fields) => getMonitorIdForBlockWithArgs('current', fields) // _${param}` + getId: (_, fields) => + getMonitorIdForBlockWithArgs('current', fields!) // _${param}` } - }; + } as Record; } - _onAnswer (answer) { + _onAnswer (answer: string) { this._answer = answer; const questionObj = this._questionList.shift(); if (questionObj) { - const [_question, resolve, target, wasVisible, wasStage] = questionObj; + const [, resolve, target, wasVisible, wasStage] = questionObj; // If the target was visible when asked, hide the say bubble unless the target was the stage. if (wasVisible && !wasStage) { this.runtime.emit('SAY', target, 'say', ''); @@ -133,13 +132,19 @@ class Scratch3SensingBlocks { this._answer = ''; } - _enqueueAsk (question, resolve, target, wasVisible, wasStage) { + _enqueueAsk ( + question: string, + resolve: () => void, + target: RenderedTarget, + wasVisible: boolean, + wasStage: boolean + ) { this._questionList.push([question, resolve, target, wasVisible, wasStage]); } _askNextQuestion () { if (this._questionList.length > 0) { - const [question, _resolve, target, wasVisible, wasStage] = this._questionList[0]; + const [question,, target, wasVisible, wasStage] = this._questionList[0]; // If the target is visible, emit a blank question and use the // say event to trigger a bubble unless the target was the stage. if (wasVisible && !wasStage) { @@ -156,7 +161,7 @@ class Scratch3SensingBlocks { this.runtime.emit('QUESTION', null); } - _clearTargetQuestions (stopTarget) { + _clearTargetQuestions (stopTarget: RenderedTarget) { const currentlyAsking = this._questionList.length > 0 && this._questionList[0][2] === stopTarget; this._questionList = this._questionList.filter(question => ( question[2] !== stopTarget @@ -172,9 +177,9 @@ class Scratch3SensingBlocks { } } - askAndWait (args, util) { + askAndWait (args: BlockArgs, util: BlockUtility) { const _target = util.target; - return new Promise(resolve => { + return new Promise(resolve => { const isQuestionAsked = this._questionList.length > 0; this._enqueueAsk(String(args.QUESTION), resolve, _target, _target.visible, _target.isStage); if (!isQuestionAsked) { @@ -187,22 +192,22 @@ class Scratch3SensingBlocks { return this._answer; } - touchingObject (args, util) { + touchingObject (args: BlockArgs, util: BlockUtility) { return util.target.isTouchingObject(args.TOUCHINGOBJECTMENU); } - touchingColor (args, util) { + touchingColor (args: BlockArgs, util: BlockUtility) { const color = Cast.toRgbColorList(args.COLOR); return util.target.isTouchingColor(color); } - colorTouchingColor (args, util) { + colorTouchingColor (args: BlockArgs, util: BlockUtility) { const maskColor = Cast.toRgbColorList(args.COLOR); const targetColor = Cast.toRgbColorList(args.COLOR2); return util.target.colorIsTouchingColor(targetColor, maskColor); } - distanceTo (args, util) { + distanceTo (args: BlockArgs, util: BlockUtility) { if (util.target.isStage) return 10000; let targetX = 0; @@ -225,13 +230,13 @@ class Scratch3SensingBlocks { return Math.sqrt((dx * dx) + (dy * dy)); } - distanceBetweenPosition (args) { + distanceBetweenPosition (args: BlockArgs) { const dx = args.X1 - args.X2; const dy = args.Y1 - args.Y2; return Math.sqrt((dx * dx) + (dy * dy)); } - directionBetweenPosition (args) { + directionBetweenPosition (args: BlockArgs) { const dx = args.X2 - args.X1; const dy = args.Y2 - args.Y1; let d = MathUtil.radToDeg(Math.atan(dx / dy)); @@ -242,7 +247,7 @@ class Scratch3SensingBlocks { return d; } - setTurboMode (turboModeOn) { + setTurboMode (turboModeOn: boolean) { this.runtime.turboMode = !!turboModeOn; if (this.runtime.turboMode) { this.runtime.emit('TURBO_MODE_ON'); @@ -251,47 +256,47 @@ class Scratch3SensingBlocks { } } - setDragMode (args, util) { + setDragMode (args: BlockArgs, util: BlockUtility) { util.target.setDraggable(args.DRAG_MODE === 'draggable'); } - getTimer (args, util) { + getTimer (args: BlockArgs, util: BlockUtility) { return util.ioQuery('clock', 'projectTimer'); } - resetTimer (args, util) { + resetTimer (args: BlockArgs, util: BlockUtility) { util.ioQuery('clock', 'resetProjectTimer'); } - getMouseX (args, util) { + getMouseX (args: BlockArgs, util: BlockUtility) { return util.ioQuery('mouse', 'getScratchX'); } - getMouseY (args, util) { + getMouseY (args: BlockArgs, util: BlockUtility) { return util.ioQuery('mouse', 'getScratchY'); } - getMouseDown (args, util) { + getMouseDown (args: BlockArgs, util: BlockUtility) { return util.ioQuery('mouse', 'getIsDown'); } - getMousePressed (args, util) { + getMousePressed (args: BlockArgs, util: BlockUtility) { return util.ioQuery('mouse', 'getMousePressed', [Number(args.MOUSE_OPTION)]); } - getJoystickX (args, util) { + getJoystickX (args: BlockArgs, util: BlockUtility) { return util.ioQuery('joystick', 'getX'); } - getJoystickY (args, util) { + getJoystickY (args: BlockArgs, util: BlockUtility) { return util.ioQuery('joystick', 'getY'); } - getJoystickDistance (args, util) { + getJoystickDistance (args: BlockArgs, util: BlockUtility) { return util.ioQuery('joystick', 'getDistance'); } - current (args) { + current (args: BlockArgs) { const menuOption = Cast.toString(args.CURRENTMENU).toLowerCase(); const date = new Date(); switch (menuOption) { @@ -306,7 +311,7 @@ class Scratch3SensingBlocks { return 0; } - getKeyPressed (args, util) { + getKeyPressed (args: BlockArgs, util: BlockUtility) { return util.ioQuery('keyboard', 'getKeyIsDown', [args.KEY_OPTION]); } @@ -320,7 +325,7 @@ class Scratch3SensingBlocks { return mSecsSinceStart / msPerDay; } - getLoudness () { + getLoudness (): number { if (typeof this.runtime.audioEngine === 'undefined') return -1; if (this.runtime.currentStepTime === null) return -1; @@ -339,7 +344,7 @@ class Scratch3SensingBlocks { return this.getLoudness() > 10; } - getAttributeOf (args) { + getAttributeOf (args: BlockArgs) { let attrTarget; if (args.OBJECT === '_stage_') { @@ -389,7 +394,7 @@ class Scratch3SensingBlocks { return 0; } - getUsername (args, util) { + getUsername (args: BlockArgs, util: BlockUtility) { return util.ioQuery('userData', 'getUsername'); } @@ -422,8 +427,8 @@ class Scratch3SensingBlocks { return 'Other'; } - colorAt (args) { - const renderer = this.runtime.renderer; + colorAt (args: BlockArgs) { + const renderer = this.runtime.renderer!; const x = Math.round(Number(args.X)); const y = Math.round(Number(args.Y)); const {color} = renderer.extractColorInScratchCoordinate(x, y); diff --git a/packages/vm/src/blocks/scratch3_sound.js b/packages/vm/src/blocks/scratch3_sound.ts similarity index 74% rename from packages/vm/src/blocks/scratch3_sound.js rename to packages/vm/src/blocks/scratch3_sound.ts index 20f75be0..1fcf0367 100644 --- a/packages/vm/src/blocks/scratch3_sound.js +++ b/packages/vm/src/blocks/scratch3_sound.ts @@ -1,24 +1,41 @@ import MathUtil from '../util/math-util'; import Cast from '../util/cast'; import Clone from '../util/clone'; +import type {BlockArgs, CategoryPrototype} from './category_prototype'; +import type Runtime from '../engine/runtime'; +import type BlockUtility from '../engine/block-utility'; +import type RenderedTarget from '../sprites/rendered-target'; /** * Occluded boolean value to make its use more understandable. - * @constant {boolean} */ const STORE_WAITING = true; -class Scratch3SoundBlocks { - constructor (runtime) { +/** + * The sound-related state, to be stored on a target. + */ +interface SoundState { + effects: { + [key: string]: number; + pitch: number; + pan: number; + }; +} + +interface TargetWithSoundState extends RenderedTarget { + soundEffects?: SoundState['effects']; +} + +class Scratch3SoundBlocks implements CategoryPrototype { + private waitingSounds: Record> = {}; + + constructor ( /** * The runtime instantiating this block package. - * @type {Runtime} */ - this.runtime = runtime; - - this.waitingSounds = {}; - - // Clear sound effects on green flag and stop button events. + public runtime: Runtime + ) { + // // Clear sound effects on green flag and stop button events. this.stopAllSounds = this.stopAllSounds.bind(this); this._stopWaitingSoundsForTarget = this._stopWaitingSoundsForTarget.bind(this); this._clearEffectsForAllTargets = this._clearEffectsForAllTargets.bind(this); @@ -37,17 +54,15 @@ class Scratch3SoundBlocks { /** * The key to load & store a target's sound-related state. - * @type {string} */ static get STATE_KEY () { - return 'Scratch.sound'; + return 'Scratch.sound' as const; } /** * The default sound-related state, to be used when a target has no existing sound state. - * @type {SoundState} */ - static get DEFAULT_SOUND_STATE () { + static get DEFAULT_SOUND_STATE (): SoundState { return { effects: { pitch: 0, @@ -58,34 +73,30 @@ class Scratch3SoundBlocks { /** * The minimum and maximum MIDI note numbers, for clamping the input to play note. - * @type {{min: number, max: number}} */ static get MIDI_NOTE_RANGE () { - return {min: 36, max: 96}; // C2 to C7 + return {min: 36, max: 96} as const; // C2 to C7 } /** * The minimum and maximum beat values, for clamping the duration of play note, play drum and rest. * 100 beats at the default tempo of 60bpm is 100 seconds. - * @type {{min: number, max: number}} */ static get BEAT_RANGE () { - return {min: 0, max: 100}; + return {min: 0, max: 100} as const; } /** * The minimum and maximum tempo values, in bpm. - * @type {{min: number, max: number}} */ static get TEMPO_RANGE () { - return {min: 20, max: 500}; + return {min: 20, max: 500} as const; } /** * The minimum and maximum values for each sound effect. - * @type {{effect:{min: number, max: number}}} */ - get EFFECT_RANGE () { + get EFFECT_RANGE (): {[key: string]: {min: number, max: number}} { if (this.runtime.limitOptions.unlimitedSoundStuffs) { return { pitch: {min: -Infinity, max: Infinity}, // Unlimited @@ -99,28 +110,27 @@ class Scratch3SoundBlocks { } /** - * @param {Target} target - collect sound state for this target. - * @returns {SoundState} the mutable sound state associated with that target. This will be created if necessary. - * @private + * Collect sound state for this target. + * @param target - the target to get the sound state for. + * @returns the mutable sound state associated with that target. This will be created if necessary. */ - _getSoundState (target) { - let soundState = target.getCustomState(Scratch3SoundBlocks.STATE_KEY); + _getSoundState (target: RenderedTarget): SoundState { + let soundState: SoundState = target.getCustomState(Scratch3SoundBlocks.STATE_KEY); if (!soundState) { soundState = Clone.simple(Scratch3SoundBlocks.DEFAULT_SOUND_STATE); target.setCustomState(Scratch3SoundBlocks.STATE_KEY, soundState); - target.soundEffects = soundState.effects; + (target as TargetWithSoundState).soundEffects = soundState.effects; } return soundState; } /** * When a Target is cloned, clone the sound state. - * @param {Target} newTarget - the newly created target. - * @param {Target} [sourceTarget] - the target used as a source for the new clone, if any. + * @param newTarget - the newly created target. + * @param sourceTarget - the target used as a source for the new clone, if any. * @listens Runtime#event:targetWasCreated - * @private */ - _onTargetCreated (newTarget, sourceTarget) { + _onTargetCreated (newTarget: RenderedTarget, sourceTarget?: RenderedTarget) { if (sourceTarget) { const soundState = sourceTarget.getCustomState(Scratch3SoundBlocks.STATE_KEY); if (soundState && newTarget) { @@ -132,7 +142,7 @@ class Scratch3SoundBlocks { /** * Retrieve the block primitives implemented by this package. - * @returns {Record} Mapping of opcode to Function. + * @returns Mapping of opcode to Function. */ getPrimitives () { return { @@ -155,21 +165,21 @@ class Scratch3SoundBlocks { return { sound_volume: { isSpriteSpecific: true, - getId: targetId => `${targetId}_volume` + getId: (targetId?: string) => `${targetId}_volume` } }; } - playSound (args, util) { + playSound (args: BlockArgs, util: BlockUtility) { // Don't return the promise, it's the only difference for AndWait this._playSound(args, util); } - playSoundAndWait (args, util) { + playSoundAndWait (args: BlockArgs, util: BlockUtility) { return this._playSound(args, util, STORE_WAITING); } - _playSound (args, util, storeWaiting) { + _playSound (args: BlockArgs, util: BlockUtility, storeWaiting?: boolean) { const index = this._getSoundIndex(args.SOUND_MENU, util); if (index >= 0) { const {target} = util; @@ -186,21 +196,21 @@ class Scratch3SoundBlocks { } } - _addWaitingSound (targetId, soundId) { + _addWaitingSound (targetId: string, soundId: string) { if (!this.waitingSounds[targetId]) { this.waitingSounds[targetId] = new Set(); } this.waitingSounds[targetId].add(soundId); } - _removeWaitingSound (targetId, soundId) { + _removeWaitingSound (targetId: string, soundId: string) { if (!this.waitingSounds[targetId]) { return; } this.waitingSounds[targetId].delete(soundId); } - _getSoundIndex (soundName, util) { + _getSoundIndex (soundName: string, util: BlockUtility): number { // if the sprite has no sounds, return -1 const len = util.target.sprite.sounds.length; if (len === 0) { @@ -223,7 +233,7 @@ class Scratch3SoundBlocks { return -1; } - getSoundIndexByName (soundName, util) { + getSoundIndexByName (soundName: string, util: BlockUtility): number { const sounds = util.target.sprite.sounds; for (let i = 0; i < sounds.length; i++) { if (sounds[i].name === soundName) { @@ -242,7 +252,7 @@ class Scratch3SoundBlocks { } } - _stopAllSoundsForTarget (target) { + _stopAllSoundsForTarget (target: RenderedTarget) { if (target.sprite.soundBank) { target.sprite.soundBank.stopAllSounds(target); if (this.waitingSounds[target.id]) { @@ -251,7 +261,7 @@ class Scratch3SoundBlocks { } } - _stopWaitingSoundsForTarget (target) { + _stopWaitingSoundsForTarget (target: RenderedTarget) { if (target.sprite.soundBank) { if (this.waitingSounds[target.id]) { for (const soundId of this.waitingSounds[target.id].values()) { @@ -262,15 +272,15 @@ class Scratch3SoundBlocks { } } - setEffect (args, util) { + setEffect (args: BlockArgs, util: BlockUtility) { return this._updateEffect(args, util, false); } - changeEffect (args, util) { + changeEffect (args: BlockArgs, util: BlockUtility) { return this._updateEffect(args, util, true); } - _updateEffect (args, util, change) { + _updateEffect (args: BlockArgs, util: BlockUtility, change: boolean) { const effect = Cast.toString(args.EFFECT).toLowerCase(); const value = Cast.toNumber(args.VALUE); @@ -291,18 +301,18 @@ class Scratch3SoundBlocks { return Promise.resolve(); } - _syncEffectsForTarget (target) { + _syncEffectsForTarget (target: TargetWithSoundState) { if (!target || !target.sprite.soundBank) return; target.soundEffects = this._getSoundState(target).effects; target.sprite.soundBank.setEffects(target); } - clearEffects (args, util) { + clearEffects (args: BlockArgs, util: BlockUtility) { this._clearEffectsForTarget(util.target); } - _clearEffectsForTarget (target) { + _clearEffectsForTarget (target: RenderedTarget) { const soundState = this._getSoundState(target); for (const effect in soundState.effects) { if (!Object.prototype.hasOwnProperty.call(soundState.effects, effect)) continue; @@ -319,17 +329,17 @@ class Scratch3SoundBlocks { } } - setVolume (args, util) { + setVolume (args: BlockArgs, util: BlockUtility) { const volume = Cast.toNumber(args.VOLUME); return this._updateVolume(volume, util); } - changeVolume (args, util) { + changeVolume (args: BlockArgs, util: BlockUtility) { const volume = Cast.toNumber(args.VOLUME) + util.target.volume; return this._updateVolume(volume, util); } - _updateVolume (volume, util) { + _updateVolume (volume: number, util: BlockUtility) { volume = MathUtil.clamp(volume, 0, 100); util.target.volume = volume; this._syncEffectsForTarget(util.target); @@ -338,19 +348,19 @@ class Scratch3SoundBlocks { return Promise.resolve(); } - getVolume (args, util) { + getVolume (args: BlockArgs, util: BlockUtility) { return util.target.volume; } - soundsMenu (args) { + soundsMenu (args: BlockArgs) { return args.SOUND_MENU; } - beatsMenu (args) { + beatsMenu (args: BlockArgs) { return args.BEATS; } - effectsMenu (args) { + effectsMenu (args: BlockArgs) { return args.EFFECT; } } diff --git a/packages/vm/src/engine/runtime.js b/packages/vm/src/engine/runtime.js index e38ff651..eb113934 100644 --- a/packages/vm/src/engine/runtime.js +++ b/packages/vm/src/engine/runtime.js @@ -30,11 +30,11 @@ import StringUtil from '../util/string-util'; import uid from '../util/uid'; import control from '../blocks/scratch3_control'; import event from '../blocks/scratch3_event'; -import looks from '../blocks/scratch3_looks.js'; +import looks from '../blocks/scratch3_looks'; import motion from '../blocks/scratch3_motion'; import operators from '../blocks/scratch3_operators'; -import sound from '../blocks/scratch3_sound.js'; -import sensing from '../blocks/scratch3_sensing.js'; +import sound from '../blocks/scratch3_sound'; +import sensing from '../blocks/scratch3_sensing'; import data from '../blocks/scratch3_data'; import procedures from '../blocks/scratch3_procedures'; @@ -184,7 +184,7 @@ const defaultExtensionColors = ['#0FBD8C', '#0DA57A', '#0B8E69']; /** * @typedef {{ * isSpriteSpecific?: boolean, - * getId: (targetId?: string, fields?: Record) => string + * getId: (targetId?: string, fields?: Record) => string * }} MonitorBlockInfo */ diff --git a/packages/vm/src/sprites/rendered-target.js b/packages/vm/src/sprites/rendered-target.js index 00e126d1..a5cc4366 100644 --- a/packages/vm/src/sprites/rendered-target.js +++ b/packages/vm/src/sprites/rendered-target.js @@ -5,6 +5,10 @@ import Clone from '../util/clone'; import Target from '../engine/target.js'; import StageLayering from '../engine/stage-layering'; +/** + * @typedef {import('../../../render/dist/types/Rectangle')} Rectangle + */ + /** * @typedef {import('../engine/runtime').default} Runtime */ @@ -619,7 +623,7 @@ class RenderedTarget extends Target { /** * Get full costume list - * @returns {object[]} list of costumes + * @returns list of costumes */ getCostumes () { return this.sprite.costumes; diff --git a/packages/vm/src/util/get-monitor-id.ts b/packages/vm/src/util/get-monitor-id.ts index 0c8cf7f7..2e56ee1e 100644 --- a/packages/vm/src/util/get-monitor-id.ts +++ b/packages/vm/src/util/get-monitor-id.ts @@ -9,8 +9,11 @@ */ // TODO this function should eventually be the single place where all monitor // IDs are obtained given an opcode for the reporter block and the list of + +import type {VMField} from '../serialization/schema'; + // selected parameters. -const getMonitorIdForBlockWithArgs = function (id: string, fields: Record): string { +const getMonitorIdForBlockWithArgs = function (id: string, fields: Record): string { let fieldString = ''; for (const fieldKey in fields) { let fieldValue = fields[fieldKey].value; @@ -24,7 +27,7 @@ const getMonitorIdForBlockWithArgs = function (id: string, fields: Record Date: Mon, 11 May 2026 11:15:45 +0800 Subject: [PATCH 20/30] :wrench: chore(vm): migrate sprite.js Signed-off-by: SimonShiki --- packages/vm/src/engine/blocks.js | 2 +- packages/vm/src/engine/stage-layering.ts | 15 ++- packages/vm/src/import/load-costume.js | 4 +- packages/vm/src/serialization/sb2.js | 2 +- packages/vm/src/serialization/sb3.js | 2 +- packages/vm/src/sprites/rendered-target.js | 2 +- .../vm/src/sprites/{sprite.js => sprite.ts} | 111 ++++++++++-------- packages/vm/src/types/global.d.ts | 5 +- packages/vm/src/util/new-block-ids.ts | 19 +-- .../vm/test/integration/internal-extension.js | 2 +- packages/vm/test/integration/sb3-roundtrip.js | 2 +- packages/vm/test/unit/blocks_looks.js | 2 +- packages/vm/test/unit/blocks_motion.js | 2 +- packages/vm/test/unit/blocks_sensing.js | 2 +- packages/vm/test/unit/engine_sequencer.js | 26 ++-- packages/vm/test/unit/engine_thread.js | 2 +- .../vm/test/unit/sprites_rendered-target.js | 2 +- packages/vm/test/unit/virtual-machine.js | 2 +- packages/vm/test/unit/vm_collectAssets.js | 2 +- 19 files changed, 108 insertions(+), 98 deletions(-) rename packages/vm/src/sprites/{sprite.js => sprite.ts} (69%) diff --git a/packages/vm/src/engine/blocks.js b/packages/vm/src/engine/blocks.js index 67e1b7d8..2273d80f 100644 --- a/packages/vm/src/engine/blocks.js +++ b/packages/vm/src/engine/blocks.js @@ -80,7 +80,7 @@ class Blocks { /** * All blocks in the workspace. * Keys are block IDs, values are metadata about the block. - * @type {Record} + * @type {Record} */ this._blocks = {}; diff --git a/packages/vm/src/engine/stage-layering.ts b/packages/vm/src/engine/stage-layering.ts index 396bea7f..c125698e 100644 --- a/packages/vm/src/engine/stage-layering.ts +++ b/packages/vm/src/engine/stage-layering.ts @@ -1,18 +1,25 @@ +export const enum StageLayer { + BACKGROUND = 'background', + VIDEO = 'video', + PEN = 'pen', + SPRITE = 'sprite' +} + class StageLayering { static get BACKGROUND_LAYER () { - return 'background'; + return StageLayer.BACKGROUND; } static get VIDEO_LAYER () { - return 'video'; + return StageLayer.VIDEO; } static get PEN_LAYER () { - return 'pen'; + return StageLayer.PEN; } static get SPRITE_LAYER () { - return 'sprite'; + return StageLayer.SPRITE; } // Order of layer groups relative to each other, diff --git a/packages/vm/src/import/load-costume.js b/packages/vm/src/import/load-costume.js index b0cf93b6..a95a341a 100644 --- a/packages/vm/src/import/load-costume.js +++ b/packages/vm/src/import/load-costume.js @@ -298,9 +298,9 @@ const handleCostumeLoadError = function (costume, runtime) { * @property {number} [bitmapResolution] - the resolution scale for a bitmap costume. * @property {!Asset} costume.asset - the asset of the costume loaded from storage. * @param {!Runtime} runtime - Scratch runtime, used to access the storage module. - * @param {?int} optVersion - Version of Scratch that the costume comes from. If this is set + * @param {?int} [optVersion] - Version of Scratch that the costume comes from. If this is set * to 2, scratch 3 will perform an upgrade step to handle quirks in SVGs from Scratch 2.0. - * @returns {?Promise} - a promise which will resolve after skinId is set, or null on error. + * @returns {Promise} - a promise which will resolve after skinId is set, or null on error. */ const loadCostumeFromAsset = function (costume, runtime, optVersion) { costume.assetId = costume.asset.assetId; diff --git a/packages/vm/src/serialization/sb2.js b/packages/vm/src/serialization/sb2.js index 0cd3f41c..afca5d25 100644 --- a/packages/vm/src/serialization/sb2.js +++ b/packages/vm/src/serialization/sb2.js @@ -12,7 +12,7 @@ import Blocks from '../engine/blocks.js'; import RenderedTarget from '../sprites/rendered-target.js'; -import Sprite from '../sprites/sprite.js'; +import Sprite from '../sprites/sprite'; import Color from '../util/color'; import log from '../util/log'; import uid from '../util/uid'; diff --git a/packages/vm/src/serialization/sb3.js b/packages/vm/src/serialization/sb3.js index 24b5bdd2..82f8d450 100644 --- a/packages/vm/src/serialization/sb3.js +++ b/packages/vm/src/serialization/sb3.js @@ -7,7 +7,7 @@ import vmPackage from '../../package.json'; import Blocks from '../engine/blocks.js'; -import Sprite from '../sprites/sprite.js'; +import Sprite from '../sprites/sprite'; import Variable from '../engine/variable'; import Comment from '../engine/comment'; import MonitorRecord from '../engine/monitor-record'; diff --git a/packages/vm/src/sprites/rendered-target.js b/packages/vm/src/sprites/rendered-target.js index a5cc4366..525ca5c1 100644 --- a/packages/vm/src/sprites/rendered-target.js +++ b/packages/vm/src/sprites/rendered-target.js @@ -173,7 +173,7 @@ class RenderedTarget extends Target { /** * Create a drawable with the this.renderer. - * @param {boolean} layerGroup The layer group this drawable should be added to + * @param {string} layerGroup The layer group this drawable should be added to */ initDrawable (layerGroup) { if (this.renderer) { diff --git a/packages/vm/src/sprites/sprite.js b/packages/vm/src/sprites/sprite.ts similarity index 69% rename from packages/vm/src/sprites/sprite.js rename to packages/vm/src/sprites/sprite.ts index 1677b820..a9e0c0c2 100644 --- a/packages/vm/src/sprites/sprite.js +++ b/packages/vm/src/sprites/sprite.ts @@ -5,52 +5,69 @@ import {loadCostumeFromAsset} from '../import/load-costume.js'; import newBlockIds from '../util/new-block-ids'; import StringUtil from '../util/string-util'; import StageLayering from '../engine/stage-layering'; +import type {StageLayer} from '../engine/stage-layering'; +import type Runtime from '../engine/runtime.js'; +import type SoundBank from '../../../audio/dist/types/SoundBank'; +import type {Asset} from 'clipcc-storage'; +import type {VMBlock} from '../serialization/schema'; + +export interface Costume { + skinId: number; + name: string; + md5: string; + bitmapResolution: number; + rotationCenterX: number; + rotationCenterY: number; +} + +export interface Sound { + soundId: string; + rate: number; + sampleCount: number; + asset: Asset; + md5: string; +} +/** + * Sprite to be used on the Scratch stage. + * All clones of a sprite have shared blocks, shared costumes, shared variables, + * shared sounds, etc. + */ class Sprite { /** - * Sprite to be used on the Scratch stage. - * All clones of a sprite have shared blocks, shared costumes, shared variables, - * shared sounds, etc. - * @param {?Blocks} blocks Shared blocks object for all clones of sprite. - * @param {Runtime} runtime Reference to the runtime. - * @class + * Shared blocks object for all clones of sprite. + */ + blocks: Blocks; + /** + * Human-readable name for this sprite (and all clones). + */ + name = ''; + /** + * List of costumes for this sprite. + */ + costumes_: Costume[] = []; + /** + * List of sounds for this sprite. + */ + sounds: Sound[] = []; + /** + * List of clones for this sprite, including the original. */ - constructor (blocks, runtime) { - this.runtime = runtime; + clones: RenderedTarget[] = []; + soundBank: SoundBank | null = null; + constructor ( + blocks: Blocks | null, + /** + * Reference to the runtime. + */ + public runtime: Runtime + ) { if (!blocks) { // Shared set of blocks for all clones. blocks = new Blocks(runtime); } this.blocks = blocks; - /** - * Human-readable name for this sprite (and all clones). - * @type {string} - */ - this.name = ''; - /** - * List of costumes for this sprite. - * Each entry is an object, e.g., - * { - * skinId: 1, - * name: "Costume Name", - * bitmapResolution: 2, - * rotationCenterX: 0, - * rotationCenterY: 0 - * } - * @type {Array.} - */ - this.costumes_ = []; - /** - * List of sounds for this sprite. - */ - this.sounds = []; - /** - * List of clones for this sprite, including the original. - * @type {Array.} - */ - this.clones = []; - this.soundBank = null; if (this.runtime && this.runtime.audioEngine) { this.soundBank = this.runtime.audioEngine.createBank(); } @@ -58,7 +75,7 @@ class Sprite { /** * Add an array of costumes, taking care to avoid duplicate names. - * @param {!Array} costumes Array of objects representing costumes. + * @param costumes Array of objects representing costumes. */ set costumes (costumes) { this.costumes_ = []; @@ -79,10 +96,10 @@ class Sprite { /** * Add a costume at the given index, taking care to avoid duplicate names. - * @param {!object} costumeObject Object representing the costume. + * @param costumeObject Object representing the costume. * @param {!int} index Index at which to add costume */ - addCostumeAt (costumeObject, index) { + addCostumeAt (costumeObject: Costume, index: number) { if (!costumeObject.name) { costumeObject.name = ''; } @@ -96,17 +113,17 @@ class Sprite { * @param {number} index Costume index to be deleted * @returns {?object} The deleted costume */ - deleteCostumeAt (index) { - return this.costumes.splice(index, 1)[0]; + deleteCostumeAt (index: number) { + return this.costumes_.splice(index, 1)[0]; } /** * Create a clone of this sprite. - * @param {string=} optLayerGroup Optional layer group the clone's drawable should be added to + * @param optLayerGroup Optional layer group the clone's drawable should be added to * Defaults to the sprite layer group * @returns {!RenderedTarget} Newly created clone. */ - createClone (optLayerGroup) { + createClone (optLayerGroup: StageLayer) { const newClone = new RenderedTarget(this, this.runtime); newClone.isOriginal = this.clones.length === 0; this.clones.push(newClone); @@ -125,9 +142,9 @@ class Sprite { /** * Disconnect a clone from this sprite. The clone is unmodified. * In particular, the clone's dispose() method is not called. - * @param {!RenderedTarget} clone - the clone to be removed. + * @param clone - the clone to be removed. */ - removeClone (clone) { + removeClone (clone: RenderedTarget) { this.runtime.fireTargetWasRemoved(clone); const cloneIndex = this.clones.indexOf(clone); if (cloneIndex >= 0) { @@ -139,7 +156,7 @@ class Sprite { const newSprite = new Sprite(null, this.runtime); const blocksContainer = this.blocks._blocks; const originalBlocks = Object.keys(blocksContainer).map(key => blocksContainer[key]); - const copiedBlocks = JSON.parse(JSON.stringify(originalBlocks)); + const copiedBlocks = JSON.parse(JSON.stringify(originalBlocks)) as VMBlock[]; newBlockIds(copiedBlocks); copiedBlocks.forEach(block => { newSprite.blocks.createBlock(block); @@ -149,7 +166,7 @@ class Sprite { const allNames = this.runtime.targets.map(t => t.sprite.name); newSprite.name = StringUtil.unusedName(this.name, allNames); - const assetPromises = []; + const assetPromises: Promise[] = []; newSprite.costumes = this.costumes_.map(costume => { const newCostume = Object.assign({}, costume); diff --git a/packages/vm/src/types/global.d.ts b/packages/vm/src/types/global.d.ts index a6c71188..146675be 100644 --- a/packages/vm/src/types/global.d.ts +++ b/packages/vm/src/types/global.d.ts @@ -3,10 +3,7 @@ declare module 'decode-html' { export default decodeHtml; } -declare global { - type int = number; -} - +type int = number; /** * Compile-time injected clipcc global metadata */ diff --git a/packages/vm/src/util/new-block-ids.ts b/packages/vm/src/util/new-block-ids.ts index caa5e99d..11128630 100644 --- a/packages/vm/src/util/new-block-ids.ts +++ b/packages/vm/src/util/new-block-ids.ts @@ -1,23 +1,12 @@ +import type {VMBlock} from '../serialization/schema'; import uid from './uid'; -interface BlockInput { - block: string; - shadow: string; -} - -interface BlockWithMutation { - id: string; - inputs: Record; - parent?: string; - next?: string; -} - /** * Mutate the given blocks to have new IDs and update all internal ID references. * Does not return anything to make it clear that the blocks are updated in-place. * @param blocks - blocks to be mutated. */ -export default (blocks: BlockWithMutation[]): void => { +export default (blocks: VMBlock[]): void => { const oldToNew: Record = {}; // First update all top-level IDs and create old-to-new mapping @@ -32,8 +21,8 @@ export default (blocks: BlockWithMutation[]): void => { for (let i = 0; i < blocks.length; i++) { for (const key in blocks[i].inputs) { const input = blocks[i].inputs[key]; - input.block = oldToNew[input.block]; - input.shadow = oldToNew[input.shadow]; + input.block = oldToNew[input.block!]; + input.shadow = oldToNew[input.shadow!]; } const parent = blocks[i].parent; if (parent) { diff --git a/packages/vm/test/integration/internal-extension.js b/packages/vm/test/integration/internal-extension.js index 88498b9a..a1fda9f4 100644 --- a/packages/vm/test/integration/internal-extension.js +++ b/packages/vm/test/integration/internal-extension.js @@ -3,7 +3,7 @@ import Worker from 'tiny-worker'; import BlockType from '../../src/extension-support/block-type'; import dispatch from '../../src/dispatch/central-dispatch'; import VirtualMachine from '../../src/virtual-machine.js'; -import Sprite from '../../src/sprites/sprite.js'; +import Sprite from '../../src/sprites/sprite'; import RenderedTarget from '../../src/sprites/rendered-target.js'; // By default Central Dispatch works with the Worker class built into the browser. Tell it to use TinyWorker instead. diff --git a/packages/vm/test/integration/sb3-roundtrip.js b/packages/vm/test/integration/sb3-roundtrip.js index 3dcdc9cd..347f05d3 100644 --- a/packages/vm/test/integration/sb3-roundtrip.js +++ b/packages/vm/test/integration/sb3-roundtrip.js @@ -6,7 +6,7 @@ import {loadSound} from '../../src/import/load-sound.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import Runtime from '../../src/engine/runtime.js'; import * as sb3 from '../../src/serialization/sb3.js'; -import Sprite from '../../src/sprites/sprite.js'; +import Sprite from '../../src/sprites/sprite'; const defaultCostumeInfo = { bitmapResolution: 1, diff --git a/packages/vm/test/unit/blocks_looks.js b/packages/vm/test/unit/blocks_looks.js index faaef070..2da32b9d 100644 --- a/packages/vm/test/unit/blocks_looks.js +++ b/packages/vm/test/unit/blocks_looks.js @@ -1,7 +1,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import Looks from '../../src/blocks/scratch3_looks'; import Runtime from '../../src/engine/runtime.js'; -import Sprite from '../../src/sprites/sprite.js'; +import Sprite from '../../src/sprites/sprite'; import RenderedTarget from '../../src/sprites/rendered-target.js'; const util = { target: { diff --git a/packages/vm/test/unit/blocks_motion.js b/packages/vm/test/unit/blocks_motion.js index ec25dbeb..8dd11daf 100644 --- a/packages/vm/test/unit/blocks_motion.js +++ b/packages/vm/test/unit/blocks_motion.js @@ -1,7 +1,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import Motion from '../../src/blocks/scratch3_motion'; import Runtime from '../../src/engine/runtime.js'; -import Sprite from '../../src/sprites/sprite.js'; +import Sprite from '../../src/sprites/sprite'; import RenderedTarget from '../../src/sprites/rendered-target.js'; import VirtualMachine from '../../src/index.js'; diff --git a/packages/vm/test/unit/blocks_sensing.js b/packages/vm/test/unit/blocks_sensing.js index 27a286ba..1b1360fa 100644 --- a/packages/vm/test/unit/blocks_sensing.js +++ b/packages/vm/test/unit/blocks_sensing.js @@ -1,7 +1,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import Sensing from '../../src/blocks/scratch3_sensing'; import Runtime from '../../src/engine/runtime.js'; -import Sprite from '../../src/sprites/sprite.js'; +import Sprite from '../../src/sprites/sprite'; import RenderedTarget from '../../src/sprites/rendered-target.js'; import BlockUtility from '../../src/engine/block-utility.js'; diff --git a/packages/vm/test/unit/engine_sequencer.js b/packages/vm/test/unit/engine_sequencer.js index e6036465..c581849d 100644 --- a/packages/vm/test/unit/engine_sequencer.js +++ b/packages/vm/test/unit/engine_sequencer.js @@ -3,17 +3,17 @@ import Sequencer from '../../src/engine/sequencer.js'; import Runtime from '../../src/engine/runtime.js'; import Thread from '../../src/engine/thread.js'; import RenderedTarget from '../../src/sprites/rendered-target.js'; -import Sprite from '../../src/sprites/sprite.js'; +import Sprite from '../../src/sprites/sprite'; test('spec', t => { t.type(Sequencer, 'function'); - + const r = new Runtime(); const s = new Sequencer(r); t.type(s, 'object'); t.ok(s instanceof Sequencer); - + t.type(s.stepThreads, 'function'); t.type(s.stepThread, 'function'); t.type(s.stepToBranch, 'function'); @@ -70,20 +70,20 @@ const generateThread = function (runtime) { const s = new Sprite(null, runtime); const rt = new RenderedTarget(s, runtime); const th = new Thread(randomString()); - + let next = randomString(); let inp = randomString(); let name = th.topBlock; - + rt.blocks.createBlock(generateBlockInput(name, next, inp)); th.pushStack(name, rt); rt.blocks.createBlock(generateBlock(inp)); - + for (let i = 0; i < 10; i++) { name = next; next = randomString(); inp = randomString(); - + rt.blocks.createBlock(generateBlockInput(name, next, inp)); th.pushStack(name, rt); rt.blocks.createBlock(generateBlock(inp)); @@ -112,7 +112,7 @@ test('stepThread', t => { th.status = Thread.STATUS_PROMISE_WAIT; s.stepThread(th); t.not(th.status, Thread.STATUS_DONE); - + t.end(); }); @@ -129,7 +129,7 @@ test('stepToBranch', t => { th.popStack(); s.stepToBranch(th, 1, false); t.not(th.peekStack(), null); - + t.end(); }); @@ -141,7 +141,7 @@ test('retireThread', t => { s.retireThread(th); t.equal(th.stack.length, 0); t.equal(th.status, Thread.STATUS_DONE); - + t.end(); }); @@ -169,8 +169,8 @@ test('stepToProcedure', t => { }; s.stepToProcedure(th, 'othercode'); t.equal(th.peekStack(), expectedBlock); - - + + t.end(); }); @@ -183,6 +183,6 @@ test('stepThreads', t => { t.equal(r.threads.length, 1); // Threads should be marked DONE and removed in the same step they finish. t.equal(s.stepThreads().length, 1); - + t.end(); }); diff --git a/packages/vm/test/unit/engine_thread.js b/packages/vm/test/unit/engine_thread.js index 001b4632..a3dfac4f 100644 --- a/packages/vm/test/unit/engine_thread.js +++ b/packages/vm/test/unit/engine_thread.js @@ -1,7 +1,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import Thread from '../../src/engine/thread.js'; import RenderedTarget from '../../src/sprites/rendered-target.js'; -import Sprite from '../../src/sprites/sprite.js'; +import Sprite from '../../src/sprites/sprite'; import Runtime from '../../src/engine/runtime.js'; test('spec', t => { diff --git a/packages/vm/test/unit/sprites_rendered-target.js b/packages/vm/test/unit/sprites_rendered-target.js index 65ab8d9d..e693c09d 100644 --- a/packages/vm/test/unit/sprites_rendered-target.js +++ b/packages/vm/test/unit/sprites_rendered-target.js @@ -1,6 +1,6 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import RenderedTarget from '../../src/sprites/rendered-target.js'; -import Sprite from '../../src/sprites/sprite.js'; +import Sprite from '../../src/sprites/sprite'; import Runtime from '../../src/engine/runtime.js'; import FakeRenderer from '../fixtures/fake-renderer.js'; diff --git a/packages/vm/test/unit/virtual-machine.js b/packages/vm/test/unit/virtual-machine.js index a12c3b73..92d1c54a 100644 --- a/packages/vm/test/unit/virtual-machine.js +++ b/packages/vm/test/unit/virtual-machine.js @@ -1,6 +1,6 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import VirtualMachine from '../../src/virtual-machine.js'; -import Sprite from '../../src/sprites/sprite.js'; +import Sprite from '../../src/sprites/sprite'; import Variable from '../../src/engine/variable'; import adapter from '../../src/engine/adapter'; import events from '../fixtures/events.json'; diff --git a/packages/vm/test/unit/vm_collectAssets.js b/packages/vm/test/unit/vm_collectAssets.js index ff676407..fe88e10d 100644 --- a/packages/vm/test/unit/vm_collectAssets.js +++ b/packages/vm/test/unit/vm_collectAssets.js @@ -1,6 +1,6 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import RenderedTarget from '../../src/sprites/rendered-target.js'; -import Sprite from '../../src/sprites/sprite.js'; +import Sprite from '../../src/sprites/sprite'; import VirtualMachine from '../../src/virtual-machine.js'; test('collectAssets', t => { From 016e831f247330af88881ede3b6be48e00e9d067 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Mon, 11 May 2026 11:23:42 +0800 Subject: [PATCH 21/30] :wrench: chore(vm): migrate thread.js Signed-off-by: SimonShiki --- packages/vm/src/engine/block-utility.js | 2 +- packages/vm/src/engine/blocks.js | 21 +- packages/vm/src/engine/execute.js | 2 +- packages/vm/src/engine/runtime.js | 2 +- packages/vm/src/engine/sequencer.js | 2 +- .../vm/src/engine/{thread.js => thread.ts} | 368 ++++++++---------- .../hat-threads-run-every-frame.js | 2 +- .../monitor-threads-run-every-frame.js | 2 +- packages/vm/test/unit/blocks_event.js | 2 +- packages/vm/test/unit/engine_sequencer.js | 2 +- packages/vm/test/unit/engine_thread.js | 2 +- 11 files changed, 176 insertions(+), 231 deletions(-) rename packages/vm/src/engine/{thread.js => thread.ts} (52%) diff --git a/packages/vm/src/engine/block-utility.js b/packages/vm/src/engine/block-utility.js index a97189d6..08dbf792 100644 --- a/packages/vm/src/engine/block-utility.js +++ b/packages/vm/src/engine/block-utility.js @@ -1,4 +1,4 @@ -import Thread from './thread.js'; +import Thread from './thread'; import Timer from '../util/timer'; /** diff --git a/packages/vm/src/engine/blocks.js b/packages/vm/src/engine/blocks.js index 2273d80f..c8cbe335 100644 --- a/packages/vm/src/engine/blocks.js +++ b/packages/vm/src/engine/blocks.js @@ -15,6 +15,7 @@ import getMonitorIdForBlockWithArgs from '../util/get-monitor-id'; /** * @typedef {import('./runtime')} Runtime + * @typedef {import('../serialization/schema').VMBlock} VMBlock * @typedef {import('./blocks-runtime-cache').RuntimeScriptCache} RuntimeScriptCache * @import * as ClipCCBlock from 'clipcc-block' */ @@ -80,7 +81,7 @@ class Blocks { /** * All blocks in the workspace. * Keys are block IDs, values are metadata about the block. - * @type {Record} + * @type {Record} */ this._blocks = {}; @@ -123,7 +124,7 @@ class Blocks { /** * Provide an object with metadata for the requested block ID. * @param {!string} blockId ID of block we have stored. - * @returns {?object} Metadata about the block, if it exists. + * @returns Metadata about the block, if it exists. */ getBlock (blockId) { return this._blocks[blockId]; @@ -170,8 +171,8 @@ class Blocks { /** * Get the opcode for a particular block - * @param {?object} block The block to query - * @returns {?string} the opcode corresponding to that block + * @param {?VMBlock} block The block to query + * @returns the opcode corresponding to that block */ getOpcode (block) { return (typeof block === 'undefined') ? null : block.opcode; @@ -179,8 +180,8 @@ class Blocks { /** * Get all fields and their values for a block. - * @param {?object} block The block to query. - * @returns {?object} All fields and their values. + * @param {?VMBlock} block The block to query. + * @returns All fields and their values. */ getFields (block) { return (typeof block === 'undefined') ? null : block.fields; @@ -188,8 +189,8 @@ class Blocks { /** * Get all non-branch inputs for a block. - * @param {?object} block the block to query. - * @returns {?Array.} All non-branch inputs and their associated blocks. + * @param {?VMBlock} block the block to query. + * @returns All non-branch inputs and their associated blocks. */ getInputs (block) { if (typeof block === 'undefined') return null; @@ -213,8 +214,8 @@ class Blocks { /** * Get mutation data for a block. - * @param {?object} block The block to query. - * @returns {?object} Mutation for the block. + * @param {?VMBlock} block The block to query. + * @returns Mutation for the block. */ getMutation (block) { return (typeof block === 'undefined') ? null : block.mutation; diff --git a/packages/vm/src/engine/execute.js b/packages/vm/src/engine/execute.js index a7bb0211..eb8f97aa 100644 --- a/packages/vm/src/engine/execute.js +++ b/packages/vm/src/engine/execute.js @@ -1,7 +1,7 @@ import BlockUtility from './block-utility.js'; import {getCached as getCachedExecuteBlock} from './blocks-execute-cache.js'; import log from '../util/log'; -import Thread from './thread.js'; +import Thread from './thread'; import {Map} from 'immutable'; import cast from '../util/cast'; diff --git a/packages/vm/src/engine/runtime.js b/packages/vm/src/engine/runtime.js index eb113934..cd084616 100644 --- a/packages/vm/src/engine/runtime.js +++ b/packages/vm/src/engine/runtime.js @@ -9,7 +9,7 @@ import Sequencer from './sequencer.js'; import execute from './execute.js'; import ScratchBlocksConstants from './scratch-blocks-constants'; import TargetType from '../extension-support/target-type'; -import Thread from './thread.js'; +import Thread from './thread'; import log from '../util/log'; import maybeFormatMessage from '../util/maybe-format-message'; import StageLayering from './stage-layering'; diff --git a/packages/vm/src/engine/sequencer.js b/packages/vm/src/engine/sequencer.js index 7a9dd086..2d6f6430 100644 --- a/packages/vm/src/engine/sequencer.js +++ b/packages/vm/src/engine/sequencer.js @@ -1,5 +1,5 @@ import Timer from '../util/timer'; -import Thread from './thread.js'; +import Thread from './thread'; import execute from './execute.js'; /** diff --git a/packages/vm/src/engine/thread.js b/packages/vm/src/engine/thread.ts similarity index 52% rename from packages/vm/src/engine/thread.js rename to packages/vm/src/engine/thread.ts index 6f694737..6860c1b6 100644 --- a/packages/vm/src/engine/thread.js +++ b/packages/vm/src/engine/thread.ts @@ -1,90 +1,62 @@ -/** - * Recycle bin for empty stackFrame objects - * @type Array<_StackFrame> - */ -const _stackFrameFreeList = []; +import type Target from './target'; +import type Blocks from './blocks'; +import type Timer from '../util/timer'; /** - * @typedef {import('./target')} Target - * @typedef {import('./blocks')} Blocks - * @typedef {import('../util/timer')} Timer + * Recycle bin for empty stackFrame objects */ +const _stackFrameFreeList: _StackFrame[] = []; /** * A frame used for each level of the stack. A general purpose * place to store a bunch of execution context and parameters - * @param {boolean} warpMode Whether this level of the stack is warping - * @class * @private */ class _StackFrame { /** - * @param {boolean} warpMode Whether this level is in warp mode. + * Whether this level of the stack is a loop. */ - constructor (warpMode) { - /** - * Whether this level of the stack is a loop. - * @type {boolean} - */ - this.isLoop = false; - - /** - * Whether this level is in warp mode. Is set by some legacy blocks and - * "turbo mode" - * @type {boolean} - */ - this.warpMode = warpMode; - - /** - * Reported value from just executed block. - * @type {any} - */ - this.justReported = null; - - /** - * The active block that is waiting on a promise. - * @type {string} - */ - this.reporting = ''; - - /** - * Persists reported inputs during async block. - * @type {?object} - */ - this.reported = null; - - /** - * Whether is waiting a custom reporter. - * @type {boolean} - */ - this.waitingReporter = false; - - /** - * Procedure parameters. - * @type {?object} - */ - this.params = null; - - /** - * A context passed to block implementations. - * @type {?object} - */ - this.executionContext = null; + isLoop = false; + /** + * Reported value from just executed block. + */ + justReported: unknown = null; + /** + * The active block that is waiting on a promise. + */ + reporting = ''; + /** + * Persists reported inputs during async block. + */ + reported: Record | null = null; + /** + * Whether is waiting a custom reporter. + */ + waitingReporter = false; + /** + * Procedure parameters. + */ + params: Record | null = null; + /** + * A context passed to block implementations. + */ + executionContext: unknown = null; + /** + * The target of blocks that this thread will execute. + */ + target: Target | null = null; - /** - * The target of blocks that this thread will execute. - * @type {?Target} - */ - this.target = null; - } + /** + * @param warpMode Whether this level is in warp mode. Is set by some legacy blocks and + * "turbo mode" + */ + constructor (public warpMode: boolean) {} /** * Reset all properties of the frame to pristine null and false states. * Used to recycle. - * @returns {_StackFrame} this */ - reset () { - + reset (): this { this.isLoop = false; this.warpMode = false; this.justReported = null; @@ -98,10 +70,9 @@ class _StackFrame { /** * Reuse an active stack frame in the stack. - * @param {?boolean} warpMode defaults to current warpMode - * @returns {_StackFrame} this + * @param warpMode defaults to current warpMode */ - reuse (warpMode = this.warpMode) { + reuse (warpMode: boolean = this.warpMode): this { this.reset(); this.warpMode = Boolean(warpMode); return this; @@ -109,10 +80,9 @@ class _StackFrame { /** * Create or recycle a stack frame object. - * @param {boolean} warpMode Enable warpMode on this frame. - * @returns {_StackFrame} The clean stack frame with correct warpMode setting. + * @param warpMode Enable warpMode on this frame. */ - static create (warpMode) { + static create (warpMode: boolean): _StackFrame { const stackFrame = _stackFrameFreeList.pop(); if (typeof stackFrame !== 'undefined') { stackFrame.warpMode = Boolean(warpMode); @@ -123,157 +93,133 @@ class _StackFrame { /** * Put a stack frame object into the recycle bin for reuse. - * @param {_StackFrame} stackFrame The frame to reset and recycle. + * @param stackFrame The frame to reset and recycle. */ - static release (stackFrame) { + static release (stackFrame: _StackFrame): void { if (typeof stackFrame !== 'undefined') { _stackFrameFreeList.push(stackFrame.reset()); } } } +const enum ThreadStatus { + RUNNING = 0, + PROMISE_WAIT = 1, + YIELD = 2, + YIELD_TICK = 3, + DONE = 4 +} + /** * A thread is a running stack context and all the metadata needed. */ class Thread { /** - * @param {?string} firstBlock First block to execute in the thread. + * ID of top block of the thread */ - constructor (firstBlock) { - /** - * ID of top block of the thread - * @type {!string} - */ - this.topBlock = firstBlock; - - /** - * Stack for the thread. When the sequencer enters a control structure, - * the block is pushed onto the stack so we know where to exit. - * @type {Array.} - */ - this.stack = []; - - /** - * Stack frames for the thread. Store metadata for the executing blocks. - * @type {Array.<_StackFrame>} - */ - this.stackFrames = []; - - /** - * Status of the thread, one of three states (below) - * @type {number} - */ - this.status = 0; /* Thread.STATUS_RUNNING */ - - /** - * Whether the thread is killed in the middle of execution. - * @type {boolean} - */ - this.isKilled = false; - - /** - * Target of this thread. - * @type {?Target} - */ - this.target = null; - - /** - * The Blocks this thread will execute. - * @type {?Blocks} - */ - this.blockContainer = null; - - /** - * Whether the thread requests its script to glow during this frame. - * @type {boolean} - */ - this.requestScriptGlowInFrame = false; - - /** - * Which block ID should glow during this frame, if any. - * @type {?string} - */ - this.blockGlowInFrame = null; - - /** - * A timer for when the thread enters warp mode. - * Substitutes the sequencer's count toward WORK_TIME on a per-thread basis. - * @type {?Timer} - */ - this.warpTimer = null; - - /** - * true if the script was activated by clicking on the stack - * @type {boolean} - */ - this.stackClick = false; - - /** - * true if the script should update a monitor value - * @type {boolean} - */ - this.updateMonitor = false; - - this.justReported = null; + topBlock: string | null; + /** + * Stack for the thread. When the sequencer enters a control structure, + * the block is pushed onto the stack so we know where to exit. + */ + stack: string[] = []; + /** + * Stack frames for the thread. Store metadata for the executing blocks. + */ + stackFrames: _StackFrame[] = []; + /** + * Status of the thread, one of three states (below) + */ + status: ThreadStatus = ThreadStatus.RUNNING; + /** + * Whether the thread is killed in the middle of execution. + */ + isKilled = false; + /** + * Target of this thread. + */ + target: Target | null = null; + /** + * The Blocks this thread will execute. + */ + blockContainer: Blocks | null = null; + /** + * Whether the thread requests its script to glow during this frame. + */ + requestScriptGlowInFrame: boolean = false; + /** + * Which block ID should glow during this frame, if any. + */ + blockGlowInFrame: string | null = null; + /** + * A timer for when the thread enters warp mode. + * Substitutes the sequencer's count toward WORK_TIME on a per-thread basis. + */ + warpTimer: Timer | null = null; + justReported: unknown = null; + /** + * true if the script was activated by clicking on the stack + */ + stackClick = false; + /** + * true if the script should update a monitor value + */ + updateMonitor = false; + /** + * An option to forcely mention that a control flow has happened. + */ + controlFlowed = false; - /** - * An option to forcely mention that a control flow has happened. - * @type {boolean} - */ - this.controlFlowed = false; + constructor (firstBlock: string | null) { + this.topBlock = firstBlock; } /** * Thread status for initialized or running thread. * This is the default state for a thread - execution should run normally, * stepping from block to block. - * @returns {number} */ static get STATUS_RUNNING () { - return 0; + return ThreadStatus.RUNNING; } /** * Threads are in this state when a primitive is waiting on a promise; * execution is paused until the promise changes thread status. - * @returns {number} */ static get STATUS_PROMISE_WAIT () { - return 1; + return ThreadStatus.PROMISE_WAIT; } /** * Thread status for yield. - * @returns {number} */ static get STATUS_YIELD () { - return 2; + return ThreadStatus.YIELD; } /** * Thread status for a single-tick yield. This will be cleared when the * thread is resumed. - * @returns {number} */ static get STATUS_YIELD_TICK () { - return 3; + return ThreadStatus.YIELD_TICK; } /** * Thread status for a finished/done thread. * Thread is in this state when there are no more blocks to execute. - * @returns {number} */ static get STATUS_DONE () { - return 4; + return ThreadStatus.DONE; } /** * Push stack and update stack frames appropriately. - * @param {string} blockId Block ID to push to stack. - * @param {?Target} target New target context. + * @param blockId Block ID to push to stack. + * @param target New target context. */ - pushStack (blockId, target) { + pushStack (blockId: string, target?: Target): void { this.stack.push(blockId); // Push an empty stack frame, if we need one. // Might not, if we just popped the stack. @@ -287,7 +233,7 @@ class Thread { } else { stackFrame.target = this.target; } - this.blockContainer = stackFrame.target.blocks; + this.blockContainer = stackFrame.target!.blocks; this.stackFrames.push(stackFrame); } } @@ -295,22 +241,22 @@ class Thread { /** * Reset the stack frame for use by the next block. * (avoids popping and re-pushing a new stack frame - keeps the warpmode the same - * @param {string} blockId Block ID to push to stack. + * @param blockId Block ID to push to stack. */ - reuseStackForNextBlock (blockId) { + reuseStackForNextBlock (blockId: string): void { this.stack[this.stack.length - 1] = blockId; this.stackFrames[this.stackFrames.length - 1].reuse(); } /** * Pop last block on the stack and its stack frame. - * @returns {string} Block ID popped from the stack. + * @returns Block ID popped from the stack. */ - popStack () { - _StackFrame.release(this.stackFrames.pop()); + popStack (): string | undefined { + _StackFrame.release(this.stackFrames.pop()!); const stackFrame = this.peekStackFrame(); if (stackFrame) { - this.blockContainer = stackFrame.target.blocks; + this.blockContainer = stackFrame.target!.blocks; } return this.stack.pop(); } @@ -318,14 +264,14 @@ class Thread { /** * Pop back down the stack frame until we hit a procedure call or the stack frame is emptied */ - stopThisScript () { + stopThisScript (): void { let blockID = this.peekStack(); while (blockID !== null) { - const block = this.blockContainer.getBlock(blockID); - if (this.peekStackFrame().waitingReporter) { + const block = this.blockContainer!.getBlock(blockID); + if (this.peekStackFrame()!.waitingReporter) { // cc - check if a reporter procedure is on the stack break; - } else if (typeof block !== 'undefined' && block.opcode === 'procedures_call') { + } else if (block && block.opcode === 'procedures_call') { // cc - prevent call command procedure repeatedly this.goToNextBlock(); break; @@ -345,43 +291,42 @@ class Thread { /** * Get top stack item. - * @returns {?string} Block ID on top of stack. + * @returns Block ID on top of stack. */ - peekStack () { + peekStack (): string | null { return this.stack.length > 0 ? this.stack[this.stack.length - 1] : null; } - /** * Get top stack frame. - * @returns {?object} Last stack frame stored on this thread. + * @returns Last stack frame stored on this thread. */ - peekStackFrame () { + peekStackFrame (): _StackFrame | null { return this.stackFrames.length > 0 ? this.stackFrames[this.stackFrames.length - 1] : null; } /** * Get stack frame above the current top. - * @returns {?object} Second to last stack frame stored on this thread. + * @returns Second to last stack frame stored on this thread. */ - peekParentStackFrame () { + peekParentStackFrame (): _StackFrame | null { return this.stackFrames.length > 1 ? this.stackFrames[this.stackFrames.length - 2] : null; } /** * Push a reported value to the parent of the current stack frame. - * @param {*} value Reported value to push. + * @param value Reported value to push. */ - pushReportedValue (value) { + pushReportedValue (value: unknown): void { this.justReported = typeof value === 'undefined' ? null : value; } /** * Initialize procedure parameters on this stack frame. */ - initParams () { + initParams (): void { const stackFrame = this.peekStackFrame(); - if (stackFrame.params === null) { + if (stackFrame && stackFrame.params === null) { stackFrame.params = {}; } } @@ -389,20 +334,20 @@ class Thread { /** * Add a parameter to the stack frame. * Use when calling a procedure with parameter values. - * @param {!string} paramName Name of parameter. - * @param {*} value Value to set for parameter. + * @param paramName Name of parameter. + * @param value Value to set for parameter. */ - pushParam (paramName, value) { - const stackFrame = this.peekStackFrame(); - stackFrame.params[paramName] = value; + pushParam (paramName: string, value: unknown): void { + const stackFrame = this.peekStackFrame()!; + stackFrame.params![paramName] = value; } /** * Get a parameter at the lowest possible level of the stack. - * @param {!string} paramName Name of parameter. - * @returns {*} value Value for parameter. + * @param paramName Name of parameter. + * @returns value Value for parameter. */ - getParam (paramName) { + getParam (paramName: string): unknown { // cc - ignore the top stack's param, it's not used by current stack for (let i = this.stackFrames.length - 2; i >= 0; i--) { const frame = this.stackFrames[i]; @@ -419,30 +364,29 @@ class Thread { /** * Whether the current execution of a thread is at the top of the stack. - * @returns {boolean} True if execution is at top of the stack. + * @returns True if execution is at top of the stack. */ - atStackTop () { + atStackTop (): boolean { return this.peekStack() === this.topBlock; } - /** * Switch the thread to the next block at the current level of the stack. * For example, this is used in a standard sequence of blocks, * where execution proceeds from one block to the next. */ - goToNextBlock () { - const nextBlockId = this.blockContainer.getNextBlock(this.peekStack()); + goToNextBlock (): void { + const nextBlockId = this.blockContainer!.getNextBlock(this.peekStack()!) as string; this.reuseStackForNextBlock(nextBlockId); } /** * Attempt to determine whether a procedure call is recursive, * by examining the stack. - * @param {!string} procedureCode Procedure code of procedure being called. - * @returns {boolean} True if the call appears recursive. + * @param procedureCode Procedure code of procedure being called. + * @returns True if the call appears recursive. */ - isRecursiveCall (procedureCode) { + isRecursiveCall (procedureCode: string): boolean { let callCount = 5; // Max number of enclosing procedure calls to examine. const sp = this.stack.length - 1; let flag = false; @@ -456,10 +400,10 @@ class Thread { } else { flag = false; } - const block = this.stackFrames[i].target.blocks.getBlock(blockId); + const block = this.stackFrames[i].target!.blocks.getBlock(blockId); // cc - block maybe not exists when triggered in toolbox. if (block && block.opcode === 'procedures_call' && - block.mutation.proccode === procedureCode) { + block.mutation?.proccode === procedureCode) { return true; } if (--callCount < 0) return false; diff --git a/packages/vm/test/integration/hat-threads-run-every-frame.js b/packages/vm/test/integration/hat-threads-run-every-frame.js index d3d4dbc0..08da93a7 100644 --- a/packages/vm/test/integration/hat-threads-run-every-frame.js +++ b/packages/vm/test/integration/hat-threads-run-every-frame.js @@ -3,7 +3,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index.js'; -import Thread from '../../src/engine/thread.js'; +import Thread from '../../src/engine/thread'; import Runtime from '../../src/engine/runtime.js'; import execute from '../../src/engine/execute.js'; diff --git a/packages/vm/test/integration/monitor-threads-run-every-frame.js b/packages/vm/test/integration/monitor-threads-run-every-frame.js index ecab73b4..c0da2ccf 100644 --- a/packages/vm/test/integration/monitor-threads-run-every-frame.js +++ b/packages/vm/test/integration/monitor-threads-run-every-frame.js @@ -3,7 +3,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index.js'; -import Thread from '../../src/engine/thread.js'; +import Thread from '../../src/engine/thread'; import Runtime from '../../src/engine/runtime.js'; const projectUri = path.resolve(__dirname, '../fixtures/timer-monitor.sb3'); diff --git a/packages/vm/test/unit/blocks_event.js b/packages/vm/test/unit/blocks_event.js index 83b02224..b4bec981 100644 --- a/packages/vm/test/unit/blocks_event.js +++ b/packages/vm/test/unit/blocks_event.js @@ -4,7 +4,7 @@ import BlockUtility from '../../src/engine/block-utility.js'; import Event from '../../src/blocks/scratch3_event'; import Runtime from '../../src/engine/runtime.js'; import Target from '../../src/engine/target.js'; -import Thread from '../../src/engine/thread.js'; +import Thread from '../../src/engine/thread'; import Variable from '../../src/engine/variable'; test('#760 - broadcastAndWait', t => { diff --git a/packages/vm/test/unit/engine_sequencer.js b/packages/vm/test/unit/engine_sequencer.js index c581849d..73910fcf 100644 --- a/packages/vm/test/unit/engine_sequencer.js +++ b/packages/vm/test/unit/engine_sequencer.js @@ -1,7 +1,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import Sequencer from '../../src/engine/sequencer.js'; import Runtime from '../../src/engine/runtime.js'; -import Thread from '../../src/engine/thread.js'; +import Thread from '../../src/engine/thread'; import RenderedTarget from '../../src/sprites/rendered-target.js'; import Sprite from '../../src/sprites/sprite'; diff --git a/packages/vm/test/unit/engine_thread.js b/packages/vm/test/unit/engine_thread.js index a3dfac4f..8ae18e14 100644 --- a/packages/vm/test/unit/engine_thread.js +++ b/packages/vm/test/unit/engine_thread.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import Thread from '../../src/engine/thread.js'; +import Thread from '../../src/engine/thread'; import RenderedTarget from '../../src/sprites/rendered-target.js'; import Sprite from '../../src/sprites/sprite'; import Runtime from '../../src/engine/runtime.js'; From b799478ae9245b542c01986f4bb837f737e9dd98 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Mon, 11 May 2026 14:20:50 +0800 Subject: [PATCH 22/30] :wrench: chore(block): migrate block-utility Signed-off-by: SimonShiki --- packages/vm/src/blocks/scratch3_control.ts | 16 +- packages/vm/src/blocks/scratch3_event.ts | 15 +- packages/vm/src/blocks/scratch3_looks.ts | 11 +- packages/vm/src/blocks/scratch3_motion.ts | 12 +- packages/vm/src/engine/block-utility.js | 265 ---------------- packages/vm/src/engine/block-utility.ts | 293 ++++++++++++++++++ .../vm/src/engine/blocks-execute-cache.js | 52 ---- .../vm/src/engine/blocks-execute-cache.ts | 60 ++++ ...ntime-cache.js => blocks-runtime-cache.ts} | 70 ++--- packages/vm/src/engine/blocks.js | 5 +- packages/vm/src/engine/execute.js | 4 +- packages/vm/src/engine/runtime.js | 3 +- packages/vm/src/engine/sequencer.js | 2 +- packages/vm/src/engine/thread.ts | 8 +- packages/vm/src/virtual-machine.js | 2 +- 15 files changed, 436 insertions(+), 382 deletions(-) delete mode 100644 packages/vm/src/engine/block-utility.js create mode 100644 packages/vm/src/engine/block-utility.ts delete mode 100644 packages/vm/src/engine/blocks-execute-cache.js create mode 100644 packages/vm/src/engine/blocks-execute-cache.ts rename packages/vm/src/engine/{blocks-runtime-cache.js => blocks-runtime-cache.ts} (55%) diff --git a/packages/vm/src/blocks/scratch3_control.ts b/packages/vm/src/blocks/scratch3_control.ts index b4e0e423..def18124 100644 --- a/packages/vm/src/blocks/scratch3_control.ts +++ b/packages/vm/src/blocks/scratch3_control.ts @@ -3,6 +3,12 @@ import type {BlockArgs, CategoryPrototype} from './category_prototype'; import type Runtime from '../engine/runtime'; import type RenderedTarget from '../sprites/rendered-target'; import type BlockUtility from '../engine/block-utility'; +import type {BaseExecutionContext} from '../engine/block-utility'; + +interface ControlExecutionContext extends BaseExecutionContext { + loopCounter?: number; + index?: number; +} class Scratch3ControlBlocks implements CategoryPrototype { /** @@ -61,9 +67,9 @@ class Scratch3ControlBlocks implements CategoryPrototype { // When the branch finishes, `repeat` will be executed again and // the second branch will be taken, yielding for the rest of the frame. // Decrease counter - util.stackFrame.loopCounter--; + (util.stackFrame as ControlExecutionContext).loopCounter!--; // If we still have some left, start the branch. - if (util.stackFrame.loopCounter >= 0) { + if ((util.stackFrame as ControlExecutionContext).loopCounter! >= 0) { util.startBranch(1, true); } } @@ -92,9 +98,9 @@ class Scratch3ControlBlocks implements CategoryPrototype { util.stackFrame.index = 0; } - if (util.stackFrame.index < Number(args.VALUE)) { - util.stackFrame.index++; - variable.value = util.stackFrame.index; + if ((util.stackFrame as ControlExecutionContext).index! < Number(args.VALUE)) { + (util.stackFrame as ControlExecutionContext).index!++; + variable.value = (util.stackFrame as ControlExecutionContext).index; util.startBranch(1, true); } } diff --git a/packages/vm/src/blocks/scratch3_event.ts b/packages/vm/src/blocks/scratch3_event.ts index bfa99a95..ae43834f 100644 --- a/packages/vm/src/blocks/scratch3_event.ts +++ b/packages/vm/src/blocks/scratch3_event.ts @@ -3,6 +3,13 @@ import type {BlockArgs, CategoryPrototype} from './category_prototype'; import type Runtime from '../engine/runtime'; import type BlockUtility from '../engine/block-utility'; import type Thread from '../engine/thread'; +import type {BaseExecutionContext} from '../engine/block-utility'; +import type Variable from '../engine/variable'; + +interface EventExecutionContext extends BaseExecutionContext { + broadcastVar?: Variable; + startedThreads?: Thread[]; +} class Scratch3EventBlocks implements CategoryPrototype { constructor ( @@ -82,7 +89,7 @@ class Scratch3EventBlocks implements CategoryPrototype { } broadcast (args: BlockArgs, util: BlockUtility) { - const broadcastVar = util.runtime.getTargetForStage()?.lookupBroadcastMsg( + const broadcastVar = util.runtime!.getTargetForStage()?.lookupBroadcastMsg( args.BROADCAST_OPTION.id, args.BROADCAST_OPTION.name); if (broadcastVar) { const broadcastOption = broadcastVar.name; @@ -94,11 +101,11 @@ class Scratch3EventBlocks implements CategoryPrototype { broadcastAndWait (args: BlockArgs, util: BlockUtility) { if (!util.stackFrame.broadcastVar) { - util.stackFrame.broadcastVar = util.runtime.getTargetForStage()?.lookupBroadcastMsg( + util.stackFrame.broadcastVar = util.runtime!.getTargetForStage()?.lookupBroadcastMsg( args.BROADCAST_OPTION.id, args.BROADCAST_OPTION.name); } if (util.stackFrame.broadcastVar) { - const broadcastOption = util.stackFrame.broadcastVar.name; + const broadcastOption = (util.stackFrame as EventExecutionContext).broadcastVar!.name; // Have we run before, starting threads? if (!util.stackFrame.startedThreads) { // No - start hats for this broadcast. @@ -107,7 +114,7 @@ class Scratch3EventBlocks implements CategoryPrototype { BROADCAST_OPTION: broadcastOption } ); - if (util.stackFrame.startedThreads.length === 0) { + if ((util.stackFrame as EventExecutionContext).startedThreads?.length === 0) { // Nothing was started. return; } diff --git a/packages/vm/src/blocks/scratch3_looks.ts b/packages/vm/src/blocks/scratch3_looks.ts index dc65b7b5..7851a9b4 100644 --- a/packages/vm/src/blocks/scratch3_looks.ts +++ b/packages/vm/src/blocks/scratch3_looks.ts @@ -11,6 +11,7 @@ import type BlockUtility from '../engine/block-utility'; import type Target from '../engine/target.js'; import type {MonitorBlockInfo} from '../engine/runtime'; import type Thread from '../engine/thread'; +import type {BaseExecutionContext} from '../engine/block-utility'; interface Bounds { left: number; @@ -19,6 +20,10 @@ interface Bounds { bottom: number; } +interface LooksExecutionContext extends BaseExecutionContext { + startedThreads?: Thread[]; +} + /** * The bubble state associated with a particular target. */ @@ -504,7 +509,7 @@ class Scratch3LooksBlocks implements CategoryPrototype { args.BACKDROP ) ); - if (util.stackFrame.startedThreads.length === 0) { + if ((util.stackFrame as LooksExecutionContext).startedThreads?.length === 0) { // Nothing was started. return; } @@ -516,14 +521,14 @@ class Scratch3LooksBlocks implements CategoryPrototype { // runtime.threads. Threads that have run all their blocks, or are // marked done but still in runtime.threads are still considered to // be waiting. - const waiting = util.stackFrame.startedThreads + const waiting = (util.stackFrame as LooksExecutionContext).startedThreads! .some((thread: Thread) => instance.runtime.threads.indexOf(thread) !== -1); if (waiting) { // If all threads are waiting for the next tick or later yield // for a tick as well. Otherwise yield until the next loop of // the threads. if ( - util.stackFrame.startedThreads + (util.stackFrame as LooksExecutionContext).startedThreads! .every((thread: Thread) => instance.runtime.isWaitingThread(thread)) ) { util.yieldTick(); diff --git a/packages/vm/src/blocks/scratch3_motion.ts b/packages/vm/src/blocks/scratch3_motion.ts index 0f00fa7c..ccb04974 100644 --- a/packages/vm/src/blocks/scratch3_motion.ts +++ b/packages/vm/src/blocks/scratch3_motion.ts @@ -154,21 +154,21 @@ class Scratch3MotionBlocks implements CategoryPrototype { } glide (args: BlockArgs, util: BlockUtility) { - if (util.stackFrame.timer) { + if (util.stackTimerAvailable(util.stackFrame)) { const timeElapsed = util.stackFrame.timer.timeElapsed(); if (timeElapsed < util.stackFrame.duration * 1000) { // In progress: move to intermediate position. const frac = timeElapsed / (util.stackFrame.duration * 1000); - const dx = frac * (util.stackFrame.endX - util.stackFrame.startX); - const dy = frac * (util.stackFrame.endY - util.stackFrame.startY); + const dx = frac * (util.stackFrame.endX! - util.stackFrame.startX!); + const dy = frac * (util.stackFrame.endY! - util.stackFrame.startY!); util.target.setXY( - util.stackFrame.startX + dx, - util.stackFrame.startY + dy + util.stackFrame.startX! + dx, + util.stackFrame.startY! + dy ); util.yield(); } else { // Finished: move to final position. - util.target.setXY(util.stackFrame.endX, util.stackFrame.endY); + util.target.setXY(util.stackFrame.endX!, util.stackFrame.endY!); } } else { // First time: save data for future use. diff --git a/packages/vm/src/engine/block-utility.js b/packages/vm/src/engine/block-utility.js deleted file mode 100644 index 08dbf792..00000000 --- a/packages/vm/src/engine/block-utility.js +++ /dev/null @@ -1,265 +0,0 @@ -import Thread from './thread'; -import Timer from '../util/timer'; - -/** - * @fileoverview - * Interface provided to block primitive functions for interacting with the - * runtime, thread, target, and convenient methods. - */ - -/** - * @typedef {import('../sprites/rendered-target').default} RenderedTarget - * @typedef {import('./sequencer').default} Sequencer - * @typedef {import('./runtime').default} Runtime - * @typedef {{now: () => number | undefined}} NowObj - */ - -class BlockUtility { - constructor (sequencer = null, thread = null) { - /** - * A sequencer block primitives use to branch or start procedures with - * @type {?Sequencer} - */ - this.sequencer = sequencer; - - /** - * The block primitives thread with the block's target, stackFrame and - * modifiable status. - * @type {?Thread} - */ - this.thread = thread; - - /** - * @type {NowObj} - */ - this._nowObj = { - now: () => this.sequencer.runtime.currentMSecs - }; - - /** - * The opcode to skip to, which is used to implement short-circuit evaluation. - * @type {?string | ?boolean} - */ - this.skipToOpcode = null; - } - - /** - * The target the primitive is working on. - * @type {RenderedTarget} - */ - get target () { - return this.thread.target; - } - - /** - * The runtime the block primitive is running in. - * @type {Runtime} - */ - get runtime () { - return this.sequencer?.runtime; - } - - /** - * Use the runtime's currentMSecs value as a timestamp value for now - * This is useful in some cases where we need compatibility with Scratch 2 - * @type {NowObj?} - */ - get nowObj () { - if (this.runtime) { - return this._nowObj; - } - return null; - } - - /** - * The stack frame used by loop and other blocks to track internal state. - * @type {Record} - */ - get stackFrame () { - const frame = this.thread.peekStackFrame(); - if (frame.executionContext === null) { - frame.executionContext = {}; - } - return frame.executionContext; - } - - /** - * Check the stack timer and return a boolean based on whether it has finished or not. - * @returns {boolean} - true if the stack timer has finished. - */ - stackTimerFinished () { - const timeElapsed = this.stackFrame.timer.timeElapsed(); - if (timeElapsed < this.stackFrame.duration) { - return false; - } - return true; - } - - /** - * Check if the stack timer needs initialization. - * @returns {boolean} - true if the stack timer needs to be initialized. - */ - stackTimerNeedsInit () { - return !this.stackFrame.timer; - } - - /** - * Create and start a stack timer - * @param {number} duration - a duration in milliseconds to set the timer for. - */ - startStackTimer (duration) { - if (this.nowObj) { - this.stackFrame.timer = new Timer(this.nowObj); - } else { - this.stackFrame.timer = new Timer(); - } - this.stackFrame.timer.start(); - this.stackFrame.duration = duration; - } - - /** - * Set the thread to yield. - */ - yield () { - this.thread.status = Thread.STATUS_YIELD; - } - - /** - * Set the thread to yield until the next tick of the runtime. - */ - yieldTick () { - this.thread.status = Thread.STATUS_YIELD_TICK; - } - - /** - * Start a branch in the current block. - * @param {number} branchNum Which branch to step to (i.e., 1, 2). - * @param {boolean} isLoop Whether this block is a loop. - */ - startBranch (branchNum, isLoop) { - this.sequencer.stepToBranch(this.thread, branchNum, isLoop); - } - - /** - * Stop all threads. - */ - stopAll () { - this.sequencer.runtime.stopAll(); - } - - /** - * Stop threads other on this target other than the thread holding the - * executed block. - */ - stopOtherTargetThreads () { - this.sequencer.runtime.stopForTarget(this.thread.target, this.thread); - } - - /** - * Stop this thread. - */ - stopThisScript () { - this.thread.stopThisScript(); - } - - /** - * Start a specified procedure on this thread. - * @param {string} procedureCode Procedure code for procedure to start. - */ - startProcedure (procedureCode) { - this.sequencer.stepToProcedure(this.thread, procedureCode); - } - - /** - * Get names and ids of parameters for the given procedure. - * @param {string} procedureCode Procedure code for procedure to query. - * @returns {Array.} List of param names for a procedure. - */ - getProcedureParamNamesAndIds (procedureCode) { - const result = this.thread.blockContainer.getProcedureParamNamesAndIds(procedureCode); - if (result) { - return result; - } - return this.sequencer.runtime.getProcedureParamNamesAndIds(procedureCode); - } - - /** - * Get names, ids, and defaults of parameters for the given procedure. - * @param {string} procedureCode Procedure code for procedure to query. - * @returns {Array.} List of param names for a procedure. - */ - getProcedureParamNamesIdsAndDefaults (procedureCode) { - const result = this.thread.blockContainer.getProcedureParamNamesIdsAndDefaults(procedureCode); - if (result) { - return result; - } - return this.sequencer.runtime.getProcedureParamNamesIdsAndDefaults(procedureCode); - } - - /** - * Initialize procedure parameters in the thread before pushing parameters. - */ - initParams () { - this.thread.initParams(); - } - - /** - * Store a procedure parameter value by its name. - * @param {string} paramName The procedure's parameter name. - * @param {*} paramValue The procedure's parameter value. - */ - pushParam (paramName, paramValue) { - this.thread.pushParam(paramName, paramValue); - } - - /** - * Retrieve the stored parameter value for a given parameter name. - * @param {string} paramName The procedure's parameter name. - * @returns {*} The parameter's current stored value. - */ - getParam (paramName) { - return this.thread.getParam(paramName); - } - - /** - * Start all relevant hats. - * @param {!string} requestedHat Opcode of hats to start. - * @param {object=} optMatchFields Optionally, fields to match on the hat. - * @param {Target=} optTarget Optionally, a target to restrict to. - * @returns {Array.} List of threads started by this function. - */ - startHats (requestedHat, optMatchFields, optTarget) { - // Store thread and sequencer to ensure we can return to the calling block's context. - // startHats may execute further blocks and dirty the BlockUtility's execution context - // and confuse the calling block when we return to it. - const callerThread = this.thread; - const callerSequencer = this.sequencer; - const result = this.sequencer.runtime.startHats(requestedHat, optMatchFields, optTarget); - - // Restore thread and sequencer to prior values before we return to the calling block. - this.thread = callerThread; - this.sequencer = callerSequencer; - - return result; - } - - /** - * Query a named IO device. - * @param {string} device The name of like the device, like keyboard. - * @param {string} func The name of the device's function to query. - * @param {Array.<*>} [args] Arguments to pass to the device's function. - * @returns {*} The expected output for the device's function. - */ - ioQuery (device, func, args) { - // Find the I/O device and execute the query/function call. - if ( - this.sequencer.runtime.ioDevices[device] && - this.sequencer.runtime.ioDevices[device][func]) { - const devObject = this.sequencer.runtime.ioDevices[device]; - // eslint-disable-next-line prefer-spread - return devObject[func].apply(devObject, args); - } - } -} - -export default BlockUtility; diff --git a/packages/vm/src/engine/block-utility.ts b/packages/vm/src/engine/block-utility.ts new file mode 100644 index 00000000..8ddfc4e6 --- /dev/null +++ b/packages/vm/src/engine/block-utility.ts @@ -0,0 +1,293 @@ +import Thread from './thread'; +import Timer from '../util/timer'; +import type Sequencer from './sequencer'; +import type Runtime from './runtime'; +import type RenderedTarget from '../sprites/rendered-target'; + +export interface BaseExecutionContext { + [key: string]: unknown; +} + +interface StackTimerContext { + timer: Timer; + duration: number; + endX?: number; + endY?: number; + startX?: number; + startY?: number; +} + +type AvailableIODevices = keyof Runtime['ioDevices']; +type DeviceFunc = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [K in keyof Runtime['ioDevices'][T]]: Runtime['ioDevices'][T][K] extends (...args: any[]) => any ? K : never; +}[keyof Runtime['ioDevices'][T]]; + +type ExecutionContext = BaseExecutionContext & Partial; + +/** + * @fileoverview + * Interface provided to block primitive functions for interacting with the + * runtime, thread, target, and convenient methods. + */ + +type NowObj = { now: () => number }; + +class BlockUtility { + /** + * A sequencer block primitives use to branch or start procedures with + */ + sequencer: Sequencer | null; + + /** + * The block primitives thread with the block's target, stackFrame and + * modifiable status. + */ + thread: Thread | null; + + _nowObj: NowObj; + + /** + * The opcode to skip to, which is used to implement short-circuit evaluation. + */ + skipToOpcode: string | boolean | null = null; + + constructor (sequencer: Sequencer | null = null, thread: Thread | null = null) { + this.sequencer = sequencer; + + this.thread = thread; + + this._nowObj = { + now: () => this.sequencer!.runtime.currentMSecs + }; + } + + /** + * The target the primitive is working on. + */ + get target (): RenderedTarget { + return this.thread!.target!; + } + + /** + * The runtime the block primitive is running in. + */ + get runtime (): Runtime | undefined { + return this.sequencer?.runtime; + } + + /** + * Use the runtime's currentMSecs value as a timestamp value for now + * This is useful in some cases where we need compatibility with Scratch 2 + */ + get nowObj (): NowObj | null { + if (this.runtime) { + return this._nowObj; + } + return null; + } + + /** + * The stack frame used by loop and other blocks to track internal state. + */ + get stackFrame (): ExecutionContext { + const frame = this.thread!.peekStackFrame(); + if (frame!.executionContext === null) { + frame!.executionContext = {}; + } + return frame!.executionContext as ExecutionContext; + } + + /** + * Check the stack timer and return a boolean based on whether it has finished or not. + * @returns true if the stack timer has finished. + */ + stackTimerFinished (): boolean { + if (!this.stackTimerAvailable(this.stackFrame)) { + throw new Error('No stack timer found when checking if stack timer is finished'); + } + const timeElapsed = this.stackFrame.timer.timeElapsed(); + if (timeElapsed < this.stackFrame.duration) { + return false; + } + return true; + } + + stackTimerAvailable (ctx: ExecutionContext): ctx is BaseExecutionContext & StackTimerContext { + return !!ctx.timer; + } + + /** + * Check if the stack timer needs initialization. + * @returns true if the stack timer needs to be initialized. + */ + stackTimerNeedsInit (): boolean { + return !this.stackFrame.timer; + } + + /** + * Create and start a stack timer + * @param duration - a duration in milliseconds to set the timer for. + */ + startStackTimer (duration: number): void { + if (this.nowObj) { + this.stackFrame.timer = new Timer(this.nowObj); + } else { + this.stackFrame.timer = new Timer(); + } + this.stackFrame.timer.start(); + this.stackFrame.duration = duration; + } + + /** + * Set the thread to yield. + */ + yield (): void { + this.thread!.status = Thread.STATUS_YIELD; + } + + /** + * Set the thread to yield until the next tick of the runtime. + */ + yieldTick (): void { + this.thread!.status = Thread.STATUS_YIELD_TICK; + } + + /** + * Start a branch in the current block. + * @param branchNum Which branch to step to (i.e., 1, 2). + * @param isLoop Whether this block is a loop. + */ + startBranch (branchNum: number, isLoop: boolean): void { + this.sequencer!.stepToBranch(this.thread!, branchNum, isLoop); + } + + /** + * Stop all threads. + */ + stopAll (): void { + this.sequencer!.runtime.stopAll(); + } + + /** + * Stop threads other on this target other than the thread holding the + * executed block. + */ + stopOtherTargetThreads (): void { + this.sequencer!.runtime.stopForTarget(this.thread!.target!, this.thread!); + } + + /** + * Stop this thread. + */ + stopThisScript (): void { + this.thread!.stopThisScript(); + } + + /** + * Start a specified procedure on this thread. + * @param procedureCode Procedure code for procedure to start. + */ + startProcedure (procedureCode: string): void { + this.sequencer!.stepToProcedure(this.thread!, procedureCode); + } + + /** + * Get names and ids of parameters for the given procedure. + * @param procedureCode Procedure code for procedure to query. + * @returns List of param names for a procedure. + */ + getProcedureParamNamesAndIds (procedureCode: string) { + const result = this.thread!.blockContainer!.getProcedureParamNamesAndIds(procedureCode); + if (result) { + return result; + } + return this.sequencer!.runtime.getProcedureParamNamesAndIds(procedureCode); + } + + /** + * Get names, ids, and defaults of parameters for the given procedure. + * @param procedureCode Procedure code for procedure to query. + * @returns List of param names for a procedure. + */ + getProcedureParamNamesIdsAndDefaults (procedureCode: string) { + const result = this.thread!.blockContainer!.getProcedureParamNamesIdsAndDefaults(procedureCode); + if (result) { + return result; + } + return this.sequencer!.runtime.getProcedureParamNamesIdsAndDefaults(procedureCode); + } + + /** + * Initialize procedure parameters in the thread before pushing parameters. + */ + initParams (): void { + this.thread!.initParams(); + } + + /** + * Store a procedure parameter value by its name. + * @param paramName The procedure's parameter name. + * @param paramValue The procedure's parameter value. + */ + pushParam (paramName: string, paramValue: unknown): void { + this.thread!.pushParam(paramName, paramValue); + } + + /** + * Retrieve the stored parameter value for a given parameter name. + * @param paramName The procedure's parameter name. + * @returns The parameter's current stored value. + */ + getParam (paramName: string): unknown { + return this.thread!.getParam(paramName); + } + + /** + * Start all relevant hats. + * @param requestedHat Opcode of hats to start. + * @param optMatchFields Optionally, fields to match on the hat. + * @param optTarget Optionally, a target to restrict to. + * @returns List of threads started by this function. + */ + startHats (requestedHat: string, optMatchFields?: object, optTarget?: RenderedTarget) { + // Store thread and sequencer to ensure we can return to the calling block's context. + // startHats may execute further blocks and dirty the BlockUtility's execution context + // and confuse the calling block when we return to it. + const callerThread = this.thread; + const callerSequencer = this.sequencer; + const result = this.sequencer!.runtime.startHats(requestedHat, optMatchFields, optTarget); + + // Restore thread and sequencer to prior values before we return to the calling block. + this.thread = callerThread; + this.sequencer = callerSequencer; + + return result; + } + + /** + * Query a named IO device. + * @param device The name of like the device, like keyboard. + * @param func The name of the device's function to query. + * @param args Arguments to pass to the device's function. + * @returns The expected output for the device's function. + */ + ioQuery> ( + device: T, + func: U, + args?: Runtime['ioDevices'][T][U] extends (...args: infer P) => unknown ? P : never + ) { + const ioDevices = this.sequencer!.runtime.ioDevices; + if (device in ioDevices && func in ioDevices[device]) { + const devObject = ioDevices[device]; + /* eslint-disable prefer-spread */ + // @ts-expect-error yeah but tsc is dumb here, so we handle it by ourselves. + return devObject[func].apply(devObject, args) as ReturnType; + /* eslint-enable prefer-spread */ + } + // @ts-expect-error if we're in ts env, it never get triggered. just make tsc happy. + return null as ReturnType; + } +} + +export default BlockUtility; diff --git a/packages/vm/src/engine/blocks-execute-cache.js b/packages/vm/src/engine/blocks-execute-cache.js deleted file mode 100644 index 88069a3a..00000000 --- a/packages/vm/src/engine/blocks-execute-cache.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @fileoverview - * Helpers shared between blocks.js and execute.js for caching execute - * information. - */ - -/** - * @typedef {import('./blocks')} Blocks - * @typedef {new (blocks: Blocks, cached: object) => object} CacheType - */ - -/** - * A private method shared with execute to build an object containing the block - * information execute needs and that is reset when other cached Blocks info is - * reset. - * @param {Blocks} blocks Blocks containing the expected blockId - * @param {string} blockId blockId for the desired execute cache - * @param {CacheType} [CacheType] constructor for cached block information - * @returns {?object} execute cache object - */ -const getCached = function (blocks, blockId, CacheType) { - const executeCache = blocks._cache._executeCached; - - let cached = executeCache[blockId]; - if (typeof cached !== 'undefined') { - return cached; - } - - const block = blocks.getBlock(blockId); - if (typeof block === 'undefined') { - return null; - } - - const cachedBlockData = { - id: blockId, - opcode: blocks.getOpcode(block), - fields: blocks.getFields(block), - inputs: blocks.getInputs(block), - mutation: blocks.getMutation(block) - }; - - cached = typeof CacheType === 'undefined' ? - cachedBlockData : - new CacheType(blocks, cachedBlockData); - - executeCache[blockId] = cached; - return cached; -}; - -export { - getCached -}; diff --git a/packages/vm/src/engine/blocks-execute-cache.ts b/packages/vm/src/engine/blocks-execute-cache.ts new file mode 100644 index 00000000..0f06808d --- /dev/null +++ b/packages/vm/src/engine/blocks-execute-cache.ts @@ -0,0 +1,60 @@ +/** + * @fileoverview + * Helpers shared between blocks.js and execute.js for caching execute + * information. + */ + +import type Blocks from './blocks'; +import type {VMInput, VMField, VMMutation} from '../serialization/schema'; + +interface CachedBlockData { + id: string; + opcode: string; + fields: Record; + inputs: Record; + mutation?: VMMutation; +} + +type CacheType = new (blocks: Blocks, cached: CachedBlockData) => object; + +/** + * A private method shared with execute to build an object containing the block + * information execute needs and that is reset when other cached Blocks info is + * reset. + * @param blocks Blocks containing the expected blockId + * @param blockId blockId for the desired execute cache + * @param CacheType constructor for cached block information + * @returns execute cache object + */ +const getCached = function (blocks: Blocks, blockId: string, CacheType?: CacheType): object | null { + const executeCache = blocks._cache._executeCached as Record; + + let cached = executeCache[blockId]; + if (typeof cached !== 'undefined') { + return cached; + } + + const block = blocks.getBlock(blockId); + if (typeof block === 'undefined') { + return null; + } + + const cachedBlockData: CachedBlockData = { + id: blockId, + opcode: blocks.getOpcode(block)!, + fields: blocks.getFields(block)!, + inputs: blocks.getInputs(block), + mutation: blocks.getMutation(block)! + }; + + cached = typeof CacheType === 'undefined' ? + cachedBlockData : + new CacheType(blocks, cachedBlockData); + + executeCache[blockId] = cached; + return cached; +}; + +export { + getCached +}; diff --git a/packages/vm/src/engine/blocks-runtime-cache.js b/packages/vm/src/engine/blocks-runtime-cache.ts similarity index 55% rename from packages/vm/src/engine/blocks-runtime-cache.js rename to packages/vm/src/engine/blocks-runtime-cache.ts index c3cd8104..ccb2538a 100644 --- a/packages/vm/src/engine/blocks-runtime-cache.js +++ b/packages/vm/src/engine/blocks-runtime-cache.ts @@ -7,61 +7,61 @@ * so we don't need to in the future. */ -/** - * @typedef {import('./blocks')} Blocks - */ +import type {VMField} from '../serialization/schema'; +import type Blocks from './blocks'; /** * A set of cached data about the top block of a script. - * @param {Blocks} container - Container holding the block and related data - * @param {string} blockId - Id for whose block data is cached in this instance */ class RuntimeScriptCache { /** - * @param {Blocks} container - Container holding the block and related data - * @param {string} blockId - Id for whose block data is cached in this instance + * Container with block data for blockId. + */ + container: Blocks; + + /** + * ID for block this instance caches. + */ + blockId: string; + + /** + * Formatted fields or fields of input blocks ready for comparison in + * runtime. + * + * This is a clone of parts of the targeted blocks. Changes to these + * clones are limited to copies under RuntimeScriptCache and will not + * appear in the original blocks in their container. This copy is + * modified changing the case of strings to uppercase. These uppercase + * values will be compared later by the VM. + */ + fieldsOfInputs: Record; + + /** + * @param container - Container holding the block and related data + * @param blockId - Id for whose block data is cached in this instance */ - constructor (container, blockId) { - /** - * Container with block data for blockId. - * @type {Blocks} - */ + constructor (container: Blocks, blockId: string) { this.container = container; - /** - * ID for block this instance caches. - * @type {string} - */ this.blockId = blockId; const block = container.getBlock(blockId); - const fields = container.getFields(block); + const fields = container.getFields(block)!; - /** - * Formatted fields or fields of input blocks ready for comparison in - * runtime. - * - * This is a clone of parts of the targeted blocks. Changes to these - * clones are limited to copies under RuntimeScriptCache and will not - * appear in the original blocks in their container. This copy is - * modified changing the case of strings to uppercase. These uppercase - * values will be compared later by the VM. - * @type {object} - */ this.fieldsOfInputs = Object.assign({}, fields); if (Object.keys(fields).length === 0) { const inputs = container.getInputs(block); for (const input in inputs) { if (!Object.prototype.hasOwnProperty.call(inputs, input)) continue; const id = inputs[input].block; - const inputBlock = container.getBlock(id); + const inputBlock = container.getBlock(id!); const inputFields = container.getFields(inputBlock); Object.assign(this.fieldsOfInputs, inputFields); } } for (const key in this.fieldsOfInputs) { const field = this.fieldsOfInputs[key] = Object.assign({}, this.fieldsOfInputs[key]); - if (field.value.toUpperCase) { + if (field.value?.toUpperCase) { field.value = field.value.toUpperCase(); } } @@ -70,12 +70,12 @@ class RuntimeScriptCache { /** * Get an array of scripts from a block container prefiltered to match opcode. - * @param {Blocks} container - Container of blocks - * @param {string} opcode - Opcode to filter top blocks by - * @returns {Array.} Array of cached script data for scripts with the given opcode + * @param container - Container of blocks + * @param opcode - Opcode to filter top blocks by + * @returns Array of cached script data for scripts with the given opcode */ -const getScripts = function (container, opcode) { - const runtimeCache = container._cache.scripts; +const getScripts = function (container: Blocks, opcode: string): RuntimeScriptCache[] { + const runtimeCache = container._cache.scripts as Record; let scripts = runtimeCache[opcode]; if (!scripts) { diff --git a/packages/vm/src/engine/blocks.js b/packages/vm/src/engine/blocks.js index c8cbe335..fafc2166 100644 --- a/packages/vm/src/engine/blocks.js +++ b/packages/vm/src/engine/blocks.js @@ -14,8 +14,9 @@ import getMonitorIdForBlockWithArgs from '../util/get-monitor-id'; */ /** - * @typedef {import('./runtime')} Runtime + * @typedef {import('./runtime').default} Runtime * @typedef {import('../serialization/schema').VMBlock} VMBlock + * @typedef {import('../serialization/schema').VMInput} VMInput * @typedef {import('./blocks-runtime-cache').RuntimeScriptCache} RuntimeScriptCache * @import * as ClipCCBlock from 'clipcc-block' */ @@ -190,7 +191,7 @@ class Blocks { /** * Get all non-branch inputs for a block. * @param {?VMBlock} block the block to query. - * @returns All non-branch inputs and their associated blocks. + * @returns {Record} All non-branch inputs and their associated blocks. */ getInputs (block) { if (typeof block === 'undefined') return null; diff --git a/packages/vm/src/engine/execute.js b/packages/vm/src/engine/execute.js index eb8f97aa..e4649803 100644 --- a/packages/vm/src/engine/execute.js +++ b/packages/vm/src/engine/execute.js @@ -1,5 +1,5 @@ -import BlockUtility from './block-utility.js'; -import {getCached as getCachedExecuteBlock} from './blocks-execute-cache.js'; +import BlockUtility from './block-utility'; +import {getCached as getCachedExecuteBlock} from './blocks-execute-cache'; import log from '../util/log'; import Thread from './thread'; import {Map} from 'immutable'; diff --git a/packages/vm/src/engine/runtime.js b/packages/vm/src/engine/runtime.js index cd084616..244a7ef3 100644 --- a/packages/vm/src/engine/runtime.js +++ b/packages/vm/src/engine/runtime.js @@ -2,7 +2,7 @@ import EventEmitter from 'events'; import {OrderedMap} from 'immutable'; import ArgumentType from '../extension-support/argument-type'; import Blocks from './blocks.js'; -import {getScripts as getCachedScriptsByOpcode} from './blocks-runtime-cache.js'; +import {getScripts as getCachedScriptsByOpcode} from './blocks-runtime-cache'; import BlockType from '../extension-support/block-type'; import Profiler from './profiler'; import Sequencer from './sequencer.js'; @@ -592,7 +592,6 @@ class Runtime extends EventEmitter { // Register and initialize "IO devices", containers for processing // I/O related data. - /** @type {Record} */ this.ioDevices = { clock: new Clock(this), cloud: new Cloud(this), diff --git a/packages/vm/src/engine/sequencer.js b/packages/vm/src/engine/sequencer.js index 2d6f6430..beefd39b 100644 --- a/packages/vm/src/engine/sequencer.js +++ b/packages/vm/src/engine/sequencer.js @@ -3,7 +3,7 @@ import Thread from './thread'; import execute from './execute.js'; /** - * @typedef {import('./runtime')} Runtime + * @typedef {import('./runtime').default} Runtime */ /** diff --git a/packages/vm/src/engine/thread.ts b/packages/vm/src/engine/thread.ts index 6860c1b6..ccd3f2fd 100644 --- a/packages/vm/src/engine/thread.ts +++ b/packages/vm/src/engine/thread.ts @@ -1,6 +1,6 @@ -import type Target from './target'; import type Blocks from './blocks'; import type Timer from '../util/timer'; +import type RenderedTarget from '../sprites/rendered-target'; /** * Recycle bin for empty stackFrame objects @@ -44,7 +44,7 @@ class _StackFrame { /** * The target of blocks that this thread will execute. */ - target: Target | null = null; + target: RenderedTarget | null = null; /** * @param warpMode Whether this level is in warp mode. Is set by some legacy blocks and @@ -138,7 +138,7 @@ class Thread { /** * Target of this thread. */ - target: Target | null = null; + target: RenderedTarget | null = null; /** * The Blocks this thread will execute. */ @@ -219,7 +219,7 @@ class Thread { * @param blockId Block ID to push to stack. * @param target New target context. */ - pushStack (blockId: string, target?: Target): void { + pushStack (blockId: string, target?: RenderedTarget): void { this.stack.push(blockId); // Push an empty stack frame, if we need one. // Might not, if we just popped the stack. diff --git a/packages/vm/src/virtual-machine.js b/packages/vm/src/virtual-machine.js index abd6f0de..465a1b2f 100644 --- a/packages/vm/src/virtual-machine.js +++ b/packages/vm/src/virtual-machine.js @@ -42,7 +42,7 @@ const CORE_EXTENSIONS = [ /** * @typedef {number} int - * @typedef {import('./engine/target')} Target + * @typedef {import('./engine/target').default} Target * @typedef {import('./serialization/sb3').ImportedExtensionsInfo} ImportedExtensionsInfo * @typedef {import('clipcc-audio')} AudioEngine * @typedef {import('clipcc-render')} RenderWebGL From 80c5bf3efa991729a3c9b3f02d8e7c8d2b6da5e9 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Mon, 11 May 2026 14:46:17 +0800 Subject: [PATCH 23/30] :art: chore(vm): lint fix Signed-off-by: SimonShiki --- packages/vm/src/blocks/category_prototype.ts | 2 +- packages/vm/src/engine/comment.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/vm/src/blocks/category_prototype.ts b/packages/vm/src/blocks/category_prototype.ts index 01bd874e..5d554f21 100644 --- a/packages/vm/src/blocks/category_prototype.ts +++ b/packages/vm/src/blocks/category_prototype.ts @@ -12,7 +12,7 @@ export type BlockFunction = (args: BlockArgs, util: BlockUtility) => any; export interface CategoryPrototype { /** * Retrieve the block primitives implemented by this package. - * @returns {Record} Mapping of opcode to Function. + * @returns Mapping of opcode to Function. */ getPrimitives(): Record; getHats?(): Record; diff --git a/packages/vm/src/engine/comment.ts b/packages/vm/src/engine/comment.ts index 92273c20..43463ad3 100644 --- a/packages/vm/src/engine/comment.ts +++ b/packages/vm/src/engine/comment.ts @@ -43,7 +43,8 @@ class Comment { public y: number, width: number, height: number, - minimized: boolean) { + minimized: boolean + ) { this.id = id || uid(); this.width = Math.max(Number(width), Comment.MIN_WIDTH); this.height = Math.max(Number(height), Comment.MIN_HEIGHT); From e19802d6ff19468c92b26b22ab38bd09473daca0 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Mon, 11 May 2026 15:25:48 +0800 Subject: [PATCH 24/30] :bug: fix: build and tests Signed-off-by: SimonShiki --- packages/gui/src/types.d.ts | 5 +++++ packages/vm/test/unit/blocks_control.js | 2 +- packages/vm/test/unit/blocks_event.js | 2 +- packages/vm/test/unit/blocks_sensing.js | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/gui/src/types.d.ts b/packages/gui/src/types.d.ts index a239de4c..55e40be5 100644 --- a/packages/gui/src/types.d.ts +++ b/packages/gui/src/types.d.ts @@ -101,3 +101,8 @@ declare module 'react-tabs' { export const Tab: React.ComponentType; export const TabPanel: React.ComponentType; } + +/** + * Compile-time injected clipcc global metadata + */ +declare const clipcc: { VERSION?: string }; diff --git a/packages/vm/test/unit/blocks_control.js b/packages/vm/test/unit/blocks_control.js index 6d2d6cea..877733d4 100644 --- a/packages/vm/test/unit/blocks_control.js +++ b/packages/vm/test/unit/blocks_control.js @@ -1,7 +1,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import Control from '../../src/blocks/scratch3_control'; import Runtime from '../../src/engine/runtime.js'; -import BlockUtility from '../../src/engine/block-utility.js'; +import BlockUtility from '../../src/engine/block-utility'; test('getPrimitives', t => { const rt = new Runtime(); diff --git a/packages/vm/test/unit/blocks_event.js b/packages/vm/test/unit/blocks_event.js index b4bec981..53feacdf 100644 --- a/packages/vm/test/unit/blocks_event.js +++ b/packages/vm/test/unit/blocks_event.js @@ -1,6 +1,6 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import Blocks from '../../src/engine/blocks.js'; -import BlockUtility from '../../src/engine/block-utility.js'; +import BlockUtility from '../../src/engine/block-utility'; import Event from '../../src/blocks/scratch3_event'; import Runtime from '../../src/engine/runtime.js'; import Target from '../../src/engine/target.js'; diff --git a/packages/vm/test/unit/blocks_sensing.js b/packages/vm/test/unit/blocks_sensing.js index 1b1360fa..dfbc8d3d 100644 --- a/packages/vm/test/unit/blocks_sensing.js +++ b/packages/vm/test/unit/blocks_sensing.js @@ -3,7 +3,7 @@ import Sensing from '../../src/blocks/scratch3_sensing'; import Runtime from '../../src/engine/runtime.js'; import Sprite from '../../src/sprites/sprite'; import RenderedTarget from '../../src/sprites/rendered-target.js'; -import BlockUtility from '../../src/engine/block-utility.js'; +import BlockUtility from '../../src/engine/block-utility'; test('getPrimitives', t => { const rt = new Runtime(); From e7c2ff974d799bbba4d9913dacce4e530e338501 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Mon, 11 May 2026 15:30:16 +0800 Subject: [PATCH 25/30] :bug: fix(vm): missing util mock Signed-off-by: SimonShiki --- packages/vm/test/unit/blocks_control.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vm/test/unit/blocks_control.js b/packages/vm/test/unit/blocks_control.js index 877733d4..e5ac5e8f 100644 --- a/packages/vm/test/unit/blocks_control.js +++ b/packages/vm/test/unit/blocks_control.js @@ -270,7 +270,8 @@ test('wait', t => { yield: () => yields++, stackTimerNeedsInit: util.stackTimerNeedsInit, startStackTimer: util.startStackTimer, - stackTimerFinished: util.stackTimerFinished + stackTimerFinished: util.stackTimerFinished, + stackTimerAvailable: util.stackTimerAvailable }; c.wait(args, mockUtil); From c6050e710ee1029821f6a475309665e1fae4db6c Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Mon, 11 May 2026 18:10:25 +0800 Subject: [PATCH 26/30] :wrench: chore(vm): migrate sequencer Signed-off-by: SimonShiki --- packages/vm/src/engine/blocks.js | 8 +- packages/vm/src/engine/profiler.ts | 2 +- packages/vm/src/engine/runtime.js | 2 +- .../src/engine/{sequencer.js => sequencer.ts} | 103 ++++++++---------- packages/vm/src/engine/thread.ts | 6 +- packages/vm/src/sprites/sprite.ts | 10 +- packages/vm/test/unit/engine_sequencer.js | 2 +- 7 files changed, 58 insertions(+), 75 deletions(-) rename packages/vm/src/engine/{sequencer.js => sequencer.ts} (83%) diff --git a/packages/vm/src/engine/blocks.js b/packages/vm/src/engine/blocks.js index fafc2166..278980bf 100644 --- a/packages/vm/src/engine/blocks.js +++ b/packages/vm/src/engine/blocks.js @@ -124,11 +124,11 @@ class Blocks { /** * Provide an object with metadata for the requested block ID. - * @param {!string} blockId ID of block we have stored. - * @returns Metadata about the block, if it exists. + * @param {string | null} [blockId] ID of block we have stored. + * @returns {VMBlock | null} Metadata about the block, if it exists. */ getBlock (blockId) { - return this._blocks[blockId]; + return this._blocks[blockId] ?? null; } /** @@ -272,7 +272,7 @@ class Blocks { /** * Get the procedure definition for a given name. * @param {?string} name Name of procedure to query. - * @param {?boolean} globalOnly True if only find global procedures. + * @param {?boolean} [globalOnly] True if only find global procedures. * @returns {?string} ID of procedure definition. */ getProcedureDefinition (name, globalOnly) { diff --git a/packages/vm/src/engine/profiler.ts b/packages/vm/src/engine/profiler.ts index 906f23c1..0f3d3ef8 100644 --- a/packages/vm/src/engine/profiler.ts +++ b/packages/vm/src/engine/profiler.ts @@ -135,7 +135,7 @@ class Profiler { * Runtime._step. * @param arg An arbitrary argument value to store with the frame. */ - start (id: number, arg: unknown): void { + start (id: number, arg?: unknown): void { this.records.push(START, id, arg, performance.now()); } diff --git a/packages/vm/src/engine/runtime.js b/packages/vm/src/engine/runtime.js index 244a7ef3..d2710d2d 100644 --- a/packages/vm/src/engine/runtime.js +++ b/packages/vm/src/engine/runtime.js @@ -5,7 +5,7 @@ import Blocks from './blocks.js'; import {getScripts as getCachedScriptsByOpcode} from './blocks-runtime-cache'; import BlockType from '../extension-support/block-type'; import Profiler from './profiler'; -import Sequencer from './sequencer.js'; +import Sequencer from './sequencer'; import execute from './execute.js'; import ScratchBlocksConstants from './scratch-blocks-constants'; import TargetType from '../extension-support/target-type'; diff --git a/packages/vm/src/engine/sequencer.js b/packages/vm/src/engine/sequencer.ts similarity index 83% rename from packages/vm/src/engine/sequencer.js rename to packages/vm/src/engine/sequencer.ts index beefd39b..a0d4728a 100644 --- a/packages/vm/src/engine/sequencer.js +++ b/packages/vm/src/engine/sequencer.ts @@ -1,80 +1,63 @@ import Timer from '../util/timer'; import Thread from './thread'; import execute from './execute.js'; - -/** - * @typedef {import('./runtime').default} Runtime - */ +import type Runtime from './runtime'; /** * Profiler frame name for stepping a single thread. - * @constant {string} */ -const stepThreadProfilerFrame = 'Sequencer.stepThread'; +const stepThreadProfilerFrame = 'Sequencer.stepThread' as const; /** * Profiler frame name for the inner loop of stepThreads. - * @constant {string} */ -const stepThreadsInnerProfilerFrame = 'Sequencer.stepThreads#inner'; +const stepThreadsInnerProfilerFrame = 'Sequencer.stepThreads#inner' as const; /** * Profiler frame name for execute. - * @constant {string} */ -const executeProfilerFrame = 'execute'; +const executeProfilerFrame = 'execute' as const; /** * Profiler frame ID for stepThreadProfilerFrame. - * @type {number} */ let stepThreadProfilerId = -1; /** * Profiler frame ID for stepThreadsInnerProfilerFrame. - * @type {number} */ let stepThreadsInnerProfilerId = -1; /** * Profiler frame ID for executeProfilerFrame. - * @type {number} */ let executeProfilerId = -1; class Sequencer { /** - * @param {Runtime} runtime The runtime object. + * A utility timer for timing thread sequencing. */ - constructor (runtime) { + timer = new Timer(); + activeThread: Thread | null = null; + constructor ( /** - * A utility timer for timing thread sequencing. - * @type {!Timer} + * The runtime object. */ - this.timer = new Timer(); - - /** - * Reference to the runtime owning this sequencer. - * @type {!Runtime} - */ - this.runtime = runtime; - - this.activeThread = null; - } + public runtime: Runtime + ) { } /** * Time to run a warp-mode thread, in ms. - * @type {number} */ static get WARP_TIME () { - return 500; + return 500 as const; } /** * Step through all threads in `this.runtime.threads`, running them in order. - * @returns {Array.} List of inactive threads after stepping. + * @returns List of inactive threads after stepping. */ - stepThreads () { + stepThreads (): Thread[] { // Work time is 75% of the thread stepping interval. const WORK_TIME = 0.75 * this.runtime.currentStepTime; // For compatibility with Scatch 2, update the millisecond clock @@ -93,9 +76,9 @@ class Sequencer { // 2. Time elapsed must be less than WORK_TIME. // 3. Either turbo mode, or no redraw has been requested by a primitive. while (this.runtime.threads.length > 0 && - numActiveThreads > 0 && - this.timer.timeElapsed() < WORK_TIME && - (this.runtime.turboMode || !this.runtime.redrawRequested)) { + numActiveThreads > 0 && + this.timer.timeElapsed() < WORK_TIME && + (this.runtime.turboMode || !this.runtime.redrawRequested)) { if (this.runtime.profiler !== null) { if (stepThreadsInnerProfilerId === -1) { stepThreadsInnerProfilerId = this.runtime.profiler.idByName(stepThreadsInnerProfilerFrame); @@ -181,9 +164,9 @@ class Sequencer { /** * Step the requested thread for as long as necessary. - * @param {!Thread} thread Thread object to step. + * @param thread Thread object to step. */ - stepThread (thread) { + stepThread (thread: Thread) { let currentBlockId = thread.peekStack(); if (!currentBlockId) { // A "null block" - empty branch. @@ -197,7 +180,7 @@ class Sequencer { } // Save the current block ID to notice if we did control flow. while ((currentBlockId = thread.peekStack())) { - let isWarpMode = thread.peekStackFrame().warpMode; + let isWarpMode = thread.peekStackFrame()?.warpMode; if (isWarpMode && !thread.warpTimer) { // Initialize warp-mode timer if it hasn't been already. // This will start counting the thread toward `Sequencer.WARP_TIME`. @@ -225,7 +208,7 @@ class Sequencer { thread.status = Thread.STATUS_RUNNING; // In warp mode, yielded blocks are re-executed immediately. if (isWarpMode && - thread.warpTimer.timeElapsed() <= Sequencer.WARP_TIME) { + thread.warpTimer!.timeElapsed() <= Sequencer.WARP_TIME) { continue; } return; @@ -254,7 +237,7 @@ class Sequencer { return; } - const stackFrame = thread.peekStackFrame(); + const stackFrame = thread.peekStackFrame()!; isWarpMode = stackFrame.warpMode; if (stackFrame.isLoop) { @@ -263,7 +246,7 @@ class Sequencer { // Unless we're in warp mode - then only return if the // warp timer is up. if (!isWarpMode || - thread.warpTimer.timeElapsed() > Sequencer.WARP_TIME) { + thread.warpTimer!.timeElapsed() > Sequencer.WARP_TIME) { // Don't do anything to the stack, since loops need // to be re-executed. return; @@ -287,20 +270,20 @@ class Sequencer { /** * Step a thread into a block's branch. - * @param {!Thread} thread Thread object to step to branch. - * @param {number} branchNum Which branch to step to (i.e., 1, 2). - * @param {boolean} isLoop Whether this block is a loop. + * @param thread Thread object to step to branch. + * @param branchNum Which branch to step to (i.e., 1, 2). + * @param isLoop Whether this block is a loop. */ - stepToBranch (thread, branchNum, isLoop) { + stepToBranch (thread: Thread, branchNum: number, isLoop: boolean) { if (!branchNum) { branchNum = 1; } const currentBlockId = thread.peekStack(); - const branchId = thread.blockContainer.getBranch( + const branchId = thread.blockContainer?.getBranch( currentBlockId, branchNum ); - thread.peekStackFrame().isLoop = isLoop; + thread.peekStackFrame()!.isLoop = isLoop; if (branchId) { // Push branch ID to the thread's stack. thread.pushStack(branchId); @@ -311,11 +294,11 @@ class Sequencer { /** * Step a procedure. - * @param {!Thread} thread Thread object to step to procedure. - * @param {!string} procedureCode Procedure code of procedure to step to. + * @param thread Thread object to step to procedure. + * @param procedureCode Procedure code of procedure to step to. */ - stepToProcedure (thread, procedureCode) { - let definition = thread.blockContainer.getProcedureDefinition(procedureCode); + stepToProcedure (thread: Thread, procedureCode: string) { + let definition = thread.blockContainer?.getProcedureDefinition(procedureCode); let target = thread.target; if (!definition) { [target, definition] = this.runtime.getProcedureDefinition(procedureCode); @@ -326,9 +309,9 @@ class Sequencer { // Look for warp-mode flag on definition, and set the thread // to warp-mode if needed. - const definitionBlock = target.blocks.getBlock(definition); - const innerBlock = target.blocks.getBlock( - definitionBlock.inputs.custom_block.block); + const definitionBlock = target!.blocks.getBlock(definition); + const innerBlock = target!.blocks.getBlock( + definitionBlock?.inputs.custom_block.block); let doWarp = false; if (innerBlock && innerBlock.mutation) { const warp = innerBlock.mutation.warp; @@ -347,13 +330,13 @@ class Sequencer { // and on to the main definition of the procedure. // When that set of blocks finishes executing, it will be popped // from the stack by the sequencer, returning control to the caller. - thread.pushStack(definition, target); + thread.pushStack(definition, target!); // In known warp-mode threads, only yield when time is up. - if (thread.peekStackFrame().warpMode && - thread.warpTimer.timeElapsed() > Sequencer.WARP_TIME) { + if (thread.peekStackFrame()!.warpMode && + thread.warpTimer!.timeElapsed() > Sequencer.WARP_TIME) { thread.status = Thread.STATUS_YIELD; } else if (doWarp) { - thread.peekStackFrame().warpMode = true; + thread.peekStackFrame()!.warpMode = true; } else if (isRecursive) { // In normal-mode threads, yield any time we have a recursive call. thread.status = Thread.STATUS_YIELD; @@ -362,11 +345,11 @@ class Sequencer { /** * Retire a thread in the middle, without considering further blocks. - * @param {!Thread} thread Thread object to retire. + * @param thread Thread object to retire. */ - retireThread (thread) { + retireThread (thread: Thread) { thread.stack = []; - thread.stackFrame = []; + thread.stackFrames = []; thread.requestScriptGlowInFrame = false; thread.status = Thread.STATUS_DONE; } diff --git a/packages/vm/src/engine/thread.ts b/packages/vm/src/engine/thread.ts index ccd3f2fd..5d529460 100644 --- a/packages/vm/src/engine/thread.ts +++ b/packages/vm/src/engine/thread.ts @@ -122,7 +122,7 @@ class Thread { * Stack for the thread. When the sequencer enters a control structure, * the block is pushed onto the stack so we know where to exit. */ - stack: string[] = []; + stack: (string | null)[] = []; /** * Stack frames for the thread. Store metadata for the executing blocks. */ @@ -219,7 +219,7 @@ class Thread { * @param blockId Block ID to push to stack. * @param target New target context. */ - pushStack (blockId: string, target?: RenderedTarget): void { + pushStack (blockId: string | null, target?: RenderedTarget): void { this.stack.push(blockId); // Push an empty stack frame, if we need one. // Might not, if we just popped the stack. @@ -252,7 +252,7 @@ class Thread { * Pop last block on the stack and its stack frame. * @returns Block ID popped from the stack. */ - popStack (): string | undefined { + popStack () { _StackFrame.release(this.stackFrames.pop()!); const stackFrame = this.peekStackFrame(); if (stackFrame) { diff --git a/packages/vm/src/sprites/sprite.ts b/packages/vm/src/sprites/sprite.ts index a9e0c0c2..638c12b4 100644 --- a/packages/vm/src/sprites/sprite.ts +++ b/packages/vm/src/sprites/sprite.ts @@ -97,9 +97,9 @@ class Sprite { /** * Add a costume at the given index, taking care to avoid duplicate names. * @param costumeObject Object representing the costume. - * @param {!int} index Index at which to add costume + * @param index Index at which to add costume */ - addCostumeAt (costumeObject: Costume, index: number) { + addCostumeAt (costumeObject: Costume, index: int) { if (!costumeObject.name) { costumeObject.name = ''; } @@ -110,8 +110,8 @@ class Sprite { /** * Delete a costume by index. - * @param {number} index Costume index to be deleted - * @returns {?object} The deleted costume + * @param index Costume index to be deleted + * @returns The deleted costume */ deleteCostumeAt (index: number) { return this.costumes_.splice(index, 1)[0]; @@ -121,7 +121,7 @@ class Sprite { * Create a clone of this sprite. * @param optLayerGroup Optional layer group the clone's drawable should be added to * Defaults to the sprite layer group - * @returns {!RenderedTarget} Newly created clone. + * @returns Newly created clone. */ createClone (optLayerGroup: StageLayer) { const newClone = new RenderedTarget(this, this.runtime); diff --git a/packages/vm/test/unit/engine_sequencer.js b/packages/vm/test/unit/engine_sequencer.js index 73910fcf..7e340f71 100644 --- a/packages/vm/test/unit/engine_sequencer.js +++ b/packages/vm/test/unit/engine_sequencer.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import Sequencer from '../../src/engine/sequencer.js'; +import Sequencer from '../../src/engine/sequencer'; import Runtime from '../../src/engine/runtime.js'; import Thread from '../../src/engine/thread'; import RenderedTarget from '../../src/sprites/rendered-target.js'; From f8f34a178317e1b244c7b65e621ef9e8f05659da Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Mon, 11 May 2026 21:25:09 +0800 Subject: [PATCH 27/30] :beers: fix(vm): accidentally change clone logic Signed-off-by: SimonShiki --- packages/vm/src/blocks/scratch3_control.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vm/src/blocks/scratch3_control.ts b/packages/vm/src/blocks/scratch3_control.ts index def18124..12eca7e9 100644 --- a/packages/vm/src/blocks/scratch3_control.ts +++ b/packages/vm/src/blocks/scratch3_control.ts @@ -182,7 +182,7 @@ class Scratch3ControlBlocks implements CategoryPrototype { } deleteClone (args: BlockArgs, util: BlockUtility) { - if (!util.target.isOriginal) return; + if (util.target.isOriginal) return; this.runtime.disposeTarget(util.target); this.runtime.stopForTarget(util.target); } From 61d5dd08e1e2d04ce0a94f21765b8b5d65f1c015 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Mon, 11 May 2026 21:57:21 +0800 Subject: [PATCH 28/30] :bug: fix(vm): inconsistent behavior Signed-off-by: SimonShiki --- packages/vm/src/engine/blocks-runtime-cache.ts | 6 +++--- packages/vm/src/engine/blocks.js | 4 ++-- packages/vm/src/engine/sequencer.ts | 1 - 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/vm/src/engine/blocks-runtime-cache.ts b/packages/vm/src/engine/blocks-runtime-cache.ts index ccb2538a..dac0cbdc 100644 --- a/packages/vm/src/engine/blocks-runtime-cache.ts +++ b/packages/vm/src/engine/blocks-runtime-cache.ts @@ -45,7 +45,7 @@ class RuntimeScriptCache { this.blockId = blockId; - const block = container.getBlock(blockId); + const block = container.getBlock(blockId)!; const fields = container.getFields(block)!; this.fieldsOfInputs = Object.assign({}, fields); @@ -54,7 +54,7 @@ class RuntimeScriptCache { for (const input in inputs) { if (!Object.prototype.hasOwnProperty.call(inputs, input)) continue; const id = inputs[input].block; - const inputBlock = container.getBlock(id!); + const inputBlock = container.getBlock(id!)!; const inputFields = container.getFields(inputBlock); Object.assign(this.fieldsOfInputs, inputFields); } @@ -85,7 +85,7 @@ const getScripts = function (container: Blocks, opcode: string): RuntimeScriptCa for (let i = 0; i < allScripts.length; i++) { const topBlockId = allScripts[i]; const block = container.getBlock(topBlockId); - if (block.opcode === opcode) { + if (block?.opcode === opcode) { scripts.push(new RuntimeScriptCache(container, topBlockId)); } } diff --git a/packages/vm/src/engine/blocks.js b/packages/vm/src/engine/blocks.js index 278980bf..0a53eafc 100644 --- a/packages/vm/src/engine/blocks.js +++ b/packages/vm/src/engine/blocks.js @@ -125,10 +125,10 @@ class Blocks { /** * Provide an object with metadata for the requested block ID. * @param {string | null} [blockId] ID of block we have stored. - * @returns {VMBlock | null} Metadata about the block, if it exists. + * @returns {VMBlock | undefined} Metadata about the block, if it exists. */ getBlock (blockId) { - return this._blocks[blockId] ?? null; + return this._blocks[blockId]; } /** diff --git a/packages/vm/src/engine/sequencer.ts b/packages/vm/src/engine/sequencer.ts index a0d4728a..ac1e6bd1 100644 --- a/packages/vm/src/engine/sequencer.ts +++ b/packages/vm/src/engine/sequencer.ts @@ -349,7 +349,6 @@ class Sequencer { */ retireThread (thread: Thread) { thread.stack = []; - thread.stackFrames = []; thread.requestScriptGlowInFrame = false; thread.status = Thread.STATUS_DONE; } From 47baeb8e1c50aef09a6a068de013b5231068ee79 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Mon, 11 May 2026 22:07:11 +0800 Subject: [PATCH 29/30] :wrench: chore(vm): export schema Signed-off-by: SimonShiki --- packages/vm/package.json | 4 ++-- packages/vm/src/{index.js => index.ts} | 2 ++ packages/vm/src/playground/benchmark.js | 2 +- packages/vm/test/extra/performance.js | 2 +- .../test/integration/block_to_workspace_comment_import.js | 2 +- .../block_to_workspace_comment_import_no_scripts.js | 2 +- packages/vm/test/integration/broadcast_special_chars_sb2.js | 2 +- packages/vm/test/integration/broadcast_special_chars_sb3.js | 2 +- packages/vm/test/integration/clone-cleanup.js | 2 +- packages/vm/test/integration/cloud_variables_sb2.js | 2 +- packages/vm/test/integration/cloud_variables_sb3.js | 2 +- packages/vm/test/integration/comments.js | 2 +- packages/vm/test/integration/comments_sb3.js | 2 +- packages/vm/test/integration/complex.js | 2 +- packages/vm/test/integration/control.js | 2 +- packages/vm/test/integration/data.js | 2 +- packages/vm/test/integration/event.js | 2 +- packages/vm/test/integration/execute.js | 2 +- packages/vm/test/integration/hat-execution-order.js | 2 +- packages/vm/test/integration/hat-threads-run-every-frame.js | 2 +- packages/vm/test/integration/import-sb.js | 2 +- packages/vm/test/integration/import-sb2-from-object.js | 2 +- packages/vm/test/integration/list-monitor-rename.js | 2 +- packages/vm/test/integration/load-extensions.js | 2 +- packages/vm/test/integration/looks.js | 2 +- .../vm/test/integration/monitor-threads-run-every-frame.js | 2 +- packages/vm/test/integration/monitors_sb2.js | 2 +- packages/vm/test/integration/monitors_sb2_to_sb3.js | 2 +- packages/vm/test/integration/monitors_sb3.js | 2 +- packages/vm/test/integration/motion.js | 2 +- packages/vm/test/integration/offline-custom-assets.js | 2 +- packages/vm/test/integration/pen.js | 2 +- packages/vm/test/integration/procedure.js | 2 +- .../vm/test/integration/running_project_changed_state.js | 2 +- packages/vm/test/integration/saythink-and-wait.js | 2 +- .../vm/test/integration/sb2-import-extension-monitors.js | 2 +- packages/vm/test/integration/sb2_corrupted_png.js | 2 +- packages/vm/test/integration/sb2_corrupted_svg.js | 2 +- packages/vm/test/integration/sb2_missing_png.js | 2 +- packages/vm/test/integration/sb2_missing_svg.js | 2 +- packages/vm/test/integration/sb3_corrupted_png.js | 2 +- packages/vm/test/integration/sb3_corrupted_sound.js | 2 +- packages/vm/test/integration/sb3_corrupted_svg.js | 2 +- packages/vm/test/integration/sb3_missing_png.js | 2 +- packages/vm/test/integration/sb3_missing_sound.js | 2 +- packages/vm/test/integration/sb3_missing_svg.js | 2 +- packages/vm/test/integration/sensing.js | 2 +- packages/vm/test/integration/sound.js | 2 +- packages/vm/test/integration/sprite2_corrupted_png.js | 2 +- packages/vm/test/integration/sprite2_corrupted_svg.js | 2 +- packages/vm/test/integration/sprite2_missing_png.js | 2 +- packages/vm/test/integration/sprite2_missing_svg.js | 2 +- packages/vm/test/integration/sprite3_corrupted_png.js | 2 +- packages/vm/test/integration/sprite3_corrupted_svg.js | 2 +- packages/vm/test/integration/sprite3_missing_png.js | 2 +- packages/vm/test/integration/sprite3_missing_svg.js | 2 +- packages/vm/test/integration/stack-click.js | 2 +- .../vm/test/integration/unknown-opcode-as-reporter-block.js | 2 +- packages/vm/test/integration/unknown-opcode-in-c-block.js | 2 +- packages/vm/test/integration/unknown-opcode.js | 2 +- packages/vm/test/integration/variable_monitor_reset.js | 2 +- packages/vm/test/integration/variable_special_chars_sb2.js | 2 +- packages/vm/test/integration/variable_special_chars_sb3.js | 2 +- packages/vm/test/unit/blocks_motion.js | 2 +- packages/vm/test/unit/serialization_sb3.js | 2 +- packages/vm/test/unit/spec.js | 2 +- packages/vm/webpack.config.js | 6 +++--- 67 files changed, 71 insertions(+), 69 deletions(-) rename packages/vm/src/{index.js => index.ts} (80%) diff --git a/packages/vm/package.json b/packages/vm/package.json index 24417ed7..3107248e 100644 --- a/packages/vm/package.json +++ b/packages/vm/package.json @@ -11,10 +11,10 @@ }, "exports": { "types": "./dist/types/index.d.ts", - "webpack": "./src/index.js", + "webpack": "./src/index.ts", "node": "./dist/node/scratch-vm.js", "browser": "./dist/web/scratch-vm.min.js", - "default": "./src/index.js" + "default": "./src/index.ts" }, "scripts": { "build:types": "tsc", diff --git a/packages/vm/src/index.js b/packages/vm/src/index.ts similarity index 80% rename from packages/vm/src/index.js rename to packages/vm/src/index.ts index 45b8c1e2..a339d345 100644 --- a/packages/vm/src/index.js +++ b/packages/vm/src/index.ts @@ -4,3 +4,5 @@ import BlockType from './extension-support/block-type'; export default VirtualMachine; export {ArgumentType, BlockType}; + +export type * as schema from './serialization/schema'; diff --git a/packages/vm/src/playground/benchmark.js b/packages/vm/src/playground/benchmark.js index 1af45633..8e600dfa 100644 --- a/packages/vm/src/playground/benchmark.js +++ b/packages/vm/src/playground/benchmark.js @@ -49,7 +49,7 @@ const importLoadSound = { }; import {ScratchStorage} from 'clipcc-storage'; -import VirtualMachine from '../index.js'; +import VirtualMachine from '../index'; import Runtime from '../engine/runtime.js'; import ScratchRender from 'clipcc-render'; import AudioEngine from 'clipcc-audio'; diff --git a/packages/vm/test/extra/performance.js b/packages/vm/test/extra/performance.js index b2307e49..8618654a 100644 --- a/packages/vm/test/extra/performance.js +++ b/packages/vm/test/extra/performance.js @@ -4,7 +4,7 @@ import path from 'path'; import process from 'process'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; /** * @fileoverview Test vm's performance. diff --git a/packages/vm/test/integration/block_to_workspace_comment_import.js b/packages/vm/test/integration/block_to_workspace_comment_import.js index c41622be..331fa0a8 100644 --- a/packages/vm/test/integration/block_to_workspace_comment_import.js +++ b/packages/vm/test/integration/block_to_workspace_comment_import.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const projectUri = path.resolve(__dirname, '../fixtures/block-to-workspace-comments.sb2'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/block_to_workspace_comment_import_no_scripts.js b/packages/vm/test/integration/block_to_workspace_comment_import_no_scripts.js index b74ff85a..d3b25228 100644 --- a/packages/vm/test/integration/block_to_workspace_comment_import_no_scripts.js +++ b/packages/vm/test/integration/block_to_workspace_comment_import_no_scripts.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const projectUri = path.resolve(__dirname, '../fixtures/block-to-workspace-comments-without-scripts.sb2'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/broadcast_special_chars_sb2.js b/packages/vm/test/integration/broadcast_special_chars_sb2.js index d882dafe..5a64eff1 100644 --- a/packages/vm/test/integration/broadcast_special_chars_sb2.js +++ b/packages/vm/test/integration/broadcast_special_chars_sb2.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import Variable from '../../src/engine/variable'; import StringUtil from '../../src/util/string-util'; import VariableUtil from '../../src/util/variable-util'; diff --git a/packages/vm/test/integration/broadcast_special_chars_sb3.js b/packages/vm/test/integration/broadcast_special_chars_sb3.js index acb3bfe1..87f901c5 100644 --- a/packages/vm/test/integration/broadcast_special_chars_sb3.js +++ b/packages/vm/test/integration/broadcast_special_chars_sb3.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import Variable from '../../src/engine/variable'; import StringUtil from '../../src/util/string-util'; import VariableUtil from '../../src/util/variable-util'; diff --git a/packages/vm/test/integration/clone-cleanup.js b/packages/vm/test/integration/clone-cleanup.js index de16c67e..79f323fa 100644 --- a/packages/vm/test/integration/clone-cleanup.js +++ b/packages/vm/test/integration/clone-cleanup.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const projectUri = path.resolve(__dirname, '../fixtures/clone-cleanup.sb2'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/cloud_variables_sb2.js b/packages/vm/test/integration/cloud_variables_sb2.js index 0de12ab7..8b14f6f9 100644 --- a/packages/vm/test/integration/cloud_variables_sb2.js +++ b/packages/vm/test/integration/cloud_variables_sb2.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const cloudVarSimpleUri = path.resolve(__dirname, '../fixtures/cloud_variables_simple.sb2'); const cloudVarLimitUri = path.resolve(__dirname, '../fixtures/cloud_variables_limit.sb2'); diff --git a/packages/vm/test/integration/cloud_variables_sb3.js b/packages/vm/test/integration/cloud_variables_sb3.js index c64fbfc9..f49904be 100644 --- a/packages/vm/test/integration/cloud_variables_sb3.js +++ b/packages/vm/test/integration/cloud_variables_sb3.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const cloudVarSimpleUri = path.resolve(__dirname, '../fixtures/cloud_variables_simple.sb3'); const cloudVarLimitUri = path.resolve(__dirname, '../fixtures/cloud_variables_limit.sb3'); diff --git a/packages/vm/test/integration/comments.js b/packages/vm/test/integration/comments.js index 525666af..fe8b92a8 100644 --- a/packages/vm/test/integration/comments.js +++ b/packages/vm/test/integration/comments.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const projectUri = path.resolve(__dirname, '../fixtures/comments.sb2'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/comments_sb3.js b/packages/vm/test/integration/comments_sb3.js index abe09b58..e88e6314 100644 --- a/packages/vm/test/integration/comments_sb3.js +++ b/packages/vm/test/integration/comments_sb3.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const projectUri = path.resolve(__dirname, '../fixtures/comments.sb3'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/complex.js b/packages/vm/test/integration/complex.js index 15365fe3..5a1bfde7 100644 --- a/packages/vm/test/integration/complex.js +++ b/packages/vm/test/integration/complex.js @@ -3,7 +3,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const projectUri = path.resolve(__dirname, '../fixtures/complex.sb2'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/control.js b/packages/vm/test/integration/control.js index df89aa7a..9330f74c 100644 --- a/packages/vm/test/integration/control.js +++ b/packages/vm/test/integration/control.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const uri = path.resolve(__dirname, '../fixtures/control.sb2'); const project = readFileToBuffer(uri); diff --git a/packages/vm/test/integration/data.js b/packages/vm/test/integration/data.js index 98fa714f..57dae7a9 100644 --- a/packages/vm/test/integration/data.js +++ b/packages/vm/test/integration/data.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const uri = path.resolve(__dirname, '../fixtures/data.sb2'); const project = readFileToBuffer(uri); diff --git a/packages/vm/test/integration/event.js b/packages/vm/test/integration/event.js index 7866c50c..88b1269c 100644 --- a/packages/vm/test/integration/event.js +++ b/packages/vm/test/integration/event.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const uri = path.resolve(__dirname, '../fixtures/event.sb2'); const project = readFileToBuffer(uri); diff --git a/packages/vm/test/integration/execute.js b/packages/vm/test/integration/execute.js index 6d1af6b2..ecf7e2e5 100644 --- a/packages/vm/test/integration/execute.js +++ b/packages/vm/test/integration/execute.js @@ -3,7 +3,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; /** * @fileoverview Transform each sb2 in fixtures/execute into a test. diff --git a/packages/vm/test/integration/hat-execution-order.js b/packages/vm/test/integration/hat-execution-order.js index ad50d33f..22dfb3a8 100644 --- a/packages/vm/test/integration/hat-execution-order.js +++ b/packages/vm/test/integration/hat-execution-order.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const projectUri = path.resolve(__dirname, '../fixtures/hat-execution-order.sb2'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/hat-threads-run-every-frame.js b/packages/vm/test/integration/hat-threads-run-every-frame.js index 08da93a7..6f64ab7a 100644 --- a/packages/vm/test/integration/hat-threads-run-every-frame.js +++ b/packages/vm/test/integration/hat-threads-run-every-frame.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import Thread from '../../src/engine/thread'; import Runtime from '../../src/engine/runtime.js'; import execute from '../../src/engine/execute.js'; diff --git a/packages/vm/test/integration/import-sb.js b/packages/vm/test/integration/import-sb.js index 1c5b405a..5c38e95d 100644 --- a/packages/vm/test/integration/import-sb.js +++ b/packages/vm/test/integration/import-sb.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const uri = path.resolve(__dirname, '../fixtures/single_sound.sb'); const project = readFileToBuffer(uri); diff --git a/packages/vm/test/integration/import-sb2-from-object.js b/packages/vm/test/integration/import-sb2-from-object.js index c3e9604f..176e95de 100644 --- a/packages/vm/test/integration/import-sb2-from-object.js +++ b/packages/vm/test/integration/import-sb2-from-object.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {extractProjectJson} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const uri = path.resolve(__dirname, '../fixtures/default.sb2'); const project = extractProjectJson(uri); diff --git a/packages/vm/test/integration/list-monitor-rename.js b/packages/vm/test/integration/list-monitor-rename.js index 794ea4f8..6fa6f684 100644 --- a/packages/vm/test/integration/list-monitor-rename.js +++ b/packages/vm/test/integration/list-monitor-rename.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const projectUri = path.resolve(__dirname, '../fixtures/list-monitor-rename.sb3'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/load-extensions.js b/packages/vm/test/integration/load-extensions.js index dd70f037..4e0c88f2 100644 --- a/packages/vm/test/integration/load-extensions.js +++ b/packages/vm/test/integration/load-extensions.js @@ -3,7 +3,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import fs from 'fs'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import dispatch from '../../src/dispatch/central-dispatch'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; /** * Call _stopLoop() on the Video Sensing extension. diff --git a/packages/vm/test/integration/looks.js b/packages/vm/test/integration/looks.js index 56c9feb1..5aa38636 100644 --- a/packages/vm/test/integration/looks.js +++ b/packages/vm/test/integration/looks.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const uri = path.resolve(__dirname, '../fixtures/looks.sb2'); const project = readFileToBuffer(uri); diff --git a/packages/vm/test/integration/monitor-threads-run-every-frame.js b/packages/vm/test/integration/monitor-threads-run-every-frame.js index c0da2ccf..d7f31da4 100644 --- a/packages/vm/test/integration/monitor-threads-run-every-frame.js +++ b/packages/vm/test/integration/monitor-threads-run-every-frame.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import Thread from '../../src/engine/thread'; import Runtime from '../../src/engine/runtime.js'; diff --git a/packages/vm/test/integration/monitors_sb2.js b/packages/vm/test/integration/monitors_sb2.js index cd04b11f..dbc7f1e6 100644 --- a/packages/vm/test/integration/monitors_sb2.js +++ b/packages/vm/test/integration/monitors_sb2.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const projectUri = path.resolve(__dirname, '../fixtures/monitors.sb2'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/monitors_sb2_to_sb3.js b/packages/vm/test/integration/monitors_sb2_to_sb3.js index 288cdbca..16d30cb0 100644 --- a/packages/vm/test/integration/monitors_sb2_to_sb3.js +++ b/packages/vm/test/integration/monitors_sb2_to_sb3.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; let vm; diff --git a/packages/vm/test/integration/monitors_sb3.js b/packages/vm/test/integration/monitors_sb3.js index 715d2a29..c407f3c6 100644 --- a/packages/vm/test/integration/monitors_sb3.js +++ b/packages/vm/test/integration/monitors_sb3.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import Variable from '../../src/engine/variable'; const projectUri = path.resolve(__dirname, '../fixtures/monitors.sb3'); diff --git a/packages/vm/test/integration/motion.js b/packages/vm/test/integration/motion.js index 543ba348..7f021cdf 100644 --- a/packages/vm/test/integration/motion.js +++ b/packages/vm/test/integration/motion.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const uri = path.resolve(__dirname, '../fixtures/motion.sb2'); const project = readFileToBuffer(uri); diff --git a/packages/vm/test/integration/offline-custom-assets.js b/packages/vm/test/integration/offline-custom-assets.js index 2cbc8313..71845c30 100644 --- a/packages/vm/test/integration/offline-custom-assets.js +++ b/packages/vm/test/integration/offline-custom-assets.js @@ -10,7 +10,7 @@ import fs from 'fs'; import {test} from '../fixtures/jest-tap-bridge.js'; import AdmZip from 'adm-zip'; import {ScratchStorage} from 'clipcc-storage'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const projectUri = path.resolve(__dirname, '../fixtures/offline-custom-assets.sb2'); const projectZip = AdmZip(projectUri); diff --git a/packages/vm/test/integration/pen.js b/packages/vm/test/integration/pen.js index a1038fb3..6965fd06 100644 --- a/packages/vm/test/integration/pen.js +++ b/packages/vm/test/integration/pen.js @@ -2,7 +2,7 @@ import Worker from 'tiny-worker'; import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import Scratch3PenBlocks from '../../src/extensions/scratch3_pen/index.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import dispatch from '../../src/dispatch/central-dispatch'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; diff --git a/packages/vm/test/integration/procedure.js b/packages/vm/test/integration/procedure.js index c08675c7..ccc71c17 100644 --- a/packages/vm/test/integration/procedure.js +++ b/packages/vm/test/integration/procedure.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const uri = path.resolve(__dirname, '../fixtures/procedure.sb2'); const project = readFileToBuffer(uri); diff --git a/packages/vm/test/integration/running_project_changed_state.js b/packages/vm/test/integration/running_project_changed_state.js index 8914b4fe..f1b84b51 100644 --- a/packages/vm/test/integration/running_project_changed_state.js +++ b/packages/vm/test/integration/running_project_changed_state.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const uri = path.resolve(__dirname, '../fixtures/looks.sb2'); const project = readFileToBuffer(uri); diff --git a/packages/vm/test/integration/saythink-and-wait.js b/packages/vm/test/integration/saythink-and-wait.js index 2fb9dac9..2a85407b 100644 --- a/packages/vm/test/integration/saythink-and-wait.js +++ b/packages/vm/test/integration/saythink-and-wait.js @@ -3,7 +3,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import dispatch from '../../src/dispatch/central-dispatch'; const uri = path.resolve(__dirname, '../fixtures/saythink-and-wait.sb2'); diff --git a/packages/vm/test/integration/sb2-import-extension-monitors.js b/packages/vm/test/integration/sb2-import-extension-monitors.js index bcd600b4..654bc7e3 100644 --- a/packages/vm/test/integration/sb2-import-extension-monitors.js +++ b/packages/vm/test/integration/sb2-import-extension-monitors.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer, extractProjectJson} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import {deserialize} from '../../src/serialization/sb2.js'; const invisibleVideoMonitorProjectUri = path.resolve(__dirname, '../fixtures/invisible-video-monitor.sb2'); diff --git a/packages/vm/test/integration/sb2_corrupted_png.js b/packages/vm/test/integration/sb2_corrupted_png.js index 06c4c86c..4c295370 100644 --- a/packages/vm/test/integration/sb2_corrupted_png.js +++ b/packages/vm/test/integration/sb2_corrupted_png.js @@ -14,7 +14,7 @@ import makeTestStorage from '../fixtures/make-test-storage.js'; import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {extractAsset, readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; const projectUri = path.resolve(__dirname, '../fixtures/corrupt_png.sb2'); diff --git a/packages/vm/test/integration/sb2_corrupted_svg.js b/packages/vm/test/integration/sb2_corrupted_svg.js index 8c1ee572..6d0716d5 100644 --- a/packages/vm/test/integration/sb2_corrupted_svg.js +++ b/packages/vm/test/integration/sb2_corrupted_svg.js @@ -14,7 +14,7 @@ import makeTestStorage from '../fixtures/make-test-storage.js'; import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {extractAsset, readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; const projectUri = path.resolve(__dirname, '../fixtures/corrupt_svg.sb2'); diff --git a/packages/vm/test/integration/sb2_missing_png.js b/packages/vm/test/integration/sb2_missing_png.js index d654533b..f1a59139 100644 --- a/packages/vm/test/integration/sb2_missing_png.js +++ b/packages/vm/test/integration/sb2_missing_png.js @@ -13,7 +13,7 @@ import makeTestStorage from '../fixtures/make-test-storage.js'; import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; const projectUri = path.resolve(__dirname, '../fixtures/missing_png.sb2'); diff --git a/packages/vm/test/integration/sb2_missing_svg.js b/packages/vm/test/integration/sb2_missing_svg.js index f49c452e..e1d4878f 100644 --- a/packages/vm/test/integration/sb2_missing_svg.js +++ b/packages/vm/test/integration/sb2_missing_svg.js @@ -13,7 +13,7 @@ import makeTestStorage from '../fixtures/make-test-storage.js'; import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; const projectUri = path.resolve(__dirname, '../fixtures/missing_svg.sb2'); diff --git a/packages/vm/test/integration/sb3_corrupted_png.js b/packages/vm/test/integration/sb3_corrupted_png.js index 8b7ce663..96fed65c 100644 --- a/packages/vm/test/integration/sb3_corrupted_png.js +++ b/packages/vm/test/integration/sb3_corrupted_png.js @@ -14,7 +14,7 @@ import makeTestStorage from '../fixtures/make-test-storage.js'; import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {extractAsset, readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; const projectUri = path.resolve(__dirname, '../fixtures/corrupt_png.sb3'); diff --git a/packages/vm/test/integration/sb3_corrupted_sound.js b/packages/vm/test/integration/sb3_corrupted_sound.js index 1dd57099..90ae37ec 100644 --- a/packages/vm/test/integration/sb3_corrupted_sound.js +++ b/packages/vm/test/integration/sb3_corrupted_sound.js @@ -12,7 +12,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import md5 from 'js-md5'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {extractAsset, readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import {serializeSounds} from '../../src/serialization/serialize-assets.js'; const projectUri = path.resolve(__dirname, '../fixtures/corrupt_sound.sb3'); diff --git a/packages/vm/test/integration/sb3_corrupted_svg.js b/packages/vm/test/integration/sb3_corrupted_svg.js index 3de25b13..a40b449c 100644 --- a/packages/vm/test/integration/sb3_corrupted_svg.js +++ b/packages/vm/test/integration/sb3_corrupted_svg.js @@ -13,7 +13,7 @@ import md5 from 'js-md5'; import makeTestStorage from '../fixtures/make-test-storage.js'; import FakeRenderer from '../fixtures/fake-renderer.js'; import {extractAsset, readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; const projectUri = path.resolve(__dirname, '../fixtures/corrupt_svg.sb3'); diff --git a/packages/vm/test/integration/sb3_missing_png.js b/packages/vm/test/integration/sb3_missing_png.js index f083f25d..3da005f3 100644 --- a/packages/vm/test/integration/sb3_missing_png.js +++ b/packages/vm/test/integration/sb3_missing_png.js @@ -13,7 +13,7 @@ import makeTestStorage from '../fixtures/make-test-storage.js'; import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; const projectUri = path.resolve(__dirname, '../fixtures/missing_png.sb3'); diff --git a/packages/vm/test/integration/sb3_missing_sound.js b/packages/vm/test/integration/sb3_missing_sound.js index a2b50256..68da36ff 100644 --- a/packages/vm/test/integration/sb3_missing_sound.js +++ b/packages/vm/test/integration/sb3_missing_sound.js @@ -10,7 +10,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import {serializeSounds} from '../../src/serialization/serialize-assets.js'; const projectUri = path.resolve(__dirname, '../fixtures/missing_sound.sb3'); diff --git a/packages/vm/test/integration/sb3_missing_svg.js b/packages/vm/test/integration/sb3_missing_svg.js index 28eed654..261904f4 100644 --- a/packages/vm/test/integration/sb3_missing_svg.js +++ b/packages/vm/test/integration/sb3_missing_svg.js @@ -12,7 +12,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import FakeRenderer from '../fixtures/fake-renderer.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; const projectUri = path.resolve(__dirname, '../fixtures/missing_svg.sb3'); diff --git a/packages/vm/test/integration/sensing.js b/packages/vm/test/integration/sensing.js index 901f9b60..79955daf 100644 --- a/packages/vm/test/integration/sensing.js +++ b/packages/vm/test/integration/sensing.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const uri = path.resolve(__dirname, '../fixtures/sensing.sb2'); const project = readFileToBuffer(uri); diff --git a/packages/vm/test/integration/sound.js b/packages/vm/test/integration/sound.js index 2fe116e6..72e3c784 100644 --- a/packages/vm/test/integration/sound.js +++ b/packages/vm/test/integration/sound.js @@ -3,7 +3,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import dispatch from '../../src/dispatch/central-dispatch'; const uri = path.resolve(__dirname, '../fixtures/sound.sb2'); diff --git a/packages/vm/test/integration/sprite2_corrupted_png.js b/packages/vm/test/integration/sprite2_corrupted_png.js index f48500b4..dbbc8655 100644 --- a/packages/vm/test/integration/sprite2_corrupted_png.js +++ b/packages/vm/test/integration/sprite2_corrupted_png.js @@ -15,7 +15,7 @@ import makeTestStorage from '../fixtures/make-test-storage.js'; import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {extractAsset, readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; const projectUri = path.resolve(__dirname, '../fixtures/default.sb3'); diff --git a/packages/vm/test/integration/sprite2_corrupted_svg.js b/packages/vm/test/integration/sprite2_corrupted_svg.js index 216df237..873f55ce 100644 --- a/packages/vm/test/integration/sprite2_corrupted_svg.js +++ b/packages/vm/test/integration/sprite2_corrupted_svg.js @@ -15,7 +15,7 @@ import makeTestStorage from '../fixtures/make-test-storage.js'; import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {extractAsset, readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; const projectUri = path.resolve(__dirname, '../fixtures/default.sb3'); diff --git a/packages/vm/test/integration/sprite2_missing_png.js b/packages/vm/test/integration/sprite2_missing_png.js index bfbdd220..ec476f5a 100644 --- a/packages/vm/test/integration/sprite2_missing_png.js +++ b/packages/vm/test/integration/sprite2_missing_png.js @@ -13,7 +13,7 @@ import makeTestStorage from '../fixtures/make-test-storage.js'; import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; // The particular project that we're loading doesn't matter for this test diff --git a/packages/vm/test/integration/sprite2_missing_svg.js b/packages/vm/test/integration/sprite2_missing_svg.js index ca94e3b7..84875d5b 100644 --- a/packages/vm/test/integration/sprite2_missing_svg.js +++ b/packages/vm/test/integration/sprite2_missing_svg.js @@ -13,7 +13,7 @@ import makeTestStorage from '../fixtures/make-test-storage.js'; import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; // The particular project that we're loading doesn't matter for this test diff --git a/packages/vm/test/integration/sprite3_corrupted_png.js b/packages/vm/test/integration/sprite3_corrupted_png.js index 1cafa522..5cce84f3 100644 --- a/packages/vm/test/integration/sprite3_corrupted_png.js +++ b/packages/vm/test/integration/sprite3_corrupted_png.js @@ -15,7 +15,7 @@ import makeTestStorage from '../fixtures/make-test-storage.js'; import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {extractAsset, readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; const projectUri = path.resolve(__dirname, '../fixtures/default.sb3'); diff --git a/packages/vm/test/integration/sprite3_corrupted_svg.js b/packages/vm/test/integration/sprite3_corrupted_svg.js index 4744e287..2c240237 100644 --- a/packages/vm/test/integration/sprite3_corrupted_svg.js +++ b/packages/vm/test/integration/sprite3_corrupted_svg.js @@ -14,7 +14,7 @@ import md5 from 'js-md5'; import makeTestStorage from '../fixtures/make-test-storage.js'; import FakeRenderer from '../fixtures/fake-renderer.js'; import {extractAsset, readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; const projectUri = path.resolve(__dirname, '../fixtures/default.sb3'); diff --git a/packages/vm/test/integration/sprite3_missing_png.js b/packages/vm/test/integration/sprite3_missing_png.js index e9039911..39f1ad2e 100644 --- a/packages/vm/test/integration/sprite3_missing_png.js +++ b/packages/vm/test/integration/sprite3_missing_png.js @@ -13,7 +13,7 @@ import makeTestStorage from '../fixtures/make-test-storage.js'; import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; // The particular project that we're loading doesn't matter for this test diff --git a/packages/vm/test/integration/sprite3_missing_svg.js b/packages/vm/test/integration/sprite3_missing_svg.js index dbb585eb..6d997aba 100644 --- a/packages/vm/test/integration/sprite3_missing_svg.js +++ b/packages/vm/test/integration/sprite3_missing_svg.js @@ -12,7 +12,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import FakeRenderer from '../fixtures/fake-renderer.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; // The particular project that we're loading doesn't matter for this test diff --git a/packages/vm/test/integration/stack-click.js b/packages/vm/test/integration/stack-click.js index 6ca5d780..15074080 100644 --- a/packages/vm/test/integration/stack-click.js +++ b/packages/vm/test/integration/stack-click.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const projectUri = path.resolve(__dirname, '../fixtures/stack-click.sb2'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/unknown-opcode-as-reporter-block.js b/packages/vm/test/integration/unknown-opcode-as-reporter-block.js index e4847a00..909b5b9f 100644 --- a/packages/vm/test/integration/unknown-opcode-as-reporter-block.js +++ b/packages/vm/test/integration/unknown-opcode-as-reporter-block.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const uri = path.resolve(__dirname, '../fixtures/unknown-opcode-as-reporter-block.sb2'); const project = readFileToBuffer(uri); diff --git a/packages/vm/test/integration/unknown-opcode-in-c-block.js b/packages/vm/test/integration/unknown-opcode-in-c-block.js index 0e63f723..4cd6c74f 100644 --- a/packages/vm/test/integration/unknown-opcode-in-c-block.js +++ b/packages/vm/test/integration/unknown-opcode-in-c-block.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const uri = path.resolve(__dirname, '../fixtures/unknown-opcode-in-c-block.sb2'); const project = readFileToBuffer(uri); diff --git a/packages/vm/test/integration/unknown-opcode.js b/packages/vm/test/integration/unknown-opcode.js index 228a0603..abdc74eb 100644 --- a/packages/vm/test/integration/unknown-opcode.js +++ b/packages/vm/test/integration/unknown-opcode.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const uri = path.resolve(__dirname, '../fixtures/unknown-opcode.sb2'); const project = readFileToBuffer(uri); diff --git a/packages/vm/test/integration/variable_monitor_reset.js b/packages/vm/test/integration/variable_monitor_reset.js index 7ef7becd..d3d48f11 100644 --- a/packages/vm/test/integration/variable_monitor_reset.js +++ b/packages/vm/test/integration/variable_monitor_reset.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; const projectUri = path.resolve(__dirname, '../fixtures/monitored_variables.sb3'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/variable_special_chars_sb2.js b/packages/vm/test/integration/variable_special_chars_sb2.js index 0faebc9e..a852d45b 100644 --- a/packages/vm/test/integration/variable_special_chars_sb2.js +++ b/packages/vm/test/integration/variable_special_chars_sb2.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import Variable from '../../src/engine/variable'; import StringUtil from '../../src/util/string-util'; import VariableUtil from '../../src/util/variable-util'; diff --git a/packages/vm/test/integration/variable_special_chars_sb3.js b/packages/vm/test/integration/variable_special_chars_sb3.js index 3b327da7..ba9c8534 100644 --- a/packages/vm/test/integration/variable_special_chars_sb3.js +++ b/packages/vm/test/integration/variable_special_chars_sb3.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import Variable from '../../src/engine/variable'; import StringUtil from '../../src/util/string-util'; import VariableUtil from '../../src/util/variable-util'; diff --git a/packages/vm/test/unit/blocks_motion.js b/packages/vm/test/unit/blocks_motion.js index 8dd11daf..7bc4fd5e 100644 --- a/packages/vm/test/unit/blocks_motion.js +++ b/packages/vm/test/unit/blocks_motion.js @@ -3,7 +3,7 @@ import Motion from '../../src/blocks/scratch3_motion'; import Runtime from '../../src/engine/runtime.js'; import Sprite from '../../src/sprites/sprite'; import RenderedTarget from '../../src/sprites/rendered-target.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; test('getPrimitives', t => { const rt = new Runtime(); diff --git a/packages/vm/test/unit/serialization_sb3.js b/packages/vm/test/unit/serialization_sb3.js index 42e9338b..6de61a41 100644 --- a/packages/vm/test/unit/serialization_sb3.js +++ b/packages/vm/test/unit/serialization_sb3.js @@ -1,6 +1,6 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import path from 'path'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; import Runtime from '../../src/engine/runtime.js'; import * as sb3 from '../../src/serialization/sb3.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; diff --git a/packages/vm/test/unit/spec.js b/packages/vm/test/unit/spec.js index cd188093..44281922 100644 --- a/packages/vm/test/unit/spec.js +++ b/packages/vm/test/unit/spec.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import VirtualMachine from '../../src/index.js'; +import VirtualMachine from '../../src/index'; test('interface', t => { const vm = new VirtualMachine(); diff --git a/packages/vm/webpack.config.js b/packages/vm/webpack.config.js index c5c9e2f8..1eb06506 100644 --- a/packages/vm/webpack.config.js +++ b/packages/vm/webpack.config.js @@ -72,8 +72,8 @@ module.exports = [ defaultsDeep({}, base, { target: 'web', entry: { - 'scratch-vm': './src/index.js', - 'scratch-vm.min': './src/index.js' + 'scratch-vm': './src/index.ts', + 'scratch-vm.min': './src/index.ts' }, output: { library: { @@ -87,7 +87,7 @@ module.exports = [ defaultsDeep({}, base, { target: 'node', entry: { - 'scratch-vm': './src/index.js' + 'scratch-vm': './src/index.ts' }, output: { library: { From c57f13607abdff56e30af204e6ad0d06f7b681ef Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Mon, 11 May 2026 22:09:01 +0800 Subject: [PATCH 30/30] :wrench: chore(vm): fix build Signed-off-by: SimonShiki --- packages/vm/src/sprites/sprite.ts | 2 +- packages/vm/src/types/global.d.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/vm/src/sprites/sprite.ts b/packages/vm/src/sprites/sprite.ts index 638c12b4..35497584 100644 --- a/packages/vm/src/sprites/sprite.ts +++ b/packages/vm/src/sprites/sprite.ts @@ -99,7 +99,7 @@ class Sprite { * @param costumeObject Object representing the costume. * @param index Index at which to add costume */ - addCostumeAt (costumeObject: Costume, index: int) { + addCostumeAt (costumeObject: Costume, index: number) { if (!costumeObject.name) { costumeObject.name = ''; } diff --git a/packages/vm/src/types/global.d.ts b/packages/vm/src/types/global.d.ts index 146675be..6cca7d18 100644 --- a/packages/vm/src/types/global.d.ts +++ b/packages/vm/src/types/global.d.ts @@ -3,7 +3,6 @@ declare module 'decode-html' { export default decodeHtml; } -type int = number; /** * Compile-time injected clipcc global metadata */