diff --git a/src/compiler/compile.js b/src/compiler/compile.js index fb43164504..b5edb81352 100644 --- a/src/compiler/compile.js +++ b/src/compiler/compile.js @@ -1,4 +1,4 @@ -const IRGenerator = require('./irgen'); +const {IRGenerator} = require('./irgen'); const JSGenerator = require('./jsgen'); const compile = thread => { diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index 40f007a75d..c3679fb41a 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -1693,4 +1693,7 @@ class IRGenerator { } } -module.exports = IRGenerator; +module.exports = { + ScriptTreeGenerator, + IRGenerator +}; diff --git a/src/engine/thread.js b/src/engine/thread.js index a4e55504f9..c8cf6cb1c1 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -514,4 +514,7 @@ class Thread { } } +// for extensions +Thread._StackFrame = _StackFrame; + module.exports = Thread; diff --git a/src/extensions/scratch3_pen/index.js b/src/extensions/scratch3_pen/index.js index 16c09dfb61..b1b8eace4a 100644 --- a/src/extensions/scratch3_pen/index.js +++ b/src/extensions/scratch3_pen/index.js @@ -298,6 +298,16 @@ class Scratch3PenBlocks { }), blockIconURI: blockIconURI, blocks: [ + // tw: additional message when on the stage for clarity + { + blockType: BlockType.LABEL, + text: formatMessage({ + id: 'tw.pen.stageSelected', + default: 'Stage selected: less pen blocks', + description: 'Label that appears in the Pen category when the stage is selected' + }), + filter: [TargetType.STAGE] + }, { opcode: 'clear', blockType: BlockType.COMMAND, diff --git a/src/util/base64-util.js b/src/util/base64-util.js index 1325291707..19165c980b 100644 --- a/src/util/base64-util.js +++ b/src/util/base64-util.js @@ -20,10 +20,13 @@ class Base64Util { /** * Convert a Uint8Array to a base64 encoded string. - * @param {Uint8Array} array - the array to convert. + * @param {Uint8Array|Array} array - the array to convert. * @return {string} - the base64 encoded string. */ static uint8ArrayToBase64 (array) { + if (Array.isArray(array)) { + array = new Uint8Array(array); + } let binary = ''; const len = array.byteLength; for (let i = 0; i < len; i++) { diff --git a/src/virtual-machine.js b/src/virtual-machine.js index fd218be46b..e1a1fcdea9 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -220,7 +220,17 @@ class VirtualMachine extends EventEmitter { this.exports = { Sprite, RenderedTarget, - JSZip + JSZip, + + i_will_not_ask_for_help_when_these_break: () => { + console.warn('You are using unsupported APIs. WHEN your code breaks, do not expect help.'); + return ({ + JSGenerator: require('./compiler/jsgen.js'), + IRGenerator: require('./compiler/irgen.js').IRGenerator, + ScriptTreeGenerator: require('./compiler/irgen.js').ScriptTreeGenerator, + Thread: require('./engine/thread.js') + }); + } }; } diff --git a/test/fixtures/tw-block-returning-promise-like.sb3 b/test/fixtures/tw-block-returning-promise-like.sb3 index d7c232ca21..4ef6f8076e 100644 Binary files a/test/fixtures/tw-block-returning-promise-like.sb3 and b/test/fixtures/tw-block-returning-promise-like.sb3 differ diff --git a/test/integration/tw_block_returning_promise_like.js b/test/integration/tw_block_returning_promise_like.js index a0e95e8c3e..e212ef4266 100644 --- a/test/integration/tw_block_returning_promise_like.js +++ b/test/integration/tw_block_returning_promise_like.js @@ -21,13 +21,13 @@ class TestExtension { }; } test () { - // returns a PromiseLike that calls handler immediately. + // returns a PromiseLike that calls handler immediately instead of in next microtask. const promise = { then (callbackFn) { callbackFn(); return promise; } - // intentionally omit catch() as that is not part of PromiseLike + // intentionally omit catch() as that is not part of PromiseLike, so it should not be used }; return promise; } @@ -45,6 +45,10 @@ for (const compilerEnabled of [false, true]) { }); t.equal(vm.runtime.compilerOptions.enabled, compilerEnabled, 'sanity check'); + vm.runtime.on('COMPILE_ERROR', () => { + t.fail('Compile error'); + }); + vm.loadProject(fixture).then(() => { let ended = 0; vm.runtime.on('SAY', (target, type, text) => { diff --git a/test/unit/tw_sandboxed_extensions.js b/test/unit/tw_sandboxed_extensions.js index a763c8b616..ec0fb4b562 100644 --- a/test/unit/tw_sandboxed_extensions.js +++ b/test/unit/tw_sandboxed_extensions.js @@ -9,6 +9,9 @@ global.fetch = (url, options = {}) => ( Promise.resolve(`[Response ${url instanceof Request ? url.url : url} options=${JSON.stringify(options)}]`) ); +// Remove navigator object from Node 21 and later +delete global.navigator; + // Need to trick the extension API to think it's running in a worker // It will not actually use this object ever. global.self = {}; diff --git a/test/unit/tw_translate.js b/test/unit/tw_translate.js index 8ac5dc7053..3e9c9ccf2c 100644 --- a/test/unit/tw_translate.js +++ b/test/unit/tw_translate.js @@ -6,9 +6,12 @@ global.fetch = () => Promise.reject(new Error('Simulated network error')); const Scratch3TranslateBlocks = require('../../src/extensions/scratch3_translate/index'); -global.navigator = { - language: 'en' -}; +// Node 21 and later defines a navigator object, but we want to override that for the test +Object.defineProperty(global, 'navigator', { + value: { + language: 'en-US' + } +}); // Translate tries to access AbortController from window, but does not require it to exist. global.window = {}; diff --git a/test/unit/tw_unsandboxed_extensions.js b/test/unit/tw_unsandboxed_extensions.js index a4bfdeec4b..48cf89c12e 100644 --- a/test/unit/tw_unsandboxed_extensions.js +++ b/test/unit/tw_unsandboxed_extensions.js @@ -52,6 +52,9 @@ global.window = { open: (url, target, features) => `[Window ${url} target=${target || ''} features=${features || ''}]` }; +// Remove navigator object from Node 21 and later +delete global.navigator; + tap.beforeEach(() => { scriptCallbacks.clear(); global.location = { diff --git a/test/unit/util_base64.js b/test/unit/util_base64.js index c18c353333..f43aa14859 100644 --- a/test/unit/util_base64.js +++ b/test/unit/util_base64.js @@ -3,6 +3,7 @@ const Base64Util = require('../../src/util/base64-util'); test('uint8ArrayToBase64', t => { t.equal(Base64Util.uint8ArrayToBase64(new Uint8Array([0, 50, 80, 200])), 'ADJQyA=='); + t.equal(Base64Util.uint8ArrayToBase64([0, 50, 80, 200]), 'ADJQyA=='); t.end(); }); @@ -25,11 +26,13 @@ test('round trips', t => { new Uint8Array(0), new Uint8Array([10, 90, 0, 255, 255, 255, 10, 2]), new Uint8Array(10000), - new Uint8Array(1000000) + new Uint8Array(100000) ]; for (const uint8array of data) { const uint8ToBase64 = Base64Util.uint8ArrayToBase64(uint8array); + const arrayToBase64 = Base64Util.uint8ArrayToBase64(Array.from(uint8array)); const bufferToBase64 = Base64Util.arrayBufferToBase64(uint8array.buffer); + t.equal(uint8ToBase64, arrayToBase64); t.equal(uint8ToBase64, bufferToBase64); const decoded = Base64Util.base64ToUint8Array(uint8ToBase64); t.same(uint8array, decoded);