Skip to content

Remove Prototype.js / Scriptaculous, replace with vanilla JS + compatibility shim - #5612

Open
mattdavenport wants to merge 30 commits into
OpenMage:mainfrom
mattdavenport:feature/prototype-removal-compatibility-layer
Open

Remove Prototype.js / Scriptaculous, replace with vanilla JS + compatibility shim#5612
mattdavenport wants to merge 30 commits into
OpenMage:mainfrom
mattdavenport:feature/prototype-removal-compatibility-layer

Conversation

@mattdavenport

@mattdavenport mattdavenport commented May 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Migrates OpenMage off Prototype.js (1.7.3) + Scriptaculous (1.8.2) in favor of native browser JavaScript, controlled by a new dev/js/prototype_mode config flag (full | shim | none).

  • full — load Prototype/Scriptaculous (legacy behavior), plus the prototype-deprecation.js instrumentation wrapper (inert unless activated via ?protodebug=1).
  • shim — load a lightweight compatibility shim (prototype-shim.js) that re-implements the surface area third-party modules tend to rely on ($(), $$(), Element.*, Ajax.*, Class.create, etc.) without polluting String/Array prototypes the same way.
  • none — ship no Prototype layer at all. Core OpenMage JS has been rewritten to vanilla and verified to work without it.

Default is full, so the upgrade is fully backwards-compatible: existing stores keep the real Prototype/Scriptaculous libraries until they opt into shim or none. Unknown/invalid config values are normalized to full. The planned rollout (see docs/prototype-migration.md) is full now → shim default in the next major → library removal in v22.0.

