Skip to content

chore: migrate to esbuild tooling and update Pyodide to 314.0.7 - #38

Open
bajrangCoder wants to merge 16 commits into
mainfrom
chore/esbuild-pyodide-314
Open

bajrangCoder wants to merge 16 commits into
mainfrom
chore/esbuild-pyodide-314

Conversation

@bajrangCoder

@bajrangCoder bajrangCoder commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Summary

Brings the plugin in line with the official Acode plugin template and fixes Python failing to load on some devices. The console UI and behavior are unchanged apart from the fixes below.

Tooling

  • Replace webpack + Babel + PostCSS + Sass with esbuild (npm run dev serves on :3000, npm run build writes plugin.zip)
  • The old build was broken: webpack.config.js imported ./pack-zip.js, which had been moved into .vscode/. pack-zip.js is back at the root (ESM port of the template's)
  • Removed the ngrok dev server (@ngrok/ngrok was never a dependency)
  • dist/lib (Pyodide) had been dropped from git and the !dist/lib/ ignore rule could not re-include it, so clean builds shipped without Python. Pyodide is now an npm dev dependency and its runtime is copied into dist/lib on every build
  • JSX replaced with tag() calls and SCSS with plain CSS (nesting lowered by esbuild, icon font inlined), so no compile plugins are needed
  • Added tsconfig.json for acode-plugin-types, Biome as a dev dependency with npm run check
  • plugin.json: main now points to main.js (where the zip puts it), readme/changelogs set, contributor added

Pyodide 0.28.1 → 314.0.7 (Python 3.14.2)

  • 314 only supports module workers, so the worker is created with { type: "module" }
  • The Pyodide loader and runtime module are bundled into worker.js, so loading no longer depends on how the host serves .mjs files
  • If .wasm is not served as application/wasm, Pyodide only logs a warning and hangs on "loading..." forever. The worker now falls back to ArrayBuffer compilation and reports any remaining failure
  • packageBaseUrl points to the matching jsDelivr build, so import numpy, pandas, micropip, etc. download on demand (previously only the stdlib worked)

Fixes for reported device errors

  • NetworkError: Failed to execute 'importScripts' ... //lib/pyodide.js failed to load: lib/ was missing from builds, and the URL had a double slash. Both fixed
  • Cannot read properties of undefined (reading 'loadPackagesFromImports'): a failed init was still marked as ready and code was sent to the worker anyway. Init failures now show a clear message naming the missing file, code is not run, and the next run retries
  • stderr was routed to stdout; errors are now shown in red
  • Plugin zip shrinks from 18.7 MB to 13.4 MB (stale Pyodide files removed)

Align the build with the official Acode plugin template and fix Python
failing to load on some devices.

Tooling:
- Replace webpack/babel/postcss/sass with esbuild (dev server on :3000)
- Restore pack-zip.js at the repo root (webpack config imported a moved file)
- Copy the Pyodide runtime from npm into dist/lib on every build; dist/lib
  had been dropped from git, so clean builds shipped without Python
- Add tsconfig for acode-plugin-types, Biome dev dependency and check script
- Replace JSX with tag() calls and SCSS with plain CSS

Pyodide:
- Update to 314.0.7 (Python 3.14); run the worker as a module worker
- Bundle the Pyodide loader into worker.js so .mjs MIME types do not matter
- Fall back to ArrayBuffer wasm compilation when .wasm is not served as
  application/wasm, which otherwise hung on "loading..."
- Load packages on demand from the matching jsDelivr CDN (numpy, micropip...)
- Route stderr correctly and show clear load errors with retry on next run

Also bump to 1.2.0, add contributor, fix plugin.json main path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread src/worker.js Fixed
@greptile-apps

greptile-apps Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[High risk] Build tooling and runtime dependency migration.

The PR appears safe to merge based on the changes since the previous review and the resolved prior threads.

Summary

The PR replaces the build pipeline with esbuild, packages the Pyodide runtime, and adds console display and experimental pygame support. Since the previous review, it has changed SDL loading to associate each script with its own runtime API and to ignore stale loads.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Request SDL load] --> B[Assign script load ID]
  B --> C[sdl.js registers API by ID]
  C --> D{ID still current?}
  D -- Yes --> E[Use SDL API]
  D -- No --> F[Discard stale run]
Loading

Reviews (7) · Last reviewed commit: "fix: keep overlapping sdl.js loads from ..."

Comment thread pack-zip.js Outdated
Comment thread esbuild.config.mjs Outdated
Comment thread src/main.js Outdated
Comment thread esbuild.config.mjs Outdated
Comment thread esbuild.config.mjs Outdated
- pack-zip: resolve the root with fileURLToPath (import.meta.dirname
  needs Node 20.11+), run in-process and write via temp file + rename
- esbuild: await packing in onEnd so failures exit non-zero and rebuilds
  never pack concurrently; start from a clean dist so stale files are
  not shipped
- main: construct the worker inside try so an unsupported module worker
  shows an error and allows retry instead of leaving init stuck
- worker: only dispatch own action handlers (CodeQL dynamic method call)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bajrangCoder

This comment was marked as outdated.

Comment thread src/worker.js Outdated
Object.hasOwn needs Chrome 93+ but the build targets Chrome 90 and
esbuild does not polyfill it. On older WebViews the worker threw on the
first init message and the page stayed on "loading..." with no error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bajrangCoder

This comment was marked as outdated.

CodeQL still flagged `actions[action](...)` as an unvalidated dynamic
method call even with the hasOwnProperty guard. Looking handlers up in a
Map means a message's action name can only reach the defined handlers,
never inherited Object methods, and avoids Object.hasOwn (Chrome 93+).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread src/worker.js Fixed
bajrangCoder and others added 9 commits October 2, 2026 18:36
Follow CodeQL's recommended pattern for js/unvalidated-dynamic-method-call:
check the name with Map.prototype.has and verify the handler is a
function before calling it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Long output lines scroll horizontally per line (unchanged default). A
new header button wraps them instead; the choice is saved in
localStorage. Also keep the >> marker from shrinking or breaking when
lines wrap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds Acode's header loader to the console page so it is clear whether a
program is still running or has finished. It hides while the program is
waiting for input(). Also drops the tooltip and active state from the
wrap toggle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The icon-sized spinner was too heavy for the console header. Use a 2px
indeterminate progress line along the bottom edge of the header in the
theme's active color instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Move Pyodide setup into src/runtime.js, shared by the worker and the
  new SDL runtime; Python helpers live in src/python and are installed
  into site-packages
- matplotlib: custom Agg backend (MPLBACKEND) posts figures as PNG to
  the console on plt.show(); unshown figures are shown when a run ends
- Pillow: Image.show() displays in the console
- Experimental SDL (pygame): programs with an async game loop run in a
  main-thread Pyodide drawing to a <canvas id="canvas"> in the console.
  SDL setup is checked with pygame.display.init() and any failure falls
  back to the worker. Keyboard input is limited to the canvas, Acode's
  document title is restored, and re-running or closing the console
  cancels the game
- Worker runs pygame headless (SDL_VIDEODRIVER=dummy): without a canvas
  Emscripten's SDL video crashed Pyodide fatally
- Handle Pyodide fatal errors: report them and restart Python on the
  next run instead of leaving the console stuck
- Hide "already loaded" package messages on every run

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pygame:
- Ordinary blocking game loops now use SDL mode: with JSPI, the calls a
  game loop makes (display.flip/update, Clock.tick, time.wait/delay,
  event.wait) pause via run_sync so the browser can draw and Acode stays
  responsive (python/acode_pygame.py)
- Check JSPI inside a task before running a blocking loop; without it,
  fall back with an explanation instead of freezing Acode
- Stop blocking loops through a flag checked at those pauses, since
  they never reach an await; stopped runs no longer print into the next
  console
- Explain the worker's "dummy not available" error

Images:
- Default matplotlib figure size to the console width (via the
  matplotlibrc read on first import), so text stays readable on phones
- Render at the device pixel ratio and display at true size, so plots
  are sharp on HiDPI screens

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a device, SDL failed with "Failed to connect to localhost/127.0.0.1:443":
on Acode's main page window.fetch can be replaced (e.g. by another plugin
using native HTTP to avoid CORS), and native HTTP cannot reach the
plugin's files at https://localhost, which only exist inside the WebView.
The worker has its own fetch, so it was unaffected.

Requests for the plugin's files and Pyodide packages now go through the
browser's fetch taken from a hidden same-origin iframe; all other
requests still use the page's fetch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rewrite the README around current features (packages, micropip, plots,
experimental pygame, console features), limitations and development
notes, replacing outdated sections. Group the 1.2.0 changelog into new,
improved, fixed and development changes, and add the missing 1.1.4 entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bajrangCoder

Copy link
Copy Markdown
Member Author

@greptile

Comment thread src/main.js Outdated
Comment thread src/sdl.js
Comment thread src/sdl.js Outdated
Comment thread src/sdl.js Outdated
- Ask before running a pygame program on Acode's main page, where it can
  reach the app (Python there is not isolated like the worker); declining
  runs it in the worker without a display, with an option to not ask again
- Do not start a game that was stopped while the SDL runtime or its
  packages were still loading, including while sdl.js itself loads
- Detect real await expressions with ast instead of matching the word,
  and stop blocking loops that cannot pause (no JSPI) after a few frames
  with a clear error instead of freezing Acode
- dispose() on unmount restores window.fetch (or makes the wrapper a
  pass-through if another plugin wrapped it since) and removes the iframe

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bajrangCoder

This comment was marked as outdated.

Comment thread src/sdl.js Outdated
dispose() removed the fetch route but kept the cached SDL runtime, and
the route was only installed on first load, so a re-initialised plugin
could reuse the runtime and download packages through a replaced fetch.

- install the route at the start of every run (no-op while active)
- dispose() drops the cached runtime so it is freed, not reused
- main.js clears its cached sdl.js promise and removes the script tag on
  destroy, so a later init loads a fresh module

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bajrangCoder

This comment was marked as outdated.

Comment thread src/main.js
Removing a <script> tag does not cancel a load in flight, so after an
unmount and remount a stale sdl.js could finish last, overwrite the
shared window.acodePythonSdl, and closing the console would then stop
the wrong SDL instance.

- each load is tagged with a unique data-load-id; sdl.js registers its
  API under that id (via document.currentScript) instead of one global
- main.js keeps the API from its own current load for stop/dispose and
  ignores loads that finish after a newer one started or after destroy
- destroy bumps the run id so runs waiting for sdl.js never start

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bajrangCoder

This comment was marked as outdated.

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.

3 participants