16 KiB
Scratch 3.0 Extensions
This document describes technical topics related to Scratch 3.0 extension development, including the Scratch 3.0 extension specification.
For documentation regarding other aspects of Scratch 3.0 extensions see this Extensions page on the wiki.
Types of Extensions
There are four types of extensions that can define everything from the Scratch's core library (such as the "Looks" and "Operators" categories) to unofficial extensions that can be loaded from a remote URL.
Scratch 3.0 does not yet support unofficial extensions.
For more details, see this Extensions page on the wiki.
Core | Team | Official | Unofficial | |
---|---|---|---|---|
Developed by Scratch Team | √ | √ | O | X |
Maintained by Scratch Team | √ | √ | O | X |
Shown in Library | X | √ | √ | X |
Sandboxed | X | X | √ | √ |
Can save projects to community | √ | √ | √ | X |
JavaScript Environment
Most Scratch 3.0 is written using JavaScript features not yet commonly supported by browsers. For compatibility we
transpile the code to ES5 before publishing or deploying. Any extension included in the scratch-vm
repository may
use ES6+ features and may use require
to reference other code within the scratch-vm
repository.
Unofficial extensions must be self-contained. Authors of unofficial extensions are responsible for ensuring browser compatibility for those extensions, including transpiling if necessary.
Translation
Scratch extensions use the ICU message format to handle
translation across languages. For core, team, and official extensions, the function formatMessage
is used to
wrap any ICU messages that need to be exported to the Scratch Transifex group
for translation.
All extensions may additionally define a translation_map
object within the getInfo
function which can provide
translations within an extension itself. The "Annotated Example" below provides a more complete illustration of how
translation within an extension can be managed. WARNING: the translation_map
feature is currently in the
proposal phase and may change before implementation.
Backwards Compatibility
Scratch is designed to be fully backwards compatible. Because of this, block definitions and opcodes should never change in a way that could cause previously saved projects to fail to load or to act in unexpected / inconsistent ways.
Defining an Extension
Scratch extensions are defined as a single Javascript class which accepts either a reference to the Scratch VM runtime or a "runtime proxy" which handles communication with the Scratch VM across a well defined worker boundary (i.e. the sandbox).
class SomeBlocks {
constructor (runtime) {
/**
* Store this for later communication with the Scratch VM runtime.
* If this extension is running in a sandbox then `runtime` is an async proxy object.
* @type {Runtime}
*/
this.runtime = runtime;
}
// ...
}
All extensions must define a function called getInfo
which returns an object that contains the information needed to
render both the blocks and the extension itself.
// Core, Team, and Official extensions can `require` VM code:
const ArgumentType = require('../../extension-support/argument-type');
const BlockType = require('../../extension-support/block-type');
class SomeBlocks {
// ...
getInfo () {
return {
id: 'someBlocks',
name: 'Some Blocks',
blocks: [
{
opcode: 'myReporter',
blockType: BlockType.REPORTER,
text: 'letter [LETTER_NUM] of [TEXT]',
arguments: {
LETTER_NUM: {
type: ArgumentType.STRING,
defaultValue: '1'
},
TEXT: {
type: ArgumentType.STRING,
defaultValue: 'text'
}
}
}
]
};
}
// ...
}
Finally the extension must define a function for any "opcode" defined in the blocks. For example:
class SomeBlocks {
// ...
myReporter (args) {
return args.TEXT.charAt(args.LETTER_NUM);
};
// ...
}
Annotated Example
// Core, Team, and Official extensions can `require` VM code:
const ArgumentType = require('../../extension-support/argument-type');
const BlockType = require('../../extension-support/block-type');
const TargetType = require('../../extension-support/target-type');
// ...or VM dependencies:
const formatMessage = require('format-message');
// Core, Team, and Official extension classes should be registered statically with the Extension Manager.
// See: scratch-vm/src/extension-support/extension-manager.js
class SomeBlocks {
constructor (runtime) {
/**
* Store this for later communication with the Scratch VM runtime.
* If this extension is running in a sandbox then `runtime` is an async proxy object.
* @type {Runtime}
*/
this.runtime = runtime;
}
/**
* @return {object} This extension's metadata.
*/
getInfo () {
return {
// Required: the machine-readable name of this extension.
// Will be used as the extension's namespace.
id: 'someBlocks',
// Optional: the human-readable name of this extension as string.
// This and any other string to be displayed in the Scratch UI may either be
// a string or a call to `formatMessage`; a plain string will not be
// translated whereas a call to `formatMessage` will connect the string
// to the translation map (see below). The `formatMessage` call is
// similar to `formatMessage` from `react-intl` in form, but will actually
// call some extension support code to do its magic. For example, we will
// internally namespace the messages such that two extensions could have
// messages with the same ID without colliding.
// See also: https://github.com/yahoo/react-intl/wiki/API#formatmessage
name: formatMessage({
id: 'extensionName',
defaultMessage: 'Some Blocks',
description: 'The name of the "Some Blocks" extension'
}),
// Optional: URI for a block icon, to display at the edge of each block for this
// extension. Data URI OK.
// TODO: what file types are OK? All web images? Just PNG?
blockIconURI: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAAAAACyOJm3AAAAFklEQVQYV2P4DwMMEMgAI/+DEUIMBgAEWB7i7uidhAAAAABJRU5ErkJggg==',
// Optional: URI for an icon to be displayed in the blocks category menu.
// If not present, the menu will display the block icon, if one is present.
// Otherwise, the category menu shows its default filled circle.
// Data URI OK.
// TODO: what file types are OK? All web images? Just PNG?
menuIconURI: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAAAAACyOJm3AAAAFklEQVQYV2P4DwMMEMgAI/+DEUIMBgAEWB7i7uidhAAAAABJRU5ErkJggg==',
// Optional: Link to documentation content for this extension.
// If not present, offer no link.
docsURI: 'https://....',
// Required: the list of blocks implemented by this extension,
// in the order intended for display.
blocks: [
{
// Required: the machine-readable name of this operation.
// This will appear in project JSON.
opcode: 'myReporter', // becomes 'someBlocks.myReporter'
// Required: the kind of block we're defining, from a predefined list.
// Fully supported block types:
// BlockType.BOOLEAN - same as REPORTER but returns a Boolean value
// BlockType.COMMAND - a normal command block, like "move {} steps"
// BlockType.HAT - starts a stack if its value changes from falsy to truthy ("edge triggered")
// BlockType.REPORTER - returns a value, like "direction"
// Block types in development or for internal use only:
// BlockType.BUTTON - place a button in the block palette
// BlockType.CONDITIONAL - control flow, like "if {}" or "if {} else {}"
// A CONDITIONAL block may return the one-based index of a branch to
// run, or it may return zero/falsy to run no branch.
// BlockType.EVENT - starts a stack in response to an event (full spec TBD)
// BlockType.LOOP - control flow, like "repeat {} {}" or "forever {}"
// A LOOP block is like a CONDITIONAL block with two differences:
// - the block is assumed to have exactly one child branch, and
// - each time a child branch finishes, the loop block is called again.
blockType: BlockType.REPORTER,
// Required for CONDITIONAL blocks, ignored for others: the number of
// child branches this block controls. An "if" or "repeat" block would
// specify a branch count of 1; an "if-else" block would specify a
// branch count of 2.
// TODO: should we support dynamic branch count for "switch"-likes?
branchCount: 0,
// Optional, default false: whether or not this block ends a stack.
// The "forever" and "stop all" blocks would specify true here.
terminal: true,
// Optional, default false: whether or not to block all threads while
// this block is busy. This is for things like the "touching color"
// block in compatibility mode, and is only needed if the VM runs in a
// worker. We might even consider omitting it from extension docs...
blockAllThreads: false,
// Required: the human-readable text on this block, including argument
// placeholders. Argument placeholders should be in [MACRO_CASE] and
// must be [ENCLOSED_WITHIN_SQUARE_BRACKETS].
text: formatMessage({
id: 'myReporter',
defaultMessage: 'letter [LETTER_NUM] of [TEXT]',
description: 'Label on the "myReporter" block'
}),
// Required: describe each argument.
// Argument order may change during translation, so arguments are
// identified by their placeholder name. In those situations where
// arguments must be ordered or assigned an ordinal, such as interaction
// with Scratch Blocks, arguments are ordered as they are in the default
// translation (probably English).
arguments: {
// Required: the ID of the argument, which will be the name in the
// args object passed to the implementation function.
LETTER_NUM: {
// Required: type of the argument / shape of the block input
type: ArgumentType.NUMBER,
// Optional: the default value of the argument
default: 1
},
// Required: the ID of the argument, which will be the name in the
// args object passed to the implementation function.
TEXT: {
// Required: type of the argument / shape of the block input
type: ArgumentType.STRING,
// Optional: the default value of the argument
default: formatMessage({
id: 'myReporter.TEXT_default',
defaultMessage: 'text',
description: 'Default for "TEXT" argument of "someBlocks.myReporter"'
})
}
},
// Optional: the function implementing this block.
// If absent, assume `func` is the same as `opcode`.
func: 'myReporter',
// Optional: list of target types for which this block should appear.
// If absent, assume it applies to all builtin targets -- that is:
// [TargetType.SPRITE, TargetType.STAGE]
filter: [TargetType.SPRITE]
},
{
// Another block...
}
],
// Optional: define extension-specific menus here.
menus: {
// Required: an identifier for this menu, unique within this extension.
menuA: [
// Static menu: list items which should appear in the menu.
{
// Required: the value of the menu item when it is chosen.
value: 'itemId1',
// Optional: the human-readable label for this item.
// Use `value` as the text if this is absent.
text: formatMessage({
id: 'menuA_item1',
defaultMessage: 'Item One',
description: 'Label for item 1 of menu A in "Some Blocks" extension'
})
},
// The simplest form of a list item is a string which will be used as
// both value and text.
'itemId2'
],
// Dynamic menu: returns an array as above.
// Called each time the menu is opened.
menuB: 'getItemsForMenuB'
},
// Optional: translations (UNSTABLE - NOT YET SUPPORTED)
translation_map: {
de: {
'extensionName': 'Einige Blöcke',
'myReporter': 'Buchstabe [LETTER_NUM] von [TEXT]',
'myReporter.TEXT_default': 'Text',
'menuA_item1': 'Artikel eins',
// Dynamic menus can be translated too
'menuB_example': 'Beispiel',
// This message contains ICU placeholders (see `myReporter()` below)
'myReporter.result': 'Buchstabe {LETTER_NUM} von {TEXT} ist {LETTER}.'
},
it: {
// ...
}
}
};
};
/**
* Implement myReporter.
* @param {object} args - the block's arguments.
* @property {string} MY_ARG - the string value of the argument.
* @returns {string} a string which includes the block argument value.
*/
myReporter (args) {
// This message contains ICU placeholders, not Scratch placeholders
const message = formatMessage({
id: 'myReporter.result',
defaultMessage: 'Letter {LETTER_NUM} of {TEXT} is {LETTER}.',
description: 'The text template for the "myReporter" block result'
});
// Note: this implementation is not Unicode-clean; it's just here as an example.
const result = args.TEXT.charAt(args.LETTER_NUM);
return message.format({
LETTER_NUM: args.LETTER_NUM,
TEXT: args.TEXT,
LETTER: result
});
};
}