Scope

  • 30 standalone JS files rewritten to vanilla (js/varien/*, js/mage/*, js/mage/adminhtml/*).
  • 175+ .phtml templates with inline JS converted.
  • ext-tree.js (admin category tree + URL rewrites) decoupled from Prototype.js — works in all three modes.
  • Prototype.js + Scriptaculous remain in the tree for full mode; physical removal (Phase 3) is deferred to v22.0 per the phase plan.
  • Background and remaining work tracked in docs/prototype-migration.md.

Notes for reviewers

  • This is opened as a draft intentionally — there is still end-to-end testing to do across prototype_mode=shim and prototype_mode=none.
  • A two-axis review (standards/spec) plus an independent second-pass review of the shim's Prototype fidelity has been applied on-branch: Ajax.Request GET parameters, Ajax.Updater script-eval/callback ordering, Class.create $super gating, Form.serialize/enable/disable contracts, setValue checkbox/multiselect semantics, stopObserving overloads, plus checkout/bundle regressions (review.js HTTP error handling, composite configure multiselect restore) were all fixed.
  • The branch was rebased onto the latest upstream/main immediately before opening this PR; conflicts touched js/mage/adminhtml/form.js, two .phtml templates, and three upstream-deleted Authorize.net DirectPost files (deletions accepted).
  • See docs/prototype-migration.md for the phase-by-phase breakdown and remaining items.

Test plan

  • Storefront smoke test with dev/js/prototype_mode=full (default) — full guest checkout funnel (configurable product w/ swatches → cart → onepage → order placed), zero console errors
  • Storefront smoke test with dev/js/prototype_mode=shim — same funnel, order placed, Prototype.Version === '1.7.3-shim', no Scriptaculous loaded
  • Storefront smoke test with dev/js/prototype_mode=none — same funnel, order placed, window.Prototype/window.$ undefined
  • Admin panel smoke test under all modes — dashboard, product edit (18 tabs, wysiwyg, configurable grid), ExtJS category tree, sales order view, system config incl. the new prototype_mode field; zero console errors in every mode
  • Run existing Cypress suite under all modes — CI now runs the full suite under full, shim, and none (all green), plus a shim API contract spec
  • Verify third-party extensions that depend on Prototype globals still load under shim — cannot be tested exhaustively from core; this is exactly why the mode flag exists: stores with Prototype-dependent extensions stay on full (the default) or shim, and none is opt-in. Extension authors are invited to test against shim.
  • Confirm category tree + URL rewrites still function under all modes — admin category tree renders and loads nodes in full/shim/none (ext-tree.js decoupled from Prototype)
  • Browser-level regression checks for shim Ajax GET params, Ajax.Updater single script eval, Class.create with/without $super, Form serialize/enable/disable, setValue, stopObserving — covered by the in-browser contract spec running in CI shim mode

🤖 Generated with Claude Code

@github-actions github-actions Bot added documentation Component: PayPal Relates to Mage_Paypal Component: Core Relates to Mage_Core Component: Catalog Relates to Mage_Catalog Template : admin Relates to admin template Component: Cms Relates to Mage_Cms Component: Reports Relates to Mage_Reports Component: CatalogInventory Relates to Mage_CatalogInventory Component: Checkout Relates to Mage_Checkout Component: Sales Relates to Mage_Sales Component: Usa Relates to Mage_Usa Component: Customer Relates to Mage_Customer Template : rwd Relates to rwd template Template : base Relates to base template Component: Adminhtml Relates to Mage_Adminhtml Component: Page Relates to Mage_Page Component: Api PageRelates to Mage_Api Component: Captcha Relates to Mage_Captcha Component: Tag Relates to Mage_Tag Component: CurrencySymbol Relates to Mage_CurrencySymbol Component: Wishlist Relates to Mage_Wishlist Component: Widget Relates to Mage_Widget Component: Weee Relates to Mage_Weee Component: Review Relates to Mage_Review Component: Rating Relates to Mage_Rating Component: Persistant Relates to Mage_Persistant Component: Payment Relates to Mage_Payment Component: Newsletter Relates to Mage_Newsletter Component: Giftmessage Relates to Mage_Giftmessage Component: Downloadable Relates to Mage_Downloadable labels May 25, 2026
Matt Davenport and others added 20 commits July 30, 2026 10:03
Rewrote the migration doc from a planning document to an
implementation status tracker. Documents all completed work
(Phases 0-2, Waves 1-7), remaining tasks (~54 .phtml files,
Phase 3 removal, ExtJS dependency, testing), architecture
decisions (config flag, shim, deprecation wrapper), and the
pattern replacement reference. Includes testing checklist
and release timeline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
21 additional admin template files rewritten to vanilla JS:
system/config (edit, js, array, csp, synchronize), system/convert
(process, run, wizard), system/email/template/edit, system/shipping
(applicable_country, ups), system/variable/js, customer/tab/addresses,
customer/validatevat, dashboard, downloadable, paypal config,
promo, rating, report, store switcher, usa/dhl, weee, widget.

~13 files still contain Prototype patterns in deeply intertwined
PHP-generated JS — these work with the compatibility shim.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removes ~15,700 lines of legacy JavaScript libraries:

Deleted files:
- js/prototype/prototype.js (7,595 lines)
- js/prototype/window.js, window_effects.js, window_ext.js
- js/prototype/effects.js, tooltip.js, tooltip_manager.js
- js/prototype/debug.js, extended_debug.js, window_readme.txt
- js/scriptaculous/ (8 files: builder, controls, dragdrop, effects,
  scriptaculous, slider, sound, unittest)

Kept in js/prototype/:
- prototype-shim.js (compatibility layer for extensions)
- prototype-deprecation.js (deprecation warnings)
- validation.js (rewritten to vanilla JS in Wave 6)
- windows/ directory (used by Dialog)

Layout XML updates:
- frontend/base/default/layout/page.xml — prototype → shim, removed
  scriptaculous
- frontend/rwd/default/layout/page.xml — same
- adminhtml/default/default/layout/main.xml — prototype + scriptaculous
  → shim only
- adminhtml/default/default/layout/dataflow.xml — prototype → shim
- frontend/*/layout/catalog.xml — removed scriptaculous.js reference

Config change:
- Default prototype_mode changed from "full" to "shim"
- Sites can still set "full" to restore old behavior (but the files
  are gone, so only "shim" or "none" are meaningful now)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Template.evaluate() method relies on gsub passing match groups
as an array (match[1], match[2], match[3]) to the replacement
callback, matching Prototype.js behavior. The previous implementation
passed native String.replace arguments directly, which broke
Template #{variable} interpolation.

Verified with 84-point smoke test suite covering: globals, Class.create
with inheritance, Object.extend, $H, $w, $A, Ajax, Event, Form,
Template, String/Array/Number extensions, Effects, Function extensions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ran the full Cypress suite against the shim and fixed all JS errors:
- Ajax.Autocompleter: lightweight reimplementation for admin global search
- document.on(): event delegation support (eventName, selector, handler)
- Array.prototype.intersect: needed by configurable swatches
- Element.store/retrieve: per-element data storage for input counters
- Element.getHeight/getWidth/disable/enable: missing DOM helpers
- Null guard in customer addresses setActiveItem (pre-existing bug)

Cypress results: 117/122 pass (same 4 pre-existing failures as base branch)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove Prototype.js dependencies from menu.js, payment.js,
product_options.js, telephone.js, weee.js, opcheckout.js, review.js,
giftmessage.js, minicart.js, app.js and configurableswatches scripts.
Adds stripScripts/evalScripts shims to prototype-shim.js.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace Prototype Class.create / $H / .each / .set/.unset / .toQueryString / .up/.down/.select / .setStyle in adminhtml phtml templates: store/switcher/enhanced, permissions/role_users_grid_js, permissions/user_roles_grid_js, importexport/import/form/before, bundle/edit/bundle, downloadable/samples, catalog/options/type/file
- Rewrite JS modules: input-counter, ext-tree, browser, grid, tabs, tools, loader, product, rules, packaging, wysiwyg/widget
- Add window.js (replaces Prototype-based varienWindow/Dialog)
- Add sortable.js (replaces Scriptaculous Sortable.create)
- Extend prototype-shim.js with missing APIs
- Migrate category edit/tree phtml and widget/instance phtml
- Fix PHP blocks: Review/Add, Review/Edit, Adminpass renderer

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…anilla

Introduces the Prototype.js compatibility switch and continues the removal:
- Core/Helper/Js: dev/js/prototype_mode config with isPrototypeMode{Full,Shim,None}
- Page/Html/Head: load Prototype / the shim according to the selected mode
- system config source Dev/Prototypemode
- js/prototype/prototype-shim: additional compatibility shims
- js/mage/adminhtml: loader, tabs, form, browser, window, variables,
  product/composite/configure, sales, wysiwyg (widget + tinymce setup)
- admin login / forgot-password templates and catalog product view options JS
…ions

Converts remaining adminhtml blocks/templates and varien/adminhtml JS off
Prototype.js, and fixes several runtime regressions found while testing the
admin and storefront:

- Paypal admin System Config (Country/Hint/Location): port document.observe,
  $$, .each, Event.observe and friends to vanilla; fix an ASI hazard where the
  trailing IIFE was parsed as a call on the preceding function-expression
  assignment, leaving togglePaypalSolutionConfigureButton undefined
- api2 permissions roles grid: grid.rows is now a native array, use forEach
  instead of Prototype's .each
- js/varien/autocomplete.js: keep the polling observer running while the field
  has focus so typing past minChars shows suggestions without needing to blur
  and re-focus; stop observing on blur instead of on hide
- js/mage/adminhtml/sales.js validateVat: guard the missing customer-group
  element so VAT validation works on guest orders
- giftmessage inline (onepage checkout): re-execute injected <script> elements
  after innerHTML (restores the former Ajax.Updater evalScripts:true) so the
  gift-options section is hidden by default instead of always visible
- Customer::validate(): only validate the password when it is actually being
  set (new customer, explicit change, or a provided value) so a logged-in
  customer is no longer redirected to account/edit on checkout; validate only
  the date part of dob to avoid a false "not a valid date" for stored datetimes

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make 'full' (Prototype.js + Scriptaculous) the default JS loading mode
instead of 'shim', so out-of-the-box behaviour matches the admin field
comment and keeps legacy scripts working until a site opts into the
lighter shim. Align config.xml, the Mage_Core_Helper_Js fallback and the
system.xml comment, and correct the size figures in that comment to the
real uncompressed values (~73KB shim vs ~377KB full).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add back the real Prototype.js + Scriptaculous library files and the
Head block logic that swaps the compatibility shim for them when
prototype_mode is 'full', with unit tests covering the full/shim/none
selection. Also fix admin AJAX templates surfaced during the migration:
send form_key/content-type on category refresh, show/hide the loader on
category move and tree save, and handle a cancelled attribute-set group
prompt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rewrite the Ext.lib.Event and Ext.lib.Ajax adapters in ext-tree.js to use
native DOM events and XMLHttpRequest instead of Prototype's Event/Ajax/Form
globals, so the admin tree widgets work under prototype_mode=none. Because
loader.js could no longer inject isAjax/form_key via its Ajax.Request
monkey-patch, Ext.lib.Ajax now calls the same _openMageAjaxUrl /
_openMageInjectFormKey helpers itself (guarded), fixing the 302 on category
tree node expand.

Also fix latent gaps in this stripped ExtJS build that surfaced once the
trees load without Prototype:
- define the missing Ext.util.DelayedTask (inline node editing threw
  'is not a constructor', e.g. renaming an attribute-set group);
- make El.fly / El.get resilient to the beforeunload teardown of
  El._flyweights / El.cache (URL rewrite category click navigates via
  window.location, racing the node-select addClass -> 'reading _global').

Replace Prototype-isms in the tree/grid templates (Object.clone ->
Object.assign, grid rows .each -> .forEach) and stop loading the now-dead
extjs/fix-defer.js in none mode (it only reconciles Prototype's defer).
Regenerate ext-tree.min.js.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert the last unguarded Prototype.js/Scriptaculous usages in USPS admin
config blocks and checkout templates to vanilla JS, so prototype_mode=none
no longer throws on these pages. Guarded dual-path code (loader.js, js.js,
salesrulejs.phtml) and the Prototype/Scriptaculous library files are left
untouched — they only run in full/shim mode.

- js/mage/adminhtml/usps_config.js: Event.observe / document.observe('dom:loaded')
  -> addEventListener / DOMContentLoaded
- Usps/AbstractTestButton.php, Usps/Createdimensions.php: document.observe
  -> DOMContentLoaded, new Ajax.Request -> fetch (POST + URLSearchParams),
  preserving the result/resultDiv/button callback contract
- usa/checkout/address/verification.phtml: full <script> rewrite —
  $() -> getElementById, .observe -> addEventListener, Ajax.Request -> fetch,
  .evalJSON -> JSON.parse, .update -> innerHTML, new Element/.insert ->
  createElement/appendChild, .show/.hide -> style.display,
  .addClassName/.removeClassName -> classList, .each -> forEach,
  Object.extend -> Object.assign
- catalog/product/list/upsell.phtml: $$() -> querySelectorAll (commented block)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two latent Prototype.js dependencies surfaced when running the Cypress
suite against a prototype_mode=none store.

1) "Template is not defined" — replace Prototype's Template class
   (new Template(text, syntax).evaluate(data)) with a plain
   String.replace of the {{key}} / #{key} placeholders:
   - Adminhtml/Block/Cms/Page/Edit.php (saveAndContinueEdit)
   - template/widget/instance/js.phtml (setSettings)
   - template/weee/renderer/tax.phtml (add row)
   - template/eav/attribute/options.phtml (base + openmage)
   All other *.evaluate() call sites already use custom vanilla template
   objects, not Prototype, so they are left untouched.

2) Swatch shows the wrong product image — in product-media.js
   getSwatchImage() step 3, the original Prototype code used
   `each(... return false)`, which does NOT break the loop (only
   `throw $break` does), so it iterated all compatible products and the
   LAST match won. The migration changed this to `some(... return true)`,
   which breaks on the FIRST match, selecting a different (visually
   identical but differently named) image. Restore the last-wins
   behaviour with a non-breaking forEach.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ess Sonar/PHPStan/ECS/spellcheck findings

