@scratch/scratch-vm 13.7.0 → 13.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scratch/scratch-vm",
3
- "version": "13.7.0",
3
+ "version": "13.7.1",
4
4
  "description": "Virtual Machine for Scratch 3.0",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/scratchfoundation/scratch-vm#readme",
@@ -60,8 +60,8 @@
60
60
  "allow-incomplete-coverage": true
61
61
  },
62
62
  "dependencies": {
63
- "@scratch/scratch-render": "13.7.0",
64
- "@scratch/scratch-svg-renderer": "13.7.0",
63
+ "@scratch/scratch-render": "13.7.1",
64
+ "@scratch/scratch-svg-renderer": "13.7.1",
65
65
  "@vernier/godirect": "1.8.3",
66
66
  "arraybuffer-loader": "1.0.8",
67
67
  "atob": "2.1.2",
@@ -91,7 +91,7 @@
91
91
  "copy-webpack-plugin": "6.4.1",
92
92
  "docdash": "1.2.0",
93
93
  "eslint": "9.39.4",
94
- "eslint-config-scratch": "14.1.10",
94
+ "eslint-config-scratch": "14.1.12",
95
95
  "expose-loader": "1.0.3",
96
96
  "file-loader": "6.2.0",
97
97
  "format-message-cli": "6.2.4",
@@ -99,15 +99,15 @@
99
99
  "in-publish": "2.0.1",
100
100
  "js-md5": "0.7.3",
101
101
  "pngjs": "3.4.0",
102
- "scratch-blocks": "2.1.14",
103
- "scratch-l10n": "6.1.70",
102
+ "scratch-blocks": "2.1.17",
103
+ "scratch-l10n": "6.1.72",
104
104
  "scratch-render-fonts": "1.0.252",
105
105
  "scratch-semantic-release-config": "4.0.1",
106
106
  "scratch-webpack-configuration": "3.1.2",
107
107
  "script-loader": "0.7.2",
108
108
  "semantic-release": "25.0.3",
109
109
  "stats.js": "0.17.0",
110
- "tap": "21.7.0",
110
+ "tap": "21.7.1",
111
111
  "tiny-worker": "2.3.0",
112
112
  "typedoc": "0.28.18",
113
113
  "webpack": "5.106.2",
@@ -775,8 +775,15 @@ class Blocks {
775
775
  // this input, or null out the input's block.
776
776
  const shadow = oldParent.inputs[e.oldInput].shadow;
777
777
  if (shadow && e.id !== shadow) {
778
- oldParent.inputs[e.oldInput].block = shadow;
779
- this._blocks[shadow].parent = oldParent.id;
778
+ if (this._blocks[shadow]) {
779
+ oldParent.inputs[e.oldInput].block = shadow;
780
+ this._blocks[shadow].parent = oldParent.id;
781
+ } else {
782
+ // Shadow block is referenced but missing — clear
783
+ // the stale reference rather than crashing.
784
+ oldParent.inputs[e.oldInput].block = null;
785
+ oldParent.inputs[e.oldInput].shadow = null;
786
+ }
780
787
  this._blocks[e.id].parent = null;
781
788
  } else {
782
789
  oldParent.inputs[e.oldInput].block = null;
@@ -95,6 +95,85 @@ const primitiveOpcodeInfoMap = {
95
95
  data_listcontents: [LIST_PRIMITIVE, 'LIST']
96
96
  };
97
97
 
98
+ /**
99
+ * Build the fields object for a replacement shadow block. Simple primitives
100
+ * (text, numbers, colours) just need {name, value}. Variable, list, and
101
+ * broadcast shadows also need {id, variableType} for the dropdown to work.
102
+ * @param {string} shadowOpcode The shadow block's opcode.
103
+ * @param {?object} template A peer shadow block to copy field values from, or null.
104
+ * @returns {?object} A fields object for the new shadow, or null if the opcode is unknown.
105
+ */
106
+ const buildShadowFields = function (shadowOpcode, template) {
107
+ const info = primitiveOpcodeInfoMap[shadowOpcode];
108
+ if (!info) return null;
109
+ const fieldName = info[1];
110
+ const templateField = template && template.fields && template.fields[fieldName];
111
+ const value = templateField ? templateField.value : '';
112
+
113
+ switch (shadowOpcode) {
114
+ case 'event_broadcast_menu':
115
+ return {
116
+ [fieldName]: {
117
+ name: fieldName,
118
+ value: value,
119
+ id: (templateField && templateField.id) || '',
120
+ variableType: Variable.BROADCAST_MESSAGE_TYPE
121
+ }
122
+ };
123
+ case 'data_variable':
124
+ return {
125
+ [fieldName]: {
126
+ name: fieldName,
127
+ value: value,
128
+ id: (templateField && templateField.id) || '',
129
+ variableType: Variable.SCALAR_TYPE
130
+ }
131
+ };
132
+ case 'data_listcontents':
133
+ return {
134
+ [fieldName]: {
135
+ name: fieldName,
136
+ value: value,
137
+ id: (templateField && templateField.id) || '',
138
+ variableType: Variable.LIST_TYPE
139
+ }
140
+ };
141
+ default:
142
+ // Simple primitives: text, math_number, math_angle, colour_picker, etc.
143
+ return {
144
+ [fieldName]: {
145
+ name: fieldName,
146
+ value: value
147
+ }
148
+ };
149
+ }
150
+ };
151
+
152
+ /**
153
+ * Find a working shadow block for the given (opcode, inputName) pair by
154
+ * looking at other blocks of the same opcode in the project. Returns the
155
+ * matching shadow block from the blocks map for use as a template, or
156
+ * null if no peer has a working shadow for that input.
157
+ * @param {object} blocks The full blocks map for the target.
158
+ * @param {string} opcode The parent block's opcode.
159
+ * @param {string} inputName The name of the input.
160
+ * @returns {?object} A template shadow block from the blocks map, or null.
161
+ */
162
+ const findPeerShadow = function (blocks, opcode, inputName) {
163
+ for (const peerId in blocks) {
164
+ if (!hasOwnProperty.call(blocks, peerId)) continue;
165
+ const peer = blocks[peerId];
166
+ if (Array.isArray(peer) || peer.opcode !== opcode) continue;
167
+ const peerInput = peer.inputs[inputName];
168
+ if (!peerInput || !peerInput.shadow) continue;
169
+ const shadowBlock = blocks[peerInput.shadow];
170
+ if (!shadowBlock || Array.isArray(shadowBlock)) continue;
171
+ if (!hasOwnProperty.call(primitiveOpcodeInfoMap, shadowBlock.opcode)) continue;
172
+ return shadowBlock;
173
+ }
174
+ return null;
175
+ };
176
+
98
177
  /**
99
178
  * Serializes primitives described above into a more compact format
100
179
  * @param {object} block the block to serialize
@@ -885,6 +964,73 @@ const deserializeBlocks = function (blocks) {
885
964
  block.inputs = {};
886
965
  }
887
966
 
967
+ // Third pass: repair missing or broken shadow blocks. Shadows can be
968
+ // missing in two ways:
969
+ // 1. shadow is a stale ID pointing to a nonexistent block
970
+ // 2. shadow is null when it shouldn't be (lost before save)
971
+ // Both are leftovers from a serialization bug where shadow blocks were
972
+ // dropped during save. Recreate the shadow by finding a peer block of
973
+ // the same opcode with an intact shadow on the same input (peer lookup).
974
+ // For broken references (case 1), fall back to a text shadow if no peer
975
+ // is available. For missing shadows (case 2), only create a shadow if a
976
+ // peer confirms one should exist — otherwise the input probably doesn't
977
+ // use shadows (e.g. statement inputs like SUBSTACK).
978
+ // Cache peer lookups per (opcode, inputName) to avoid repeated O(n) scans.
979
+ const peerShadowCache = Object.create(null);
980
+ for (const blockId in blocks) {
981
+ if (!Object.prototype.hasOwnProperty.call(blocks, blockId)) continue;
982
+ const block = blocks[blockId];
983
+ if (Array.isArray(block)) continue;
984
+ for (const inputName in block.inputs) {
985
+ if (!Object.prototype.hasOwnProperty.call(block.inputs, inputName)) continue;
986
+ const input = block.inputs[inputName];
987
+
988
+ const shadowIsBroken = input.shadow &&
989
+ !Object.prototype.hasOwnProperty.call(blocks, input.shadow);
990
+ const shadowIsMissing = !input.shadow && input.block &&
991
+ Object.prototype.hasOwnProperty.call(blocks, input.block) &&
992
+ !blocks[input.block].shadow;
993
+
994
+ if (!shadowIsBroken && !shadowIsMissing) continue;
995
+
996
+ // Try to find a peer block with a working shadow for this input.
997
+ const cacheKey = `${block.opcode}/${inputName}`;
998
+ if (!Object.prototype.hasOwnProperty.call(peerShadowCache, cacheKey)) {
999
+ peerShadowCache[cacheKey] = findPeerShadow(blocks, block.opcode, inputName);
1000
+ }
1001
+ const template = peerShadowCache[cacheKey];
1002
+ if (!template && !shadowIsBroken) {
1003
+ // No peer has a shadow either — this input probably doesn't
1004
+ // use one (e.g. custom procedure inputs). Leave it alone.
1005
+ continue;
1006
+ }
1007
+ const shadowOpcode = template ? template.opcode : 'text';
1008
+ const fields = buildShadowFields(shadowOpcode, template);
1009
+ if (!fields) {
1010
+ // Unknown shadow type — clear any stale reference
1011
+ // so it doesn't cause crashes.
1012
+ if (shadowIsBroken) input.shadow = null;
1013
+ continue;
1014
+ }
1015
+ const newShadowId = uid();
1016
+ blocks[newShadowId] = {
1017
+ id: newShadowId,
1018
+ opcode: shadowOpcode,
1019
+ next: null,
1020
+ parent: blockId,
1021
+ shadow: true,
1022
+ topLevel: false,
1023
+ inputs: Object.create(null),
1024
+ fields: fields
1025
+ };
1026
+ input.shadow = newShadowId;
1027
+ // If no block is connected, also set block to the shadow
1028
+ if (!input.block || !Object.prototype.hasOwnProperty.call(blocks, input.block)) {
1029
+ input.block = newShadowId;
1030
+ }
1031
+ }
1032
+ }
1033
+
888
1034
  return blocks;
889
1035
  };
890
1036