Skip to content

Dev #5368

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 24 commits into from
Closed

Dev #5368

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a467c3f
feat: new extension
Rbel12b Mar 8, 2025
1201041
Merge pull request #1 from scratchfoundation/develop
Rbel12b Mar 8, 2025
712b82f
feat(samlabs): new blocks for the samlabs extension
Rbel12b Mar 11, 2025
2e84c39
fix(samlabs): device handling
Rbel12b Mar 11, 2025
2cec3fd
fix(samlabs): fixed code for baby sam bot commands
Rbel12b Mar 12, 2025
824e374
fix(samlabs): typo in rgb led adv name
Rbel12b Mar 17, 2025
a27717c
Merge pull request #2 from scratchfoundation/develop
Rbel12b Mar 20, 2025
7cd642a
Merge pull request #3 from Rbel12b/dev
Rbel12b Mar 20, 2025
b5532e9
refactor: samlabs
Rbel12b Mar 27, 2025
a7df807
feat: samlabs extension: add scratch-link compatibility
Rbel12b Mar 29, 2025
d83ec9a
refactor: break samlabs code into two extensions
Rbel12b Mar 29, 2025
184df5e
fix: web BLEapi detection
Rbel12b Mar 30, 2025
b0fead7
fix: use rate limiter
Rbel12b Mar 30, 2025
4256f57
Merge pull request #4 from Rbel12b/dev
Rbel12b Mar 30, 2025
d79527e
Merge pull request #5 from scratchfoundation/develop
Rbel12b Mar 30, 2025
7eb1699
feat: add send interval
Rbel12b Mar 31, 2025
1ea541b
fix: change Baby SAM Bot name
Rbel12b Mar 31, 2025
5225a1c
fix: variable creation: remove requestblocksUpdate
Rbel12b Mar 31, 2025
509e0d0
feat: extension loader, move samlabs to other repo
Rbel12b Apr 1, 2025
a815252
feat: ability to load extensions from url, add microbit more extension
Rbel12b Apr 2, 2025
62be60d
fix: custom extension loading, load samlabs by default
Rbel12b Apr 2, 2025
1c5b67c
fix: custom extension loading
Rbel12b Apr 3, 2025
0baf19a
feat: add root robot extension
Rbel12b Apr 9, 2025
b2108b7
Merge remote-tracking branch 'upstream/develop' into dev
Rbel12b Apr 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/engine/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,7 @@ class Runtime extends EventEmitter {
const categoryInfo = {
id: extensionInfo.id,
name: maybeFormatMessage(extensionInfo.name),
extensionURL: extensionInfo.extensionURL,
showStatusButton: extensionInfo.showStatusButton,
blockIconURI: extensionInfo.blockIconURI,
menuIconURI: extensionInfo.menuIconURI
Expand Down
128 changes: 120 additions & 8 deletions src/extension-support/extension-manager.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const dispatch = require('../dispatch/central-dispatch');
const log = require('../util/log');
const maybeFormatMessage = require('../util/maybe-format-message');
const formatMessage = require('format-message');

const BlockType = require('./block-type');

Expand All @@ -23,7 +24,24 @@ const builtinExtensions = {
ev3: () => require('../extensions/scratch3_ev3'),
makeymakey: () => require('../extensions/scratch3_makeymakey'),
boost: () => require('../extensions/scratch3_boost'),
gdxfor: () => require('../extensions/scratch3_gdx_for')
gdxfor: () => require('../extensions/scratch3_gdx_for'),
root: () => require('../extensions/root')
};

const customExtensions = {
samlabs: () => require('../../../scratch-samlabs/src/vm/extensions/block/samlabs'),
sambot: () => require('../../../scratch-samlabs/src/vm/extensions/block/sambot'),
microbitMore: () => ({url: 'https://microbit-more.github.io/dist/microbitMore.mjs'}),
controlplus: () => ({url: 'https://bricklife.com/scratch-gui/xcratch/controlplus.mjs'}),
duplotrain: () => ({url: 'https://bricklife.com/scratch-gui/xcratch/duplotrain.mjs'}),
legoble: () => ({url: 'https://bricklife.com/scratch-gui/xcratch/legoble.mjs'}),
legoluigi: () => ({url: 'https://bricklife.com/scratch-gui/xcratch/legoluigi.mjs'}),
legomario: () => ({url: 'https://bricklife.com/scratch-gui/xcratch/legomario.mjs'}),
legopeach: () => ({url: 'https://bricklife.com/scratch-gui/xcratch/legopeach.mjs'}),
legoremote: () => ({url: 'https://bricklife.com/scratch-gui/xcratch/legoremote.mjs'}),
poweredup: () => ({url: 'https://bricklife.com/scratch-gui/xcratch/poweredup.mjs'}),
spikeessential: () => ({url: 'https://bricklife.com/scratch-gui/xcratch/spikeessential.mjs'})
// spikeprime: () => ({url: 'https://bricklife.com/scratch-gui/xcratch/spikeprime.mjs'})
};

/**
Expand Down Expand Up @@ -93,10 +111,13 @@ class ExtensionManager {
* @type {Runtime}
*/
this.runtime = runtime;
this.runtime.formatMessage = formatMessage;

dispatch.setService('extensions', this).catch(e => {
log.error(`ExtensionManager was unable to register extension service: ${JSON.stringify(e)}`);
});

this.loadExtensionURL('samlabs');
}

/**
Expand Down Expand Up @@ -134,12 +155,90 @@ class ExtensionManager {
this._loadedExtensions.set(extensionId, serviceName);
}

/**
* Fetch URL and return entry object and block class of the extension.
* @param {string} extensionURL - URL for module of the extension.
* @returns {{entry: object, blockClass: BlockClass}} Array with entry and block class of the extension.
*/
fetchExtension (extensionURL) {
return import(/* webpackIgnore: true */ extensionURL)
.then(module => {
const entry = module.entry;
entry.extensionURL = extensionURL;
const blockClass = module.blockClass;
blockClass.extensionURL = extensionURL;
return {entry: entry, blockClass: blockClass};
});
}

addBultinExtension (entry, blockClass) {
builtinExtensions[entry.extensionId] = () => blockClass;
this.extensionLibraryContent.unshift(entry);
}

/**
* Instanceate new block object and register that in the runtime.
* @param {object} entry - Entry object to register.
* @param {class} blockClass - Class of block object to regiser.
* @returns {object} Block object which was registered.
*/
registerExtensionBlock (entry, blockClass) {
const runtime = this.runtime;
const block = new blockClass(runtime);
const extensionID = block.getInfo().id;
if (entry.extensionId !== extensionID) {
// Reject by the security risk.
throw new Error(`Extension ID mismatch entry: '${entry.extensionId}' block: '${extensionID}'`);
}
if (this.isExtensionLoaded(extensionID)) {
// Remove from loaded extensions
const oldServiceName = this._loadedExtensions.get(extensionID);
this._loadedExtensions.delete(extensionID);
// Remove from dispatcher
delete dispatch.services[oldServiceName];
// Remove from block info
const oldeBlockInfoIndex = runtime._blockInfo.findIndex(info => info.id === extensionID);
if (oldeBlockInfoIndex >= 0) {
runtime._blockInfo.splice(oldeBlockInfoIndex, 1);
}
}
const serviceName = this._registerInternalExtension(block);
this._loadedExtensions.set(extensionID, serviceName);
const oldEntryIndex = this.extensionLibraryContent
.findIndex(libEntry => libEntry.extensionId === extensionID);
if (oldEntryIndex >= 0) {
// Remove from extension library
this.extensionLibraryContent.splice(oldEntryIndex, 1);
}
if (this.extensionLibraryContent) {
this.extensionLibraryContent.unshift(entry);
}
return block;
}

/**
* Load an extension by URL or internal extension ID
* @param {string} extensionURL - the URL for the extension to load OR the ID of an internal extension
* @returns {Promise} resolved once the extension is loaded and initialized or rejected on failure
*/
loadExtensionURL (extensionURL) {
async loadExtensionURL (extensionURL) {
if (Object.prototype.hasOwnProperty.call(customExtensions, extensionURL)) {
if (this.isExtensionLoaded(extensionURL)) {
const message = `Rejecting attempt to load a second extension with ID ${extensionURL}`;
log.warn(message);
return Promise.resolve();
}
let extension;
if (customExtensions[extensionURL]().url) {
extension = await this.fetchExtension(customExtensions[extensionURL]().url);
} else {
extension = customExtensions[extensionURL]();
}
const extensionInstance = new extension.blockClass(this.runtime);
const serviceName = this._registerInternalExtension(extensionInstance);
this._loadedExtensions.set(extensionURL, serviceName);
return Promise.resolve();
}
if (Object.prototype.hasOwnProperty.call(builtinExtensions, extensionURL)) {
/** @TODO dupe handling for non-builtin extensions. See commit 670e51d33580e8a2e852b3b038bb3afc282f81b9 */
if (this.isExtensionLoaded(extensionURL)) {
Expand All @@ -155,13 +254,26 @@ class ExtensionManager {
return Promise.resolve();
}

return new Promise((resolve, reject) => {
// If we `require` this at the global level it breaks non-webpack targets, including tests
const worker = new Worker('./extension-worker.js');
const builtinClassFunc = Object.values(builtinExtensions)
.find(blockClassFunc => blockClassFunc().extensionURL === extensionURL);
if (builtinClassFunc) {
const blockClass = builtinClassFunc();
const block = new blockClass(this.runtime);
const serviceName = this._registerInternalExtension(block);
this._loadedExtensions.set(blockClass.EXTENSION_ID, serviceName);
return Promise.resolve();
}

this.pendingExtensions.push({extensionURL, resolve, reject});
dispatch.addWorker(worker);
});
// To access the runtime even in outer extensions, it loaded by dynamic import() istead of extension-worker.
return this.fetchExtension(extensionURL)
.then(({entry, blockClass}) => {
this.registerExtensionBlock(entry, blockClass);
return Promise.resolve();
})
.catch(error => {
log.log(error);
return Promise.reject(error);
});
}

/**
Expand Down
Loading
Loading