CodeQL (23 high alerts):
- prototype-shim.js: stripTags/stripScripts/extractScripts/evalScripts now
  use inert <template> parsing; align ScriptFragment with upstream Prototype
- varien/js.js getInnerText: clone node + drop script/style instead of regex
- uploader/instance.js: drop _XSSFilter regex, HTML-escape template vars and
  use textContent for progress/error messages
- varien/form.js setDataToChild: build select/input via DOM APIs;
  extraChildParams parsed as attributes on an inert template
- RegionUpdater (varien + adminhtml form.js): assign option.text directly
- configure.js: collect popup scripts from DOM instead of regexing response
- widget.js: create options container via createElement
- tools.js: DOM-built image preview; setLocation now blocks non-HTTP(S) URLs
- grid.js: route reload/export navigation through setLocation
- opcheckout.js: _opcStripTags via template parsing
- packaging.js: drop innerHTML regex fallback in _getElementText

SonarCloud reliability (5 bugs):
- varien/js.js: keep Autocomplete instance reference
- loader.js: collapse identical if/else branches
- telephone.js: regex group no longer matches empty string
- file.phtml: label + onchange for delete checkbox (a11y)

PHPStan: strict comparison in Menu.php, typed iterables in Prototypemode
and Head, drop redundant array_values in HeadTest

