Removing Prototype from Jenkins
Summary
Usage of the Prototype JavaScript framework has been deprecated in recent versions of Jenkins core and will be removed completely in the future. Plugin developers should prepare for this transition by removing usages of Prototype and testing with Prototype removed.
Motivation
Prototype was created by Sam Stephenson in February 2005 as part of Ajax support in Ruby on Rails. While it was considered a major advance at the time, it has now fallen out of favor due to its invasive modifications to standard JavaScript functionality. At this point in time, all Prototype features have native equivalents in the Web Platform. Moreover, the very presence of Prototype’s invasive modifications actively causes compatibility problems with modern JavaScript libraries.
Worse, core currently uses a patched version of Prototype 1.7, released on November 15, 2010. The latest version is 1.7.3, released on September 22, 2015. When an attempt was made to upgrade to 1.7.3 in 2018 in JENKINS-49319, the change had to be reverted.
Clearly the status quo is unsustainable. For these reasons, we have been working to remove Prototype from the Jenkins ecosystem in JENKINS-70906.
Testing core and plugins
As of Jenkins 2.404, a user experimental flag has been added to remove Prototype. The flag can be enabled or disabled on a per-user basis and removes Prototype from all Jenkins UI pages. The flag is intended to be used by core and plugin developers to do testing with Prototype removed. To enable the flag, go to the Configure page for your user in 2.404 or later, scroll to the Experiments section at the bottom, and enable the flag.
As of Jenkins 2.404, most usages of Prototype have been removed from core, and even more will be removed by 2.405. We anticipate that all core usages of Prototype will be removed in a forthcoming weekly release. At that point, the focus will shift to removing Prototype usages from plugins.
To test with Prototype removed, enable the user experimental flag and watch the browser console log for error messages as you exercise various pieces of JavaScript code. If an error occurs with Prototype removed but does not occur with Prototype present, you have found code that needs to be adapted.
Adapting plugins
When adapting plugins, first ensure that the plugin is using plugin parent POM 4.58 or greater. This contains an update to support the Fetch API in the HtmlUnit browser used by the test harness. Without this, you will likely see errors concerning the Fetch API in HtmlUnit tests.
The next thing to do is search for common Prototype usages and convert them to native JavaScript APIs. The following command attempts to search for some common usages in views:
find . -type f \( -name "*.groovy" -o -name "*.jelly" -o -name "*.js" \) -exec grep -HnE '\.each\(|Object\.toJSON|Prototype\.Selector|\$\$\(|\$A|\$F|\.on\(|\.observe\(|\.fire\(|Form\.getInputs|Element\.stopObserving|\.removeClassName\(|\.addClassName\(|\.hasClassName\(|\.nextSiblings\(|\.firstDescendant\(|\.previous\(|\.up\(|\.down\(|\.next\(|\.childElements\(|\.escapeHTML\(|\.show\(\)|\.hide\(\)|\.setStyle\(|\.setOpacity\(|\.getResponseHeader\(|Ajax\.Request|Ajax\.Updater|Ajax\.PeriodicalUpdater' {} \;
This is neither an exhaustive list, nor is it guaranteed to be free from false positives. But it is a good place to start. Below I will give some examples of common usages and their recommended replacements. When in doubt, consult the Prototype API documentation for information about the old usage, and consult the Web Platform documentation for information about recommended replacements.
Once you have removed the usage of Prototype, test your plugin both with and without the user experimental flag enabled. If the line you have changed works with and without Prototype (as verified by stepping into the line with the browser’s JavaScript debugger), then you are ready to merge and release the change.
Cheat sheet
The following are my rough and unpolished notes from doing this conversion a few dozen times. This is a good place to start, but it is is not an exhaustive list of changes that need to be made.
General changes
Replace any usages of .each
with .forEach
.
If the argument to the Prototype $ function is a string, then replace it with document.getElementById
.
For example, replace $("my-id") with document.getElementById("my-id")
.
If the argument to the Prototype $ function is an Element
, then simply remove the call to the Prototype $ function.
For example, replace $(element) with element
.
The Prototype double dollar-sign function should be replaced with either document.querySelector
or document.querySelectorAll
, depending on whether the first result or all results are required.
Also be on the lookout for usages of Prototype.Selector.find
and Prototype.Selector.select
, which can also be replaced by query selectors.
The Prototype $A function should be replaced with Array.from
.
Class names
The next most common set of issues is regarding class names.
-
Replace e.g.
element.hasClassName("my-class")
withelement.classList.contains("my-class")
. -
Similarly, replace e.g.
element.removeClassName("my-class")
withelement.classList.remove("my-class")
. -
Similarly, replace e.g.
element.addClassName("my-class")
withelement.classList.add("my-class")
.
One caveat here is that the Prototype versions of these functions can accept a space-separated string of multiple class names; the native JavaScript versions do not accept this and instead require you to iterate over each class name.
Element manipulation
The next most common set of issues is regarding element manipulation.
-
Replace e.g.
element.childElements()
withelement.children
. -
Replace e.g.
element.down()
withelement.firstElementChild
. -
Replace e.g.
element.firstDescendant()
withelement.firstElementChild
. -
Replace e.g.
element.next()
withelement.nextElementSibling
. -
Replace e.g.
element.previous()
withelement.previousElementSibling
. -
Replace e.g.
element.setOpacity(0)
withelement.style.opacity = 0
. -
Replace e.g.
element.setStyle({foo: bar})
withelement.style.foo = bar
. -
Replace e.g.
element.show()
withelement.style.display = ""
and e.g.element.hide()
withelement.style.display = "none"
. -
Replace e.g.
element.up("div")
withelement.closest("div")
. -
Replace e.g.
element.up()
withelement.parentNode
. -
Replace Prototype-based element creation with
document.createElement
.
Event handling
Another common set of issues is regarding event handling.
-
Replace e.g.
Element.observe(element, "event", callback)
withelement.addEventListener("event", callback)
. -
Replace e.g.
element.observe("event", callback)
withelement.addEventListener("event", callback)
. -
Replace e.g.
Element.on(element, "event", callback)
withelement.addEventListener("event", callback)
. -
Replace e.g.
element.on("event", callback)
withelement.addEventListener("event", callback)
. -
Replace e.g.
Element.stopObserving
withdocument.removeEventListener
. -
Replace e.g.
Event.fire(element, "event")
withelement.dispatchEvent(new Event("event"))
. -
Replace e.g.
Event.on(element, "event", callback)
withelement.addEventListener("event", callback)
.
JSON strings
Calls to Object.toJSON
are problematic.
They need to be converted to JSON.stringify
when Prototype is not present, but JSON.stringify
is actually broken when Prototype is present.
The recommendation is to use a conditional during the transition phase:
// TODO simplify when Prototype.js is removed
if (Object.toJSON) {
// Prototype.js
return Object.toJSON(obj);
} else {
// Standard
return JSON.stringify(obj);
}
Ajax requests
Finally, the most difficult set of changes relates to Ajax requests.
Anything that uses Ajax.Request
, Ajax.Updater
, or Ajax.PeriodicalUpdater
should be converted to using the Fetch API.
The best way to learn how to do this is to study the examples from recent core pull requests.
Note that Ajax.Request
defaults to POST requests, but the Fetch API defaults to GET requests.
If the original code did not specify a method, ensure you are still doing a POST request.
Also note that the Jenkins version of Prototype automatically adds a crumb to POST requests; this must be done explicitly when using the Fetch API by adding a Crumb
header.
Core features a crumb.wrap()
method that takes an existing object (which may be empty) and adds the Crumb
header to it.
application/x-www-form-urlencoded
parameters should be passed to the Fetch API in the body, but beware that HtmlUnit is not compatible with these.
Search core for objectToUrlFormEncoded
for a workaround.
The Fetch API will return a response object.
If the original Prototype code used onSuccess
, you will need to check response.ok
before doing the action;
if the original Prototype code used onCompletion
, you can skip this check.
If you are checking the response for a header with .getResponseHeader
in Prototype, this will need to be replaced with .headers.get
.
If you have read this far, congratulations and good luck!