@scratch/scratch-vm 11.0.0-beta.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/.jsdoc.json +20 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +7004 -0
- package/LICENSE +12 -0
- package/README.md +120 -0
- package/TRADEMARK +1 -0
- package/commitlint.config.js +4 -0
- package/docs/extensions.md +527 -0
- package/package.json +103 -0
- package/release.config.js +13 -0
- package/src/blocks/scratch3_control.js +206 -0
- package/src/blocks/scratch3_core_example.js +69 -0
- package/src/blocks/scratch3_data.js +257 -0
- package/src/blocks/scratch3_event.js +137 -0
- package/src/blocks/scratch3_looks.js +612 -0
- package/src/blocks/scratch3_motion.js +288 -0
- package/src/blocks/scratch3_operators.js +154 -0
- package/src/blocks/scratch3_procedures.js +79 -0
- package/src/blocks/scratch3_sensing.js +336 -0
- package/src/blocks/scratch3_sound.js +350 -0
- package/src/dispatch/central-dispatch.js +143 -0
- package/src/dispatch/shared-dispatch.js +235 -0
- package/src/dispatch/worker-dispatch.js +110 -0
- package/src/engine/adapter.js +176 -0
- package/src/engine/block-utility.js +242 -0
- package/src/engine/blocks-execute-cache.js +19 -0
- package/src/engine/blocks-runtime-cache.js +78 -0
- package/src/engine/blocks.js +1299 -0
- package/src/engine/comment.js +56 -0
- package/src/engine/execute.js +580 -0
- package/src/engine/monitor-record.js +23 -0
- package/src/engine/mutation-adapter.js +48 -0
- package/src/engine/profiler.js +390 -0
- package/src/engine/runtime.js +2686 -0
- package/src/engine/scratch-blocks-constants.js +27 -0
- package/src/engine/sequencer.js +361 -0
- package/src/engine/stage-layering.js +29 -0
- package/src/engine/target.js +799 -0
- package/src/engine/thread.js +404 -0
- package/src/engine/variable.js +70 -0
- package/src/extension-support/argument-type.js +47 -0
- package/src/extension-support/block-type.js +50 -0
- package/src/extension-support/define-messages.js +18 -0
- package/src/extension-support/extension-manager.js +440 -0
- package/src/extension-support/extension-metadata.js +64 -0
- package/src/extension-support/extension-worker.js +59 -0
- package/src/extension-support/reporter-scope.js +18 -0
- package/src/extension-support/target-type.js +17 -0
- package/src/extensions/scratch3_boost/index.js +2113 -0
- package/src/extensions/scratch3_ev3/index.js +1355 -0
- package/src/extensions/scratch3_gdx_for/index.js +981 -0
- package/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js +44 -0
- package/src/extensions/scratch3_makeymakey/index.js +396 -0
- package/src/extensions/scratch3_microbit/index.js +984 -0
- package/src/extensions/scratch3_music/assets/drums/1-snare.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/10-wood-block.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/11-cowbell.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/12-triangle.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/13-bongo.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/14-conga.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/15-cabasa.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/16-guiro.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/17-vibraslap.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/18-cuica.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/2-bass-drum.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/3-side-stick.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/4-crash-cymbal.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/5-open-hi-hat.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/6-closed-hi-hat.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/7-tambourine.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/8-hand-clap.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/9-claves.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/108.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/24.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/84.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/96.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/10-clarinet/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/10-clarinet/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/84.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/12-flute/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/12-flute/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/17-music-box/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/18-steel-drum/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/19-marimba/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/2-electric-piano/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/20-synth-lead/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/21-synth-pad/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/3-organ/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/4-guitar/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/5-electric-guitar/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/6-bass/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/6-bass/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/7-pizzicato/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/index.js +1333 -0
- package/src/extensions/scratch3_music/manifest.js +63 -0
- package/src/extensions/scratch3_pen/index.js +770 -0
- package/src/extensions/scratch3_speech2text/index.js +700 -0
- package/src/extensions/scratch3_text2speech/index.js +766 -0
- package/src/extensions/scratch3_translate/index.js +286 -0
- package/src/extensions/scratch3_video_sensing/debug.js +13 -0
- package/src/extensions/scratch3_video_sensing/index.js +594 -0
- package/src/extensions/scratch3_video_sensing/library.js +384 -0
- package/src/extensions/scratch3_video_sensing/math.js +76 -0
- package/src/extensions/scratch3_video_sensing/view.js +509 -0
- package/src/extensions/scratch3_wedo2/index.js +1616 -0
- package/src/import/load-costume.js +416 -0
- package/src/import/load-sound.js +116 -0
- package/src/index.js +10 -0
- package/src/io/ble.js +256 -0
- package/src/io/bt.js +202 -0
- package/src/io/clock.js +39 -0
- package/src/io/cloud.js +169 -0
- package/src/io/keyboard.js +146 -0
- package/src/io/mouse.js +145 -0
- package/src/io/mouseWheel.js +28 -0
- package/src/io/userData.js +24 -0
- package/src/io/video.js +210 -0
- package/src/playground/benchmark.css +83 -0
- package/src/playground/benchmark.js +753 -0
- package/src/playground/index.html +111 -0
- package/src/playground/suite.css +78 -0
- package/src/playground/suite.html +25 -0
- package/src/playground/suite.js +544 -0
- package/src/playground/video-sensing.html +18 -0
- package/src/playground/video-sensing.js +133 -0
- package/src/serialization/deserialize-assets.js +178 -0
- package/src/serialization/sb2.js +1295 -0
- package/src/serialization/sb2_specmap.js +1818 -0
- package/src/serialization/sb3.js +1320 -0
- package/src/serialization/serialize-assets.js +60 -0
- package/src/sprites/rendered-target.js +1115 -0
- package/src/sprites/sprite.js +177 -0
- package/src/util/base64-util.js +48 -0
- package/src/util/cast.js +218 -0
- package/src/util/clone.js +17 -0
- package/src/util/color.js +204 -0
- package/src/util/fetch-with-timeout.js +53 -0
- package/src/util/get-monitor-id.js +33 -0
- package/src/util/jsonrpc.js +114 -0
- package/src/util/log.js +4 -0
- package/src/util/math-util.js +122 -0
- package/src/util/maybe-format-message.js +18 -0
- package/src/util/new-block-ids.js +33 -0
- package/src/util/rateLimiter.js +73 -0
- package/src/util/scratch-link-websocket.js +133 -0
- package/src/util/string-util.js +94 -0
- package/src/util/task-queue.js +203 -0
- package/src/util/timer.js +103 -0
- package/src/util/uid.js +29 -0
- package/src/util/variable-util.js +48 -0
- package/src/util/xml-escape.js +33 -0
- package/src/virtual-machine.js +1586 -0
- package/webpack.config.js +134 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
const dispatch = require('../dispatch/central-dispatch');
|
|
2
|
+
const log = require('../util/log');
|
|
3
|
+
const maybeFormatMessage = require('../util/maybe-format-message');
|
|
4
|
+
|
|
5
|
+
const BlockType = require('./block-type');
|
|
6
|
+
|
|
7
|
+
// These extensions are currently built into the VM repository but should not be loaded at startup.
|
|
8
|
+
// TODO: move these out into a separate repository?
|
|
9
|
+
// TODO: change extension spec so that library info, including extension ID, can be collected through static methods
|
|
10
|
+
|
|
11
|
+
const builtinExtensions = {
|
|
12
|
+
// This is an example that isn't loaded with the other core blocks,
|
|
13
|
+
// but serves as a reference for loading core blocks as extensions.
|
|
14
|
+
coreExample: () => require('../blocks/scratch3_core_example'),
|
|
15
|
+
// These are the non-core built-in extensions.
|
|
16
|
+
pen: () => require('../extensions/scratch3_pen'),
|
|
17
|
+
wedo2: () => require('../extensions/scratch3_wedo2'),
|
|
18
|
+
music: () => require('../extensions/scratch3_music'),
|
|
19
|
+
microbit: () => require('../extensions/scratch3_microbit'),
|
|
20
|
+
text2speech: () => require('../extensions/scratch3_text2speech'),
|
|
21
|
+
translate: () => require('../extensions/scratch3_translate'),
|
|
22
|
+
videoSensing: () => require('../extensions/scratch3_video_sensing'),
|
|
23
|
+
ev3: () => require('../extensions/scratch3_ev3'),
|
|
24
|
+
makeymakey: () => require('../extensions/scratch3_makeymakey'),
|
|
25
|
+
boost: () => require('../extensions/scratch3_boost'),
|
|
26
|
+
gdxfor: () => require('../extensions/scratch3_gdx_for')
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {object} ArgumentInfo - Information about an extension block argument
|
|
31
|
+
* @property {ArgumentType} type - the type of value this argument can take
|
|
32
|
+
* @property {*|undefined} default - the default value of this argument (default: blank)
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {object} ConvertedBlockInfo - Raw extension block data paired with processed data ready for scratch-blocks
|
|
37
|
+
* @property {ExtensionBlockMetadata} info - the raw block info
|
|
38
|
+
* @property {object} json - the scratch-blocks JSON definition for this block
|
|
39
|
+
* @property {string} xml - the scratch-blocks XML definition for this block
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {object} CategoryInfo - Information about a block category
|
|
44
|
+
* @property {string} id - the unique ID of this category
|
|
45
|
+
* @property {string} name - the human-readable name of this category
|
|
46
|
+
* @property {string|undefined} blockIconURI - optional URI for the block icon image
|
|
47
|
+
* @property {string} color1 - the primary color for this category, in '#rrggbb' format
|
|
48
|
+
* @property {string} color2 - the secondary color for this category, in '#rrggbb' format
|
|
49
|
+
* @property {string} color3 - the tertiary color for this category, in '#rrggbb' format
|
|
50
|
+
* @property {Array.<ConvertedBlockInfo>} blocks - the blocks, separators, etc. in this category
|
|
51
|
+
* @property {Array.<object>} menus - the menus provided by this category
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {object} PendingExtensionWorker - Information about an extension worker still initializing
|
|
56
|
+
* @property {string} extensionURL - the URL of the extension to be loaded by this worker
|
|
57
|
+
* @property {Function} resolve - function to call on successful worker startup
|
|
58
|
+
* @property {Function} reject - function to call on failed worker startup
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
class ExtensionManager {
|
|
62
|
+
constructor (runtime) {
|
|
63
|
+
/**
|
|
64
|
+
* The ID number to provide to the next extension worker.
|
|
65
|
+
* @type {int}
|
|
66
|
+
*/
|
|
67
|
+
this.nextExtensionWorker = 0;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* FIFO queue of extensions which have been requested but not yet loaded in a worker,
|
|
71
|
+
* along with promise resolution functions to call once the worker is ready or failed.
|
|
72
|
+
*
|
|
73
|
+
* @type {Array.<PendingExtensionWorker>}
|
|
74
|
+
*/
|
|
75
|
+
this.pendingExtensions = [];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Map of worker ID to workers which have been allocated but have not yet finished initialization.
|
|
79
|
+
* @type {Array.<PendingExtensionWorker>}
|
|
80
|
+
*/
|
|
81
|
+
this.pendingWorkers = [];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Map of loaded extension URLs/IDs (equivalent for built-in extensions) to service name.
|
|
85
|
+
* @type {Map.<string,string>}
|
|
86
|
+
* @private
|
|
87
|
+
*/
|
|
88
|
+
this._loadedExtensions = new Map();
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Keep a reference to the runtime so we can construct internal extension objects.
|
|
92
|
+
* TODO: remove this in favor of extensions accessing the runtime as a service.
|
|
93
|
+
* @type {Runtime}
|
|
94
|
+
*/
|
|
95
|
+
this.runtime = runtime;
|
|
96
|
+
|
|
97
|
+
dispatch.setService('extensions', this).catch(e => {
|
|
98
|
+
log.error(`ExtensionManager was unable to register extension service: ${JSON.stringify(e)}`);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check whether an extension is registered or is in the process of loading. This is intended to control loading or
|
|
104
|
+
* adding extensions so it may return `true` before the extension is ready to be used. Use the promise returned by
|
|
105
|
+
* `loadExtensionURL` if you need to wait until the extension is truly ready.
|
|
106
|
+
* @param {string} extensionID - the ID of the extension.
|
|
107
|
+
* @returns {boolean} - true if loaded, false otherwise.
|
|
108
|
+
*/
|
|
109
|
+
isExtensionLoaded (extensionID) {
|
|
110
|
+
return this._loadedExtensions.has(extensionID);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Synchronously load an internal extension (core or non-core) by ID. This call will
|
|
115
|
+
* fail if the provided id is not does not match an internal extension.
|
|
116
|
+
* @param {string} extensionId - the ID of an internal extension
|
|
117
|
+
*/
|
|
118
|
+
loadExtensionIdSync (extensionId) {
|
|
119
|
+
if (!Object.prototype.hasOwnProperty.call(builtinExtensions, extensionId)) {
|
|
120
|
+
log.warn(`Could not find extension ${extensionId} in the built in extensions.`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** @TODO dupe handling for non-builtin extensions. See commit 670e51d33580e8a2e852b3b038bb3afc282f81b9 */
|
|
125
|
+
if (this.isExtensionLoaded(extensionId)) {
|
|
126
|
+
const message = `Rejecting attempt to load a second extension with ID ${extensionId}`;
|
|
127
|
+
log.warn(message);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const extension = builtinExtensions[extensionId]();
|
|
132
|
+
const extensionInstance = new extension(this.runtime);
|
|
133
|
+
const serviceName = this._registerInternalExtension(extensionInstance);
|
|
134
|
+
this._loadedExtensions.set(extensionId, serviceName);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Load an extension by URL or internal extension ID
|
|
139
|
+
* @param {string} extensionURL - the URL for the extension to load OR the ID of an internal extension
|
|
140
|
+
* @returns {Promise} resolved once the extension is loaded and initialized or rejected on failure
|
|
141
|
+
*/
|
|
142
|
+
loadExtensionURL (extensionURL) {
|
|
143
|
+
if (Object.prototype.hasOwnProperty.call(builtinExtensions, extensionURL)) {
|
|
144
|
+
/** @TODO dupe handling for non-builtin extensions. See commit 670e51d33580e8a2e852b3b038bb3afc282f81b9 */
|
|
145
|
+
if (this.isExtensionLoaded(extensionURL)) {
|
|
146
|
+
const message = `Rejecting attempt to load a second extension with ID ${extensionURL}`;
|
|
147
|
+
log.warn(message);
|
|
148
|
+
return Promise.resolve();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const extension = builtinExtensions[extensionURL]();
|
|
152
|
+
const extensionInstance = new extension(this.runtime);
|
|
153
|
+
const serviceName = this._registerInternalExtension(extensionInstance);
|
|
154
|
+
this._loadedExtensions.set(extensionURL, serviceName);
|
|
155
|
+
return Promise.resolve();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return new Promise((resolve, reject) => {
|
|
159
|
+
// If we `require` this at the global level it breaks non-webpack targets, including tests
|
|
160
|
+
const worker = new Worker('./extension-worker.js');
|
|
161
|
+
|
|
162
|
+
this.pendingExtensions.push({extensionURL, resolve, reject});
|
|
163
|
+
dispatch.addWorker(worker);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Regenerate blockinfo for any loaded extensions
|
|
169
|
+
* @returns {Promise} resolved once all the extensions have been reinitialized
|
|
170
|
+
*/
|
|
171
|
+
refreshBlocks () {
|
|
172
|
+
const allPromises = Array.from(this._loadedExtensions.values()).map(serviceName =>
|
|
173
|
+
dispatch.call(serviceName, 'getInfo')
|
|
174
|
+
.then(info => {
|
|
175
|
+
info = this._prepareExtensionInfo(serviceName, info);
|
|
176
|
+
dispatch.call('runtime', '_refreshExtensionPrimitives', info);
|
|
177
|
+
})
|
|
178
|
+
.catch(e => {
|
|
179
|
+
log.error(`Failed to refresh built-in extension primitives: ${JSON.stringify(e)}`);
|
|
180
|
+
})
|
|
181
|
+
);
|
|
182
|
+
return Promise.all(allPromises);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
allocateWorker () {
|
|
186
|
+
const id = this.nextExtensionWorker++;
|
|
187
|
+
const workerInfo = this.pendingExtensions.shift();
|
|
188
|
+
this.pendingWorkers[id] = workerInfo;
|
|
189
|
+
return [id, workerInfo.extensionURL];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Synchronously collect extension metadata from the specified service and begin the extension registration process.
|
|
194
|
+
* @param {string} serviceName - the name of the service hosting the extension.
|
|
195
|
+
*/
|
|
196
|
+
registerExtensionServiceSync (serviceName) {
|
|
197
|
+
const info = dispatch.callSync(serviceName, 'getInfo');
|
|
198
|
+
this._registerExtensionInfo(serviceName, info);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Collect extension metadata from the specified service and begin the extension registration process.
|
|
203
|
+
* @param {string} serviceName - the name of the service hosting the extension.
|
|
204
|
+
*/
|
|
205
|
+
registerExtensionService (serviceName) {
|
|
206
|
+
dispatch.call(serviceName, 'getInfo').then(info => {
|
|
207
|
+
this._registerExtensionInfo(serviceName, info);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Called by an extension worker to indicate that the worker has finished initialization.
|
|
213
|
+
* @param {int} id - the worker ID.
|
|
214
|
+
* @param {*?} e - the error encountered during initialization, if any.
|
|
215
|
+
*/
|
|
216
|
+
onWorkerInit (id, e) {
|
|
217
|
+
const workerInfo = this.pendingWorkers[id];
|
|
218
|
+
delete this.pendingWorkers[id];
|
|
219
|
+
if (e) {
|
|
220
|
+
workerInfo.reject(e);
|
|
221
|
+
} else {
|
|
222
|
+
workerInfo.resolve(id);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Register an internal (non-Worker) extension object
|
|
228
|
+
* @param {object} extensionObject - the extension object to register
|
|
229
|
+
* @returns {string} The name of the registered extension service
|
|
230
|
+
*/
|
|
231
|
+
_registerInternalExtension (extensionObject) {
|
|
232
|
+
const extensionInfo = extensionObject.getInfo();
|
|
233
|
+
const fakeWorkerId = this.nextExtensionWorker++;
|
|
234
|
+
const serviceName = `extension_${fakeWorkerId}_${extensionInfo.id}`;
|
|
235
|
+
dispatch.setServiceSync(serviceName, extensionObject);
|
|
236
|
+
dispatch.callSync('extensions', 'registerExtensionServiceSync', serviceName);
|
|
237
|
+
return serviceName;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Sanitize extension info then register its primitives with the VM.
|
|
242
|
+
* @param {string} serviceName - the name of the service hosting the extension
|
|
243
|
+
* @param {ExtensionInfo} extensionInfo - the extension's metadata
|
|
244
|
+
* @private
|
|
245
|
+
*/
|
|
246
|
+
_registerExtensionInfo (serviceName, extensionInfo) {
|
|
247
|
+
extensionInfo = this._prepareExtensionInfo(serviceName, extensionInfo);
|
|
248
|
+
dispatch.call('runtime', '_registerExtensionPrimitives', extensionInfo).catch(e => {
|
|
249
|
+
log.error(`Failed to register primitives for extension on service ${serviceName}:`, e);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Modify the provided text as necessary to ensure that it may be used as an attribute value in valid XML.
|
|
255
|
+
* @param {string} text - the text to be sanitized
|
|
256
|
+
* @returns {string} - the sanitized text
|
|
257
|
+
* @private
|
|
258
|
+
*/
|
|
259
|
+
_sanitizeID (text) {
|
|
260
|
+
return text.toString().replace(/[<"&]/, '_');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Apply minor cleanup and defaults for optional extension fields.
|
|
265
|
+
* TODO: make the ID unique in cases where two copies of the same extension are loaded.
|
|
266
|
+
* @param {string} serviceName - the name of the service hosting this extension block
|
|
267
|
+
* @param {ExtensionInfo} extensionInfo - the extension info to be sanitized
|
|
268
|
+
* @returns {ExtensionInfo} - a new extension info object with cleaned-up values
|
|
269
|
+
* @private
|
|
270
|
+
*/
|
|
271
|
+
_prepareExtensionInfo (serviceName, extensionInfo) {
|
|
272
|
+
extensionInfo = Object.assign({}, extensionInfo);
|
|
273
|
+
if (!/^[a-z0-9]+$/i.test(extensionInfo.id)) {
|
|
274
|
+
throw new Error('Invalid extension id');
|
|
275
|
+
}
|
|
276
|
+
extensionInfo.name = extensionInfo.name || extensionInfo.id;
|
|
277
|
+
extensionInfo.blocks = extensionInfo.blocks || [];
|
|
278
|
+
extensionInfo.targetTypes = extensionInfo.targetTypes || [];
|
|
279
|
+
extensionInfo.blocks = extensionInfo.blocks.reduce((results, blockInfo) => {
|
|
280
|
+
try {
|
|
281
|
+
let result;
|
|
282
|
+
switch (blockInfo) {
|
|
283
|
+
case '---': // separator
|
|
284
|
+
result = '---';
|
|
285
|
+
break;
|
|
286
|
+
default: // an ExtensionBlockMetadata object
|
|
287
|
+
result = this._prepareBlockInfo(serviceName, blockInfo);
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
results.push(result);
|
|
291
|
+
} catch (e) {
|
|
292
|
+
// TODO: more meaningful error reporting
|
|
293
|
+
log.error(`Error processing block: ${e.message}, Block:\n${JSON.stringify(blockInfo)}`);
|
|
294
|
+
}
|
|
295
|
+
return results;
|
|
296
|
+
}, []);
|
|
297
|
+
extensionInfo.menus = extensionInfo.menus || {};
|
|
298
|
+
extensionInfo.menus = this._prepareMenuInfo(serviceName, extensionInfo.menus);
|
|
299
|
+
return extensionInfo;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Prepare extension menus. e.g. setup binding for dynamic menu functions.
|
|
304
|
+
* @param {string} serviceName - the name of the service hosting this extension block
|
|
305
|
+
* @param {Array.<MenuInfo>} menus - the menu defined by the extension.
|
|
306
|
+
* @returns {Array.<MenuInfo>} - a menuInfo object with all preprocessing done.
|
|
307
|
+
* @private
|
|
308
|
+
*/
|
|
309
|
+
_prepareMenuInfo (serviceName, menus) {
|
|
310
|
+
const menuNames = Object.getOwnPropertyNames(menus);
|
|
311
|
+
for (let i = 0; i < menuNames.length; i++) {
|
|
312
|
+
const menuName = menuNames[i];
|
|
313
|
+
let menuInfo = menus[menuName];
|
|
314
|
+
|
|
315
|
+
// If the menu description is in short form (items only) then normalize it to general form: an object with
|
|
316
|
+
// its items listed in an `items` property.
|
|
317
|
+
if (!menuInfo.items) {
|
|
318
|
+
menuInfo = {
|
|
319
|
+
items: menuInfo
|
|
320
|
+
};
|
|
321
|
+
menus[menuName] = menuInfo;
|
|
322
|
+
}
|
|
323
|
+
// If `items` is a string, it should be the name of a function in the extension object. Calling the
|
|
324
|
+
// function should return an array of items to populate the menu when it is opened.
|
|
325
|
+
if (typeof menuInfo.items === 'string') {
|
|
326
|
+
const menuItemFunctionName = menuInfo.items;
|
|
327
|
+
const serviceObject = dispatch.services[serviceName];
|
|
328
|
+
// Bind the function here so we can pass a simple item generation function to Scratch Blocks later.
|
|
329
|
+
menuInfo.items = this._getExtensionMenuItems.bind(this, serviceObject, menuItemFunctionName);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return menus;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Fetch the items for a particular extension menu, providing the target ID for context.
|
|
337
|
+
* @param {object} extensionObject - the extension object providing the menu.
|
|
338
|
+
* @param {string} menuItemFunctionName - the name of the menu function to call.
|
|
339
|
+
* @returns {Array} menu items ready for scratch-blocks.
|
|
340
|
+
* @private
|
|
341
|
+
*/
|
|
342
|
+
_getExtensionMenuItems (extensionObject, menuItemFunctionName) {
|
|
343
|
+
// Fetch the items appropriate for the target currently being edited. This assumes that menus only
|
|
344
|
+
// collect items when opened by the user while editing a particular target.
|
|
345
|
+
const editingTarget = this.runtime.getEditingTarget() || this.runtime.getTargetForStage();
|
|
346
|
+
const editingTargetID = editingTarget ? editingTarget.id : null;
|
|
347
|
+
const extensionMessageContext = this.runtime.makeMessageContextForTarget(editingTarget);
|
|
348
|
+
|
|
349
|
+
// TODO: Fix this to use dispatch.call when extensions are running in workers.
|
|
350
|
+
const menuFunc = extensionObject[menuItemFunctionName];
|
|
351
|
+
const menuItems = menuFunc.call(extensionObject, editingTargetID).map(
|
|
352
|
+
item => {
|
|
353
|
+
item = maybeFormatMessage(item, extensionMessageContext);
|
|
354
|
+
switch (typeof item) {
|
|
355
|
+
case 'object':
|
|
356
|
+
return [
|
|
357
|
+
maybeFormatMessage(item.text, extensionMessageContext),
|
|
358
|
+
item.value
|
|
359
|
+
];
|
|
360
|
+
case 'string':
|
|
361
|
+
return [item, item];
|
|
362
|
+
default:
|
|
363
|
+
return item;
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
if (!menuItems || menuItems.length < 1) {
|
|
368
|
+
throw new Error(`Extension menu returned no items: ${menuItemFunctionName}`);
|
|
369
|
+
}
|
|
370
|
+
return menuItems;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Apply defaults for optional block fields.
|
|
375
|
+
* @param {string} serviceName - the name of the service hosting this extension block
|
|
376
|
+
* @param {ExtensionBlockMetadata} blockInfo - the block info from the extension
|
|
377
|
+
* @returns {ExtensionBlockMetadata} - a new block info object which has values for all relevant optional fields.
|
|
378
|
+
* @private
|
|
379
|
+
*/
|
|
380
|
+
_prepareBlockInfo (serviceName, blockInfo) {
|
|
381
|
+
blockInfo = Object.assign({}, {
|
|
382
|
+
blockType: BlockType.COMMAND,
|
|
383
|
+
terminal: false,
|
|
384
|
+
blockAllThreads: false,
|
|
385
|
+
arguments: {}
|
|
386
|
+
}, blockInfo);
|
|
387
|
+
blockInfo.opcode = blockInfo.opcode && this._sanitizeID(blockInfo.opcode);
|
|
388
|
+
blockInfo.text = blockInfo.text || blockInfo.opcode;
|
|
389
|
+
|
|
390
|
+
switch (blockInfo.blockType) {
|
|
391
|
+
case BlockType.EVENT:
|
|
392
|
+
if (blockInfo.func) {
|
|
393
|
+
log.warn(`Ignoring function "${blockInfo.func}" for event block ${blockInfo.opcode}`);
|
|
394
|
+
}
|
|
395
|
+
break;
|
|
396
|
+
case BlockType.BUTTON:
|
|
397
|
+
if (blockInfo.opcode) {
|
|
398
|
+
log.warn(`Ignoring opcode "${blockInfo.opcode}" for button with text: ${blockInfo.text}`);
|
|
399
|
+
}
|
|
400
|
+
break;
|
|
401
|
+
default: {
|
|
402
|
+
if (!blockInfo.opcode) {
|
|
403
|
+
throw new Error('Missing opcode for block');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const funcName = blockInfo.func ? this._sanitizeID(blockInfo.func) : blockInfo.opcode;
|
|
407
|
+
|
|
408
|
+
const getBlockInfo = blockInfo.isDynamic ?
|
|
409
|
+
args => args && args.mutation && args.mutation.blockInfo :
|
|
410
|
+
() => blockInfo;
|
|
411
|
+
const callBlockFunc = (() => {
|
|
412
|
+
if (dispatch._isRemoteService(serviceName)) {
|
|
413
|
+
return (args, util, realBlockInfo) =>
|
|
414
|
+
dispatch.call(serviceName, funcName, args, util, realBlockInfo);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// avoid promise latency if we can call direct
|
|
418
|
+
const serviceObject = dispatch.services[serviceName];
|
|
419
|
+
if (!serviceObject[funcName]) {
|
|
420
|
+
// The function might show up later as a dynamic property of the service object
|
|
421
|
+
log.warn(`Could not find extension block function called ${funcName}`);
|
|
422
|
+
}
|
|
423
|
+
return (args, util, realBlockInfo) =>
|
|
424
|
+
serviceObject[funcName](args, util, realBlockInfo);
|
|
425
|
+
})();
|
|
426
|
+
|
|
427
|
+
blockInfo.func = (args, util) => {
|
|
428
|
+
const realBlockInfo = getBlockInfo(args);
|
|
429
|
+
// TODO: filter args using the keys of realBlockInfo.arguments? maybe only if sandboxed?
|
|
430
|
+
return callBlockFunc(args, util, realBlockInfo);
|
|
431
|
+
};
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return blockInfo;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
module.exports = ExtensionManager;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} ExtensionMetadata
|
|
3
|
+
* All the metadata needed to register an extension.
|
|
4
|
+
* @property {string} id - a unique alphanumeric identifier for this extension. No special characters allowed.
|
|
5
|
+
* @property {string} [name] - the human-readable name of this extension.
|
|
6
|
+
* @property {string} [blockIconURI] - URI for an image to be placed on each block in this extension. Data URI ok.
|
|
7
|
+
* @property {string} [menuIconURI] - URI for an image to be placed on this extension's category menu item. Data URI ok.
|
|
8
|
+
* @property {string} [docsURI] - link to documentation content for this extension.
|
|
9
|
+
* @property {Array.<ExtensionBlockMetadata|string>} blocks - the blocks provided by this extension, plus separators.
|
|
10
|
+
* @property {Object.<ExtensionMenuMetadata>} [menus] - map of menu name to metadata for each of this extension's menus.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object} ExtensionBlockMetadata
|
|
15
|
+
* All the metadata needed to register an extension block.
|
|
16
|
+
* @property {string} opcode - a unique alphanumeric identifier for this block. No special characters allowed.
|
|
17
|
+
* @property {string} [func] - the name of the function implementing this block. Can be shared by other blocks/opcodes.
|
|
18
|
+
* @property {BlockType} blockType - the type of block (command, reporter, etc.) being described.
|
|
19
|
+
* @property {string} text - the text on the block, with [PLACEHOLDERS] for arguments.
|
|
20
|
+
* @property {Boolean} [hideFromPalette] - true if this block should not appear in the block palette.
|
|
21
|
+
* @property {Boolean} [isTerminal] - true if the block ends a stack - no blocks can be connected after it.
|
|
22
|
+
* @property {Boolean} [disableMonitor] - true if this block is a reporter but should not allow a monitor.
|
|
23
|
+
* @property {ReporterScope} [reporterScope] - if this block is a reporter, this is the scope/context for its value.
|
|
24
|
+
* @property {Boolean} [isEdgeActivated] - sets whether a hat block is edge-activated.
|
|
25
|
+
* @property {Boolean} [shouldRestartExistingThreads] - sets whether a hat/event block should restart existing threads.
|
|
26
|
+
* @property {int} [branchCount] - for flow control blocks, the number of branches/substacks for this block.
|
|
27
|
+
* @property {Object.<ExtensionArgumentMetadata>} [arguments] - map of argument placeholder to metadata about each arg.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {object} ExtensionArgumentMetadata
|
|
32
|
+
* All the metadata needed to register an argument for an extension block.
|
|
33
|
+
* @property {ArgumentType} type - the type of the argument (number, string, etc.)
|
|
34
|
+
* @property {*} [defaultValue] - the default value of this argument.
|
|
35
|
+
* @property {string} [menu] - the name of the menu to use for this argument, if any.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @typedef {ExtensionDynamicMenu|ExtensionMenuItems} ExtensionMenuMetadata
|
|
40
|
+
* All the metadata needed to register an extension drop-down menu.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {string} ExtensionDynamicMenu
|
|
45
|
+
* The string name of a function which returns menu items.
|
|
46
|
+
* @see {ExtensionMenuItems} - the type of data expected to be returned by the specified function.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Array.<ExtensionMenuItemSimple|ExtensionMenuItemComplex>} ExtensionMenuItems
|
|
51
|
+
* Items in an extension menu.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {string} ExtensionMenuItemSimple
|
|
56
|
+
* A menu item for which the label and value are identical strings.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {object} ExtensionMenuItemComplex
|
|
61
|
+
* A menu item for which the label and value can differ.
|
|
62
|
+
* @property {*} value - the value of the block argument when this menu item is selected.
|
|
63
|
+
* @property {string} text - the human-readable label of this menu item in the menu.
|
|
64
|
+
*/
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/* eslint-env worker */
|
|
2
|
+
|
|
3
|
+
const ArgumentType = require('../extension-support/argument-type');
|
|
4
|
+
const BlockType = require('../extension-support/block-type');
|
|
5
|
+
const dispatch = require('../dispatch/worker-dispatch');
|
|
6
|
+
const TargetType = require('../extension-support/target-type');
|
|
7
|
+
|
|
8
|
+
class ExtensionWorker {
|
|
9
|
+
constructor () {
|
|
10
|
+
this.nextExtensionId = 0;
|
|
11
|
+
|
|
12
|
+
this.initialRegistrations = [];
|
|
13
|
+
|
|
14
|
+
dispatch.waitForConnection.then(() => {
|
|
15
|
+
dispatch.call('extensions', 'allocateWorker').then(x => {
|
|
16
|
+
const [id, extension] = x;
|
|
17
|
+
this.workerId = id;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
importScripts(extension);
|
|
21
|
+
|
|
22
|
+
const initialRegistrations = this.initialRegistrations;
|
|
23
|
+
this.initialRegistrations = null;
|
|
24
|
+
|
|
25
|
+
Promise.all(initialRegistrations).then(() => dispatch.call('extensions', 'onWorkerInit', id));
|
|
26
|
+
} catch (e) {
|
|
27
|
+
dispatch.call('extensions', 'onWorkerInit', id, e);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
this.extensions = [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
register (extensionObject) {
|
|
36
|
+
const extensionId = this.nextExtensionId++;
|
|
37
|
+
this.extensions.push(extensionObject);
|
|
38
|
+
const serviceName = `extension.${this.workerId}.${extensionId}`;
|
|
39
|
+
const promise = dispatch.setService(serviceName, extensionObject)
|
|
40
|
+
.then(() => dispatch.call('extensions', 'registerExtensionService', serviceName));
|
|
41
|
+
if (this.initialRegistrations) {
|
|
42
|
+
this.initialRegistrations.push(promise);
|
|
43
|
+
}
|
|
44
|
+
return promise;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
global.Scratch = global.Scratch || {};
|
|
49
|
+
global.Scratch.ArgumentType = ArgumentType;
|
|
50
|
+
global.Scratch.BlockType = BlockType;
|
|
51
|
+
global.Scratch.TargetType = TargetType;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Expose only specific parts of the worker to extensions.
|
|
55
|
+
*/
|
|
56
|
+
const extensionWorker = new ExtensionWorker();
|
|
57
|
+
global.Scratch.extensions = {
|
|
58
|
+
register: extensionWorker.register.bind(extensionWorker)
|
|
59
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Indicate the scope for a reporter's value.
|
|
3
|
+
* @enum {string}
|
|
4
|
+
*/
|
|
5
|
+
const ReporterScope = {
|
|
6
|
+
/**
|
|
7
|
+
* This reporter's value is global and does not depend on context.
|
|
8
|
+
*/
|
|
9
|
+
GLOBAL: 'global',
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* This reporter's value is specific to a particular target/sprite.
|
|
13
|
+
* Another target may have a different value or may not even have a value.
|
|
14
|
+
*/
|
|
15
|
+
TARGET: 'target'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
module.exports = ReporterScope;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default types of Target supported by the VM
|
|
3
|
+
* @enum {string}
|
|
4
|
+
*/
|
|
5
|
+
const TargetType = {
|
|
6
|
+
/**
|
|
7
|
+
* Rendered target which can move, change costumes, etc.
|
|
8
|
+
*/
|
|
9
|
+
SPRITE: 'sprite',
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Rendered target which cannot move but can change backdrops
|
|
13
|
+
*/
|
|
14
|
+
STAGE: 'stage'
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
module.exports = TargetType;
|