Skip to content

Port sp_javascriptV2 to PenguinMod's ACE editor approach; fix field default-value double-escaping#55

Merged
supervoidcoder merged 2 commits intodevelopfrom
copilot/fix-js-extension-errors-again
Mar 6, 2026
Merged

Port sp_javascriptV2 to PenguinMod's ACE editor approach; fix field default-value double-escaping#55
supervoidcoder merged 2 commits intodevelopfrom
copilot/fix-js-extension-errors-again

Conversation

Copy link

Copilot AI commented Mar 6, 2026

The JS extension's code editor block crashed with Cannot read properties of null (reading 'isInFlyout') and never rendered the ACE editor. A second bug caused the ARGS input to display { "FOO": "bar" } literally instead of { "FOO": "bar" }.

src/extensions/sp_javascriptV2/index.js — ported to PenguinMod's implementation

Root cause of null crash: jsCommandBinded (and jsReporter/jsBooleanBinded) embedded the custom editor field directly on the outer block via type: ArgumentType.CUSTOM. In the flyout those blocks have no parent, so srcBlock.parentBlock_ was always null.

Fix: Switch to fillIn: "codeInput" (matching PenguinMod), so the editor field lives inside a codeInput shadow whose parentBlock_ is always the outer block:

// Before — editor directly on jsCommandBinded, parent is null in flyout
CODE: { type: ArgumentType.CUSTOM, id: "SPjavascriptV2-codeEditor", defaultValue: CODE_EDITOR_INIT_SENTINEL }

// After — shadow block carries the field; parent is always set
CODE: { fillIn: "codeInput" }

Root cause of blank editor box: The old code injected ACE inside a sandboxed <iframe> communicating over postMessage. The iframe never rendered correctly in this context.

Fix: Replaced with PenguinMod's approach — ACE scripts are injected once into the page and the editor is mounted directly into the block's foreignObject <div>. Also brings in:

  • updateEditorSchema — populates ACE autocomplete with global function names
  • _compileCode / _parseArguments — cleaner argument injection (array vs. object dispatch)
  • serialize / deserialize — persists sandbox toggle state across saves

src/engine/runtime.js — fix double-escaped field default values

defaultValue was passed through xmlEscape.escapeAttribute twice — once at computation, once at XML interpolation — causing "&quot;&amp;quot; in the emitted XML. Blockly then displayed the raw entity text.

Changed computation to plain xmlEscape (correct for element content, no JSON.stringify side-effects) and removed the redundant second escape at the insertion point:

// Before
const defaultValue = xmlEscape.escapeAttribute(maybeFormatMessage(...).toString());
context.inputList.push(`<field ...>${xmlEscape.escapeAttribute(defaultValue)}</field>`);

// After
const defaultValue = xmlEscape(maybeFormatMessage(...).toString());
context.inputList.push(`<field ...>${defaultValue}</field>`);

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • cdn.assets.scratch.mit.edu
    • Triggering command: /opt/hostedtoolcache/node/24.14.0/x64/bin/node /opt/hostedtoolcache/node/24.14.0/x64/bin/node ./test/integration/complex.js (dns block)
    • Triggering command: /opt/hostedtoolcache/node/24.14.0/x64/bin/node /opt/hostedtoolcache/node/24.14.0/x64/bin/node ./test/integration/import_nested_sb2.js (dns block)
    • Triggering command: /opt/hostedtoolcache/node/24.14.0/x64/bin/node /opt/hostedtoolcache/node/24.14.0/x64/bin/node ./test/integration/import_sb2.js (dns block)
  • turbowarp.org
    • Triggering command: /opt/hostedtoolcache/node/24.14.0/x64/bin/node /opt/hostedtoolcache/node/24.14.0/x64/bin/node ./test/unit/tw_extension_manager.js (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

This section details on the original issue you should resolve

<issue_title>finsih js extension pls img oing insane</issue_title>
<issue_description>the js extension code is from penguinmod and i just stole a bunch of code from there to get it to work here too. the extension itself is stored in extensions/sp_javascriptV2/index.js and then there's a bunch of edited mushed code from pmod in virtual-machine.js and a bunch of other places BUT I KEEP GETTING ERRORS AND ERRORRS AND IT DOESN'T WORK OTPIUBVTEY;ROCJTUHV DFLS;ICJKVHGDKLIOIUCVGFKSDIPOIUVDFGSDIPO[]OIUVYFIOFDP[]P[OIBUGVGIOPFD[]\P[OBIGOFP[]\PONJHIGPG[]F[POI NHGP[G]\TRE

WE MADE SOME PROGRESS BY GETTING THE ACTTUAL JAVASCRIPT CODE TO RUN AND THE SANDBOXING TO WORK BUT I REALLY, REALLY, REALLY NEED THAT ACE EDITOR THING THAT EACH BLOCK HAS WAHHHHHHHHHHHHHHHHH I KEEP GETTING XML ERRORS OR WHATEVER

userscript.js:144
Ignoring non-existent field TEXT in block SPjavascriptV2_codeInput
r.Flyout.show @ userscript.js:144
updateToolbox @ blocks.jsx:340
(anonymous) @ blocks.jsx:318
setTimeout
requestToolboxUpdate @ blocks.jsx:317
componentDidUpdate @ blocks.jsx:273
</issue_description>

<agent_instructions>please make sure to read this issue AND #54 you HAVE to read it so you know what you have already done. Thankfully, that PR actually helped things, but it didn't fully resolve the problem. Previously, the code input was null and errored, but now there's a correctly sized dark box where the input should be, it's just not correctly working. Since the extension is originally from pmod, make sure to look at their code to cross verify https://github.com/PenguinMod/PenguinMod-Vm the code structure is almost the same. The new error I'm getting is:
Uncaught TypeError: Cannot read properties of null (reading 'isInFlyout')
at Object.onInit (index.js:44:32)
at blockly_compressed_vertical.js:2049:457
(anonymous) @ index.js:44
(anonymous) @ blockly_compressed_vertical.js:2049

and the box is correctly sized and dark but there's no actual code editor. also the second input on the extension says { &quot;FOO&quot;: &quot;bar&quot; } when it's supposed to be `{"FOO": "bar"} or something like that</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@supervoidcoder @coderabbitai I'M GONNA EXPLODE FROM ANGERINESS 💥💥💥💥💥💥💥💥 @supervoidcoder @coderabbitai we started work on this extension since OCTOBER OF FRICKING LAST YEAR @supervoidcoder @coderabbitai well technically i didn't even make it BUT I'VE BEEN TRYING TO CORRECTLY STEAL THE CODE FOR MONTHS @supervoidcoder @coderabbitai huh i really thought this function i (borroweed) fom pmod would've fixed it

/**
* Creates escaped text suitable for attributes
* @param {string} unsafe the contents to escape
* @returns {string} escaped contents
*/
const escapeAttribute = unsafe => {
const escaped = xmlEscape(unsafe);
return JSON.stringify(escaped).slice(1, -1);
};
</comment_new>
<comment_new>@supervoidcoder
@coderabbitai copilot made a pr look at #54</comment_new>
<comment_new>@supervoidcoder
@coderabbitai no it doesn't 😆 how the hell is a gazillion trillion indentation issues bugs

it's been there forever and it always blocks ci because the code never follows lint style guide and NEVER WILL</comment_new>


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

…double-escaping

Co-authored-by: supervoidcoder <88671013+supervoidcoder@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix issues in JavaScript extension for sandboxing Port sp_javascriptV2 to PenguinMod's ACE editor approach; fix field default-value double-escaping Mar 6, 2026
@supervoidcoder supervoidcoder marked this pull request as ready for review March 6, 2026 13:58
@supervoidcoder
Copy link
Member

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Code editor now features autocomplete for custom functions
    • Code editor is resizable and draggable for improved usability
    • Enhanced code execution engine with improved sandboxing support
  • Refactor

    • Runtime XML handling improved for better compatibility

Walkthrough

This PR modifies XML escaping semantics for block argument default values in the runtime engine and significantly overhauls the JavaScript V2 extension's code editor integration with ACE-based dynamic loading, autocompletion schema management, and sandbox state persistence.

Changes

Cohort / File(s) Summary
Runtime XML Escaping
src/engine/runtime.js
Updated escaping behavior for block argument defaultValue: changed from escapeAttribute() to direct xmlEscape() without attribute-specific escaping, and removed escaping from field content insertion.
JavaScript V2 Editor Overhaul
src/extensions/sp_javascriptV2/index.js
Replaced inline code editor with dynamic ACE-based loader featuring async package loading, per-block editor instances, draggable resizing, and integrated autocompletion. Refactored code execution (runCode → _compileCode), argument parsing (_parseArguments), and function validation (_isLegalFuncName). Added serialize/deserialize for sandbox state persistence, schema refresh triggers on global function changes, and per-instance Safari detection (isInSafari property).

Sequence Diagram

sequenceDiagram
    participant User as User/Blockly
    participant Ext as SPjavascriptV2
    participant ACE as ACE Editor Loader
    participant Schema as Schema Manager
    participant Compiler as Code Compiler
    participant Sandbox as SandboxRunner
    
    User->>Ext: Initialize extension with block
    Ext->>ACE: Load ACE packages (async)
    ACE-->>Ext: ACE loaded & ready
    Ext->>Ext: Create per-block editor instance
    Ext->>Schema: Build autocompletion from globalFuncs
    Schema-->>Ext: Schema updated
    Ext->>Ext: Initialize editor with defaultValue (SECRET_BLOCK_KEY)
    
    User->>Ext: Enter/modify code in editor
    Ext->>Ext: Update field value via ACE session
    
    User->>Ext: Define new global function
    Ext->>Ext: Validate name (_isLegalFuncName)
    Ext->>Ext: Store in globalFuncs
    Ext->>Schema: Refresh schema with updated globalFuncs
    Schema-->>Ext: Autocompletion list updated
    
    User->>Ext: Execute code block
    Ext->>Compiler: Compile code with injected globals (_compileCode)
    Compiler->>Compiler: Parse arguments (_parseArguments)
    Compiler->>Sandbox: Execute (sandboxed or unsandboxed)
    Sandbox-->>Ext: Result
    Ext-->>User: Return output
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • #35: Extends sp_javascriptV2's unsandboxed execution capabilities and callback dispatch mechanisms that are refactored in this PR's _compileCode overhaul.
  • #38: Directly modifies the JavaScript V2 extension's code-editor field integration and default-value initialization patterns similarly addressed here.
  • #54: Updates runtime.js's handling of block-argument defaultValue XML generation, overlapping with this PR's escaping semantics changes.

Poem

🐰 Hoppy times with code that flows,
ACE editors in nested rows,
Schemas dance, escaping's fixed,
Sandbox states and globals mixed,
One rabbit's quest to edit true! 🌟

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the two main changes: porting the sp_javascriptV2 extension to PenguinMod's ACE editor approach and fixing field default-value double-escaping.
Description check ✅ Passed The description is detailed and directly related to the changeset, explaining both the root causes (null crash and blank editor), the fixes applied, and the technical changes in both files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch copilot/fix-js-extension-errors-again

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/extensions/sp_javascriptV2/index.js (2)

242-260: ⚠️ Potential issue | 🟠 Major

Restore pointer events after resize ends.

Line 242 sets input.style.pointerEvents = "none", but onMouseUp() never turns it back on. After the first resize, the editor can stay unclickable until some unrelated class mutation flips the style again.

💡 Minimal fix
         function onMouseUp() {
           isResizing = false;
+          input.style.pointerEvents = "all";
           ScratchBlocks.mainWorkspace.allowDragging = true;
           parent.setMovable(true);
           document.removeEventListener("mousemove", onMouseMove);
           document.removeEventListener("mouseup", onMouseUp);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/extensions/sp_javascriptV2/index.js` around lines 242 - 260, The resize
handler disables pointer events on the editor input (input.style.pointerEvents =
"none") but onMouseUp never restores it, leaving the editor unclickable after a
resize; update the onMouseUp() function (the same scope that removes the
mousemove/mouseup listeners and toggles
isResizing/ScratchBlocks.mainWorkspace/parent.setMovable) to reset the input
pointer events (e.g., set input.style.pointerEvents back to "" or "auto"),
guarding that input is defined before touching its style, and keep this
restoration alongside the other teardown steps so the editor becomes interactive
again after resizing.

494-505: ⚠️ Potential issue | 🟠 Major

Make deserialization assign the target state instead of toggling it.

deserialize() calls toggleSandbox(), so a second deserialize—or loading into an instance that is already unsandboxed—flips the flag the wrong way. Restore the desired state explicitly and refresh blocks once the permission check passes.

💡 Safer restore logic
   deserialize(data) {
-    if (data.isUnsandboxed) this.toggleSandbox();
+    const shouldBeUnsandboxed = Boolean(data && data.isUnsandboxed);
+    if (shouldBeUnsandboxed === this.isEditorUnsandboxed) return;
+
+    if (!shouldBeUnsandboxed) {
+      this.isEditorUnsandboxed = false;
+      this.runtime.extensionManager.refreshBlocks("SPjavascriptV2");
+      return;
+    }
+
+    this.runtime.vm.securityManager.canUnsandbox("JavaScript").then(isAllowed => {
+      if (!isAllowed) return;
+      this.isEditorUnsandboxed = true;
+      this.runtime.extensionManager.refreshBlocks("SPjavascriptV2");
+    });
   }

Also applies to: 747-749

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/extensions/sp_javascriptV2/index.js` around lines 494 - 505,
deserialize() should not call toggleSandbox() because that flips
isEditorUnsandboxed; instead set the desired state explicitly: call
runtime.vm.securityManager.canUnsandbox("JavaScript").then(isAllowed => { if
(!isAllowed) return; this.isEditorUnsandboxed = <desiredBoolean>;
this.runtime.extensionManager.refreshBlocks("SPjavascriptV2"); }); Replace any
deserialize usages that call toggleSandbox() (and the similar code area around
the other occurrence) so they assign this.isEditorUnsandboxed directly and
refresh blocks only after the permission check passes, referencing
toggleSandbox(), deserialize(), isEditorUnsandboxed,
runtime.vm.securityManager.canUnsandbox, and
runtime.extensionManager.refreshBlocks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/extensions/sp_javascriptV2/index.js`:
- Around line 138-145: The DOM cleanup is missing: open(), modify(), and close()
create new recyclableDiv nodes and listeners without removing previous
instances; update these functions (open, modify, close) to first find and remove
any existing recyclableDiv appended to the parent (or keep a single
module-scoped recyclableDiv and reuse it), detach any event listeners attached
to unloadedEditor/svgGroup_ (undo svgGroup_.setAttribute closures or remove
handlers) and then append or reinitialize the element; reference the
recyclableDiv, unloadedEditor, parent, open(), modify(), close(), and svgGroup_
symbols when making the change so no stale nodes or listeners remain across
mount cycles.
- Around line 16-29: The ACE loading logic (ACE_URL, ACE_PACKAGES,
loadedPackages, importAcePackages and waitForAce()) currently only listens for
onload and can hang indefinitely if assets fail; update importAcePackages to
attach onerror handlers to each created <script> that increments a failure
counter and rejects/marks that package as failed, add a global timeout in
waitForAce() (e.g., Promise.race with a timer) that resolves/rejects after a
configurable delay, and modify the caller that initializes the editor to detect
load failure and instantiate a plain-text fallback editor (simple <textarea> or
contentEditable element) when ACE fails to load or the timeout fires so the UI
never stays blocked.

---

Outside diff comments:
In `@src/extensions/sp_javascriptV2/index.js`:
- Around line 242-260: The resize handler disables pointer events on the editor
input (input.style.pointerEvents = "none") but onMouseUp never restores it,
leaving the editor unclickable after a resize; update the onMouseUp() function
(the same scope that removes the mousemove/mouseup listeners and toggles
isResizing/ScratchBlocks.mainWorkspace/parent.setMovable) to reset the input
pointer events (e.g., set input.style.pointerEvents back to "" or "auto"),
guarding that input is defined before touching its style, and keep this
restoration alongside the other teardown steps so the editor becomes interactive
again after resizing.
- Around line 494-505: deserialize() should not call toggleSandbox() because
that flips isEditorUnsandboxed; instead set the desired state explicitly: call
runtime.vm.securityManager.canUnsandbox("JavaScript").then(isAllowed => { if
(!isAllowed) return; this.isEditorUnsandboxed = <desiredBoolean>;
this.runtime.extensionManager.refreshBlocks("SPjavascriptV2"); }); Replace any
deserialize usages that call toggleSandbox() (and the similar code area around
the other occurrence) so they assign this.isEditorUnsandboxed directly and
refresh blocks only after the permission check passes, referencing
toggleSandbox(), deserialize(), isEditorUnsandboxed,
runtime.vm.securityManager.canUnsandbox, and
runtime.extensionManager.refreshBlocks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a9f1650e-f1ee-4658-a858-9ae2f3c68c57

📥 Commits

Reviewing files that changed from the base of the PR and between 4e8589c and 69c9864.

📒 Files selected for processing (2)
  • src/engine/runtime.js
  • src/extensions/sp_javascriptV2/index.js

Comment on lines +16 to +29
const ACE_URL = "https://cdn.jsdelivr.net/npm/ace-builds@1.32.3/src-min-noconflict/";
const ACE_PACKAGES = [
"ace.js", "ext-language_tools.js",
"mode-javascript.js", "theme-monokai.js"
];

let loadedPackages = 0;
const importAcePackages = () => {
for (const packageName of ACE_PACKAGES) {
const script = document.createElement("script");
script.src = ACE_URL + packageName;
script.async = false;
script.onload = () => loadedPackages++;
document.body.appendChild(script);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

wc -l src/extensions/sp_javascriptV2/index.js

Repository: OmniBlocks/scratch-vm

Length of output: 1876


🏁 Script executed:

cat -n src/extensions/sp_javascriptV2/index.js | sed -n '16,42p'

Repository: OmniBlocks/scratch-vm

Length of output: 1089


🏁 Script executed:

cat -n src/extensions/sp_javascriptV2/index.js | sed -n '160,175p'

Repository: OmniBlocks/scratch-vm

Length of output: 659


🏁 Script executed:

# Find the waitForAce function definition
rg -n "waitForAce" src/extensions/sp_javascriptV2/index.js -A 10 -B 2

Repository: OmniBlocks/scratch-vm

Length of output: 784


Add error and timeout handling to prevent permanent editor hang when ACE assets fail to load.

waitForAce() only resolves when all onload callbacks fire. If external ACE assets are blocked by CSP, DNS failure, offline mode, or CDN unavailability, the Promise never resolves and the editor at line 166 remains permanently blocked. Add onerror handlers to the script elements, implement a timeout mechanism, and provide a plain-text fallback editor when ACE fails to load.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/extensions/sp_javascriptV2/index.js` around lines 16 - 29, The ACE
loading logic (ACE_URL, ACE_PACKAGES, loadedPackages, importAcePackages and
waitForAce()) currently only listens for onload and can hang indefinitely if
assets fail; update importAcePackages to attach onerror handlers to each created
<script> that increments a failure counter and rejects/marks that package as
failed, add a global timeout in waitForAce() (e.g., Promise.race with a timer)
that resolves/rejects after a configurable delay, and modify the caller that
initializes the editor to detect load failure and instantiate a plain-text
fallback editor (simple <textarea> or contentEditable element) when ACE fails to
load or the timeout fires so the UI never stays blocked.

Comment on lines +138 to 145
// element reused by the custom input api
const recyclableDiv = document.createElement("div");
recyclableDiv.setAttribute("style", `display: flex; justify-content: center; padding-top: 10px; width: 250px; height: 200px;`);
recyclableDiv.setAttribute("style", `display: flex; justify-content: center; padding-top: 10px; width: 250px; height: 100px;`);

const unloadedEditor = document.createElement("div");
unloadedEditor.setAttribute("style", "background: #272822; border-radius: 10px; border: none; width: 100%; height: calc(100% - 20px);");
recyclableDiv.appendChild(unloadedEditor);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Teardown cleanup is missing between mount cycles — elements and listeners accumulate in the parent container.

The open(), modify(), and close() functions all create fresh recyclableDiv elements and append them to the same parent container without clearing previous instances. This causes DOM nodes, event listeners (line 160, 220), and closures from the wrapped svgGroup_.setAttribute to persist across invocations. After multiple open/close cycles, multiple listeners will fire simultaneously and operate on stale block references.

Each invocation should remove the old recyclableDiv before appending the new one, or use a single reusable instance with proper state reset.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/extensions/sp_javascriptV2/index.js` around lines 138 - 145, The DOM
cleanup is missing: open(), modify(), and close() create new recyclableDiv nodes
and listeners without removing previous instances; update these functions (open,
modify, close) to first find and remove any existing recyclableDiv appended to
the parent (or keep a single module-scoped recyclableDiv and reuse it), detach
any event listeners attached to unloadedEditor/svgGroup_ (undo
svgGroup_.setAttribute closures or remove handlers) and then append or
reinitialize the element; reference the recyclableDiv, unloadedEditor, parent,
open(), modify(), close(), and svgGroup_ symbols when making the change so no
stale nodes or listeners remain across mount cycles.

@supervoidcoder supervoidcoder merged commit e191cee into develop Mar 6, 2026
0 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finsih js extension pls img oing insane

2 participants