ECS: single-quote fixes in Bundle Attributes blocks

Spellcheck: add migration doc terms to wordlist
Wrap code identifiers and file names in backticks (pyspelling ignores
code spans) and add remaining prose terms to the wordlist. Verified
locally with pyspelling against .github/spellcheck.yml.
- Rector: SimplifyQuoteEscapeRector on PR-touched inline-JS PHP blocks,
  constant spacing, drop deprecated ReflectionMethod::setAccessible in
  HeadTest (all from vendor/bin/rector process --config .rector.php)
- file.phtml: escapeHtml() around the translated Delete label; clears
  Sonar phpsecurity:S5131 (translations are DB/CSV-sourced) and matches
  surrounding template style
Prototype shim (js/prototype/prototype-shim.js):
- Ajax.Request: send GET parameters (fetch used the unmutated local url);
  route callback exceptions to onException instead of misreporting them
  as status-0 failures
- Ajax.Updater: accept missing options, honor string insertion positions,
  update content at onComplete time (Prototype order), eval response
  scripts exactly once (previously twice with evalScripts:true, once
  without), and never re-run scripts already present in the target;
  clone options so PeriodicalUpdater cycles don't stack wrappers
- Class.create: only inject $super when the override declares it as its
  first parameter; unconditional injection corrupted ordinary overrides
- Form: getElements returns all controls so enable() can undo disable();
  serialize/serializeElements follow the Prototype options contract
  (hash default, submit handling, file exclusion, multiselect arrays)
- Element#setValue: Prototype checkbox/radio truthiness semantics and
  multi-select array support; explicit string coercion for options
- stopObserving: support (name) and () overloads via an observe registry;
  add window.stopObserving; drop duplicate EP.select definition

Vanilla rewrites:
- checkout/review.js: treat non-2xx responses as failures instead of
  enabling Place Order on an error page; serialize repeated field names
  (billing[street][]) and multi-selects without dropping values
- composite/configure.js: preserve all selected values of bundle
  multiselects across confirm/reopen; measure popup height after making
  it visible so centering works
- opcheckout.js + downloadable.phtml: populate event.memo alongside
  detail so Prototype-era observers keep working in none mode
- telephone.js: remove dead keypress bindings and empty handler

PHP:
- Js.php: normalize unknown prototype_mode values to full instead of
  silently degrading into an implicit shim-like asset state
- Head.php: load prototype-deprecation.js after prototype.js in full
  mode (Phase 0 instrumentation was orphaned); cover both in HeadTest

Docs: prototype-migration.md ext-tree.js rows updated (decoupled).
- New mode-aware spec asserts the emitted script loadout is consistent
  for whichever prototype_mode the site under test runs, and in shim
  mode exercises the prototype-shim.js API contract: Class.create
  $super gating, Ajax.Request GET parameters on the wire, Ajax.Updater
  single script evaluation, Form serialization of repeated names and
  multiselects, enable/disable round-trip, setValue checkbox and
  multiselect semantics, and the stopObserving bulk overload.
- CI: run the full Cypress suite two more times with
  dev/js/prototype_mode overridden to shim and none via the
  environment config loader, in a separate job so the existing
  required check name is unchanged.
Copilot AI review requested due to automatic review settings July 30, 2026 14:03
@mattdavenport
mattdavenport force-pushed the feature/prototype-removal-compatibility-layer branch from 206838c to dd887cc Compare July 30, 2026 14:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@sonarqubecloud

Copy link
Copy Markdown

@addison74
addison74 requested a review from Copilot July 30, 2026 18:38

This comment was marked as duplicate.

@mattdavenport

Copy link
Copy Markdown
Contributor Author

@sreichel I know this is a big one, but could you take a look at this when you have some time? Thanks!

@addison74
addison74 requested a balanced review from Copilot August 11, 2026 21:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Component: Adminhtml Relates to Mage_Adminhtml Component: Api PageRelates to Mage_Api Component: Api2 Relates to Mage_Api2 Component: Bundle Relates to Mage_Bundle Component: Captcha Relates to Mage_Captcha Component: Catalog Relates to Mage_Catalog Component: CatalogInventory Relates to Mage_CatalogInventory Component: Centinel Relates to Mage_Centinel Component: Checkout Relates to Mage_Checkout Component: Cms Relates to Mage_Cms Component: Core Relates to Mage_Core Component: CurrencySymbol Relates to Mage_CurrencySymbol Component: Customer Relates to Mage_Customer Component: Dataflow Relates to Mage_Dataflow Component: Downloadable Relates to Mage_Downloadable Component: Giftmessage Relates to Mage_Giftmessage Component: ImportExport Relates to Mage_ImportExport Component: Newsletter Relates to Mage_Newsletter Component: Page Relates to Mage_Page Component: Paygate Relates to Mage_Paygate Component: Payment Relates to Mage_Payment Component: PayPal Relates to Mage_Paypal Component: Persistant Relates to Mage_Persistant Component: Rating Relates to Mage_Rating Component: Reports Relates to Mage_Reports Component: Review Relates to Mage_Review Component: Sales Relates to Mage_Sales Component: Tag Relates to Mage_Tag Component: Usa Relates to Mage_Usa Component: Weee Relates to Mage_Weee Component: Widget Relates to Mage_Widget Component: Wishlist Relates to Mage_Wishlist documentation JavaScript Relates to js/* Template : admin Relates to admin template Template : base Relates to base template Template : install Relates to install template Template : rwd Relates to rwd template XML Layout

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants