jQuery 4 (legacy code and migration to DOM APIs) Module 6 – Platforms, plugins and jQuery UI
Auditing and replacing third-party jQuery plugins
Assess each jQuery plugin by its last release, declared jQuery range, advisories and licence, decide its way out, and replace one without changing a page.
What you will learn
- Assess each plugin by its last release, its declared jQuery range, its published advisories and its licence
- Test a plugin on jQuery 4.0.0 instead of trusting the range its package declares
- Decide whether to upgrade, keep, patch, replace or remove each plugin, and defend the decision
- Replace a plugin behind a flag, with checks that pin its old behaviour before anything changes
Before you start
What you’ll learn · Before you start
What you will learn
- Assess each plugin by its last release, its declared jQuery range, its published advisories and its licence
- Test a plugin on jQuery 4.0.0 instead of trusting the range its package declares
- Decide whether to upgrade, keep, patch, replace or remove each plugin, and defend the decision
- Replace a plugin behind a flag, with checks that pin its old behaviour before anything changes
Before you start
On this page
A shop runs twelve third-party jQuery plugins. Set up on jQuery 4.0.0 as the shop’s pages set them up, six ran untouched, five threw a TypeError that jQuery Migrate could bridge, and one refused jQuery 4 outright, with a version check of its own. Eight of the twelve declare a jQuery range in their package.json, and for six of those eight the range predicted the wrong result.
So an audit reads the facts and then runs each plugin. You will rate each plugin by four published facts, test the range it declares against a real run, read what its licence allows, decide one way out per plugin, and replace one without changing a page, the cookie plugin twenty templates depend on.
Four facts rate a plugin, and three come from npm
Four facts say most of what you need about a plugin before anyone touches its code. npm’s registry gives three: the newest release and when it came out, the jQuery range its package declares, and its licence field. OSV gives the fourth: the security advisory: A published notice of a security flaw in a package: what the flaw is, which releases it affects and which release fixes it. Databases such as OSV and the GitHub Advisory Database collect them for npm packages, jquery among them. More in “Why upgrade jQuery: CVEs, support and browsers” that affect the release the site runs, which the OSV API answers for a package and version. The project’s repository adds one more signal: whether it is archived or says it has ended. This program printed all of it for the shop:
site-plugins.json
{
"about": "The jQuery plugins of this lesson's fixture shop: the release its pages load, how many of its page templates use each one, and its repository. snapshot.mjs adds what npm, OSV and GitHub say about each.",
"plugins": [
{
"name": "bootstrap",
"installed": "4.6.2",
"pages": 12,
"repo": "twbs/bootstrap"
},
{
"name": "bootstrap-datepicker",
"installed": "1.10.1",
"pages": 3,
"repo": "uxsolutions/bootstrap-datepicker"
},
{
"name": "@fancyapps/fancybox",
"installed": "3.5.7",
"pages": 3,
"repo": "fancyapps/fancybox"
},
{
"name": "jquery.cookie",
"installed": "1.4.1",
"pages": 20,
"repo": "carhartl/jquery-cookie"
},
{
"name": "jquery-mask-plugin",
"installed": "1.14.16",
"pages": 0,
"repo": "igorescobar/jQuery-Mask-Plugin"
},
{
"name": "jquery-validation",
"installed": "1.19.5",
"pages": 6,
"repo": "jquery-validation/jquery-validation"
},
{
"name": "lightbox2",
"installed": "2.12.0",
"pages": 4,
"repo": "lokesh/lightbox2"
},
{
"name": "magnific-popup",
"installed": "1.2.0",
"pages": 5,
"repo": "dimsemenov/Magnific-Popup"
},
{
"name": "owl.carousel",
"installed": "2.3.4",
"pages": 2,
"repo": "OwlCarousel2/OwlCarousel2"
},
{
"name": "select2",
"installed": "4.0.13",
"pages": 7,
"repo": "select2/select2"
},
{
"name": "slick-carousel",
"installed": "1.8.1",
"pages": 1,
"repo": "kenwheeler/slick"
},
{
"name": "toastr",
"installed": "2.1.4",
"pages": 9,
"repo": "CodeSeven/toastr"
}
]
} plugins-snapshot.json
{
"about": "What npm, OSV and GitHub published about each plugin of site-plugins.json, as snapshot.mjs read it.",
"plugins": [
{
"name": "bootstrap",
"installed": {
"version": "4.6.2",
"published": "2022-07-19",
"jquery": {
"field": "peerDependencies",
"range": "1.9.1 - 3"
},
"deprecated": "This version of Bootstrap is no longer supported. Please upgrade to the latest version.",
"licence": "MIT"
},
"latest": {
"version": "5.3.8",
"published": "2025-08-26",
"jquery": null,
"deprecated": null
},
"advisories": [],
"repository": {
"name": "twbs/bootstrap",
"archived": false,
"description": "The most popular HTML, CSS, and JavaScript framework for developing responsive, mobile first projects on the web."
}
},
{
"name": "bootstrap-datepicker",
"installed": {
"version": "1.10.1",
"published": "2025-09-16",
"jquery": {
"field": "dependencies",
"range": ">=3.4.0 <4.0.0"
},
"deprecated": null,
"licence": "Apache-2.0"
},
"latest": {
"version": "1.10.1",
"published": "2025-09-16",
"jquery": {
"field": "dependencies",
"range": ">=3.4.0 <4.0.0"
},
"deprecated": null
},
"advisories": [],
"repository": {
"name": "uxsolutions/bootstrap-datepicker",
"archived": false,
"description": "A datepicker for twitter bootstrap (@twbs)"
}
},
{
"name": "@fancyapps/fancybox",
"installed": {
"version": "3.5.7",
"published": "2019-03-11",
"jquery": {
"field": "peerDependencies",
"range": ">=1.9.0"
},
"deprecated": null,
"licence": "GPL-3.0"
},
"latest": {
"version": "3.5.7",
"published": "2019-03-11",
"jquery": {
"field": "peerDependencies",
"range": ">=1.9.0"
},
"deprecated": null
},
"advisories": [],
"repository": {
"name": "fancyapps/fancybox",
"archived": true,
"description": "jQuery lightbox script for displaying images, videos and more. Touch enabled, responsive and fully customizable. "
}
},
{
"name": "jquery.cookie",
"installed": {
"version": "1.4.1",
"published": "2014-04-27",
"jquery": null,
"deprecated": null,
"licence": "MIT"
},
"latest": {
"version": "1.4.1",
"published": "2014-04-27",
"jquery": null,
"deprecated": null
},
"advisories": [],
"repository": {
"name": "carhartl/jquery-cookie",
"archived": true,
"description": "No longer maintained, superseded by JS Cookie:"
}
},
{
"name": "jquery-mask-plugin",
"installed": {
"version": "1.14.16",
"published": "2019-07-31",
"jquery": null,
"deprecated": null,
"licence": "MIT"
},
"latest": {
"version": "1.14.16",
"published": "2019-07-31",
"jquery": null,
"deprecated": null
},
"advisories": [],
"repository": {
"name": "igorescobar/jQuery-Mask-Plugin",
"archived": false,
"description": "A jQuery Plugin to make masks on form fields and HTML elements."
}
},
{
"name": "jquery-validation",
"installed": {
"version": "1.19.5",
"published": "2022-07-01",
"jquery": {
"field": "peerDependencies",
"range": "^1.7 || ^2.0 || ^3.1"
},
"deprecated": null,
"licence": "MIT"
},
"latest": {
"version": "1.22.1",
"published": "2026-02-18",
"jquery": {
"field": "peerDependencies",
"range": "^1.7 || ^2.0 || ^3.1 || ^4.0"
},
"deprecated": null
},
"advisories": [
{
"id": "CVE-2025-3573",
"fixed": "1.20.0"
}
],
"repository": {
"name": "jquery-validation/jquery-validation",
"archived": false,
"description": "jQuery Validation Plugin library sources"
}
},
{
"name": "lightbox2",
"installed": {
"version": "2.12.0",
"published": "2026-02-28",
"jquery": {
"field": "dependencies",
"range": "^3.7.1"
},
"deprecated": "Lightbox2 is in maintenance mode. Lightbox3 is available: https://lokeshdhakar.com/projects/lightbox3/",
"licence": null
},
"latest": {
"version": "2.12.0",
"published": "2026-02-28",
"jquery": {
"field": "dependencies",
"range": "^3.7.1"
},
"deprecated": "Lightbox2 is in maintenance mode. Lightbox3 is available: https://lokeshdhakar.com/projects/lightbox3/"
},
"advisories": [],
"repository": {
"name": "lokesh/lightbox2",
"archived": false,
"description": "THE original Lightbox script (v2)."
}
},
{
"name": "magnific-popup",
"installed": {
"version": "1.2.0",
"published": "2024-06-08",
"jquery": null,
"deprecated": null,
"licence": "MIT"
},
"latest": {
"version": "1.2.0",
"published": "2024-06-08",
"jquery": null,
"deprecated": null
},
"advisories": [],
"repository": {
"name": "dimsemenov/Magnific-Popup",
"archived": false,
"description": "Light and responsive lightbox script with focus on performance."
}
},
{
"name": "owl.carousel",
"installed": {
"version": "2.3.4",
"published": "2018-04-20",
"jquery": {
"field": "dependencies",
"range": ">=1.8.3"
},
"deprecated": null,
"licence": "SEE LICENSE IN https://github.com/OwlCarousel2/OwlCarousel2/blob/HEAD/LICENSE"
},
"latest": {
"version": "2.3.4",
"published": "2018-04-20",
"jquery": {
"field": "dependencies",
"range": ">=1.8.3"
},
"deprecated": null
},
"advisories": [],
"repository": {
"name": "OwlCarousel2/OwlCarousel2",
"archived": false,
"description": "DEPRECATED jQuery Responsive Carousel."
}
},
{
"name": "select2",
"installed": {
"version": "4.0.13",
"published": "2020-01-28",
"jquery": null,
"deprecated": null,
"licence": "MIT"
},
"latest": {
"version": "4.1.0",
"published": "2026-05-26",
"jquery": null,
"deprecated": null
},
"advisories": [],
"repository": {
"name": "select2/select2",
"archived": false,
"description": "Select2 is a jQuery based replacement for select boxes. It supports searching, remote data sets, and infinite scrolling of results."
}
},
{
"name": "slick-carousel",
"installed": {
"version": "1.8.1",
"published": "2017-10-03",
"jquery": {
"field": "peerDependencies",
"range": ">=1.8.0"
},
"deprecated": null,
"licence": "MIT"
},
"latest": {
"version": "2.0.0",
"published": "2026-09-13",
"jquery": {
"field": "peerDependencies",
"range": ">=1.8.0"
},
"deprecated": null
},
"advisories": [],
"repository": {
"name": "kenwheeler/slick",
"archived": false,
"description": "the last carousel you'll ever need"
}
},
{
"name": "toastr",
"installed": {
"version": "2.1.4",
"published": "2017-12-08",
"jquery": {
"field": "dependencies",
"range": ">=1.12.0"
},
"deprecated": null,
"licence": null
},
"latest": {
"version": "2.1.4",
"published": "2017-12-08",
"jquery": {
"field": "dependencies",
"range": ">=1.12.0"
},
"deprecated": null
},
"advisories": [],
"repository": {
"name": "CodeSeven/toastr",
"archived": false,
"description": "Simple javascript toast notifications"
}
}
]
} rules.mjs
// The scorecard's rules, used by facts.mjs, ranges.mjs and decide.mjs. Each takes the facts of one plugin: its entry in
// plugins-snapshot.json (npm, OSV, GitHub) and its runs in recorded.mjs.
// jQuery 4.0.0 came out on 17 January 2026 (the jQuery blog's release post); a plugin whose newest release is older
// was published before it, so its authors cannot have tested that release against it.
export const JQUERY_4 = '2026-01-17';
// Does an npm range accept 4.0.0? Enough of npm's range syntax for the ranges plugins declare: alternatives with ||,
// hyphen ranges (1.9.1 - 3), comparators (>=3.4.0 <4.0.0) and carets (^3.7.1, ^4.0).
const parse = (v) => v.replace(/^[v=]/, '').split('.').map(Number);
function cmp(a, b) {
for (let i = 0; i < 3; i++) if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) - (b[i] ?? 0);
return 0;
}
function accepts(part, v) {
const hyphen = /^(\S+)\s+-\s+(\S+)$/.exec(part);
if (hyphen) {
const top = parse(hyphen[2]);
const below = top.length === 3 ? null : top.length === 2 ? [top[0], top[1] + 1, 0] : [top[0] + 1, 0, 0];
return cmp(v, parse(hyphen[1])) >= 0 && (below ? cmp(v, below) < 0 : cmp(v, top) <= 0);
}
return part.split(/\s+/).every((c) => {
const caret = /^\^(.+)$/.exec(c);
if (caret) {
const low = parse(caret[1]);
return cmp(v, low) >= 0 && cmp(v, [low[0] + 1, 0, 0]) < 0;
}
const [, op, ver] = /^(>=|<=|>|<|=)?(.+)$/.exec(c);
const d = cmp(v, parse(ver));
return { '>=': d >= 0, '<=': d <= 0, '>': d > 0, '<': d < 0 }[op] ?? d === 0;
});
}
export const acceptsFour = (range) => range.split('||').some((part) => accepts(part.trim(), [4, 0, 0]));
// How a release ran, set up as the shop does: clean on jQuery 4.0.0 alone, only with Migrate 4.0.2, or not even then.
function outcome(run) {
return !run.lines.some(([kind, text]) => kind === 'error' || /^setup threw /.test(text));
}
export function onFour(runs) {
if (!runs) return null;
if (outcome(runs.four)) return 'clean';
return outcome(runs['four-migrate']) ? 'needs Migrate' : 'breaks';
}
const PERMISSIVE = /^(MIT|ISC|Apache-2\.0|BSD-2-Clause|BSD-3-Clause)$/;
const COPYLEFT = /^(A?GPL|LGPL)-/;
export function licenceClass(licence) {
if (!licence) return 'not declared';
if (/^SEE LICENSE IN/.test(licence)) return 'in a file of its own';
return PERMISSIVE.test(licence) ? 'permissive' : COPYLEFT.test(licence) ? 'copyleft' : 'other';
}
// Has the project said it has ended, or named what comes next? npm's deprecation message, an archived repository, or a
// repository description that says it is deprecated.
const said = (text) => `"${text.trim().replace(/[:.]$/, '')}"`;
export function endSignal(facts) {
const npm = facts.installed.deprecated || facts.latest.deprecated;
if (npm) return `npm: ${said(npm.split(/(?<=\.) /)[0])}`;
if (facts.repository.archived) return `repository archived (${said(facts.repository.description.split('. ')[0])})`;
if (/^deprecated\b/i.test(facts.repository.description ?? '')) return `repository: ${said(facts.repository.description)}`;
return null;
}
// The decision, rule by rule, with the rule that decided it.
export function decide({ pages, facts, installedRun, latestRun }) {
const newer = facts.latest.version !== facts.installed.version;
const fixable = facts.advisories.filter((a) => a.fixed);
if (pages === 0) return ['remove', 'no page template uses it'];
if (fixable.length && newer) return ['upgrade', `${fixable.map((a) => a.id).join(', ')} is fixed in ${fixable.map((a) => a.fixed).join(', ')}`];
if (newer && latestRun === 'clean') return ['upgrade', `${facts.latest.version} runs on 4.0.0 alone`];
const signal = endSignal(facts);
if (installedRun === 'clean' && !facts.advisories.length) return ['keep', signal ? `it runs on 4.0.0 alone; plan the move (${signal})` : 'it runs on 4.0.0 alone'];
if (signal) return ['replace', signal];
if (installedRun === 'breaks') return ['replace', 'it breaks on 4.0.0 even with Migrate'];
if (licenceClass(facts.installed.licence) !== 'permissive') return ['replace', `its licence (${facts.installed.licence ?? 'none stated'}) needs reading before any patch`];
return ['patch', 'it runs with Migrate, and its licence allows a recorded patch'];
} // What npm, OSV and GitHub say about each plugin of the shop (plugins-snapshot.json): its newest release, whether the
// project announced an end or a successor, the advisories of the release the shop runs, and its licence.
import { endSignal, JQUERY_4, licenceClass } from './rules.mjs';
import { site, snapshot } from './show.mjs';
for (const p of site) {
const f = snapshot[p.name];
const newest = f.latest.version === p.installed ? 'no newer release' : `newest ${f.latest.version}${f.installed.jquery && !f.latest.jquery ? ', which does not use jQuery' : ''}`;
const age = f.latest.published >= JQUERY_4 ? 'since jQuery 4.0.0' : 'before jQuery 4.0.0';
console.log(`${p.name} ${p.installed}: ${newest}, last released ${f.latest.published.slice(0, 7)} (${age})`);
const signal = endSignal(f);
const advisories = f.advisories.length ? f.advisories.map((a) => `${a.id}, fixed in ${a.fixed}`).join('; ') : 'none';
const licence = !f.installed.licence ? 'none in package.json' : /^SEE LICENSE IN/.test(f.installed.licence) ? 'in its LICENSE file' : f.installed.licence;
console.log(` advisories: ${advisories}; licence: ${licence} (${licenceClass(f.installed.licence)})`);
if (signal) console.log(` ${signal}`);
} Output
bootstrap 4.6.2: newest 5.3.8, which does not use jQuery, last released 2025-08 (before jQuery 4.0.0)
advisories: none; licence: MIT (permissive)
npm: "This version of Bootstrap is no longer supported"
bootstrap-datepicker 1.10.1: no newer release, last released 2025-09 (before jQuery 4.0.0)
advisories: none; licence: Apache-2.0 (permissive)
@fancyapps/fancybox 3.5.7: no newer release, last released 2019-03 (before jQuery 4.0.0)
advisories: none; licence: GPL-3.0 (copyleft)
repository archived ("jQuery lightbox script for displaying images, videos and more")
jquery.cookie 1.4.1: no newer release, last released 2014-04 (before jQuery 4.0.0)
advisories: none; licence: MIT (permissive)
repository archived ("No longer maintained, superseded by JS Cookie")
jquery-mask-plugin 1.14.16: no newer release, last released 2019-07 (before jQuery 4.0.0)
advisories: none; licence: MIT (permissive)
jquery-validation 1.19.5: newest 1.22.1, last released 2026-02 (since jQuery 4.0.0)
advisories: CVE-2025-3573, fixed in 1.20.0; licence: MIT (permissive)
lightbox2 2.12.0: no newer release, last released 2026-02 (since jQuery 4.0.0)
advisories: none; licence: none in package.json (not declared)
npm: "Lightbox2 is in maintenance mode"
magnific-popup 1.2.0: no newer release, last released 2024-06 (before jQuery 4.0.0)
advisories: none; licence: MIT (permissive)
owl.carousel 2.3.4: no newer release, last released 2018-04 (before jQuery 4.0.0)
advisories: none; licence: in its LICENSE file (in a file of its own)
repository: "DEPRECATED jQuery Responsive Carousel"
select2 4.0.13: newest 4.1.0, last released 2026-05 (since jQuery 4.0.0)
advisories: none; licence: MIT (permissive)
slick-carousel 1.8.1: newest 2.0.0, last released 2026-09 (since jQuery 4.0.0)
advisories: none; licence: MIT (permissive)
toastr 2.1.4: no newer release, last released 2017-12 (before jQuery 4.0.0)
advisories: none; licence: none in package.json (not declared)
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node facts.mjs
Read the dates against one, the day jQuery 4.0.0 came out (release post). Eight plugins had no release since then, so their authors cannot have tested those releases against it: that is a question for the run in the next section, not a verdict. Five carry a sign that their project has ended. npm marks Bootstrap 4.6.2 as no longer supported and lightbox2 as in maintenance mode, with Lightbox3 available; the repositories of fancyBox 3 and jquery.cookie are archived repository: A repository its owners have made read-only: no new issues, changes or releases. On GitHub it says so at the top, and it usually means the project has ended., and jquery.cookie’s names JS Cookie as its successor (jquery-cookie); Owl Carousel’s repository calls it deprecated (Owl Carousel 2). One advisory affects a release the shop runs: CVE-2025-3573 in jquery-validation 1.19.5, fixed in 1.20.0.
Ask about the release you run
Query OSV with the version your pages load, not the newest one: an advisory affects a range of releases, and the answer then names both the problem and the release that fixes it.
Checkpoint
Check: The four facts that rate a plugin
A declared range is a signal; a run on jQuery 4 is the proof
A package’s declared range: The releases of another package that a package's package.json says it works with, in dependencies or peerDependencies, such as >=1.8.0 or ^3.7.1. npm acts on it; whether the code works needs a run., in dependencies or peerDependencies, states the
jQuery releases it works with, and npm acts on that statement: a dependency range that excludes your jQuery gets a nested copy: A second copy of a package that npm installs inside another package's folder, because that package's dependency range excludes the release the project installs: jquery-ui 1.13.3 got its own jquery 3.7.1 beside 4.0.0. More in “Audit a jQuery codebase before you upgrade”, a peer range
stops the install (package.json). Whether the code
works is another matter. Each plugin was set up on one page in jsdom, as the shop’s templates set it up, on jQuery
4.0.0 alone and with jQuery Migrate 4.0.2:
site/plugins.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Shop: every plugin on one page</title>
</head>
<body>
<!-- One page that holds what each of the shop's plugins works on, so record.mjs can set each up as the shop does. -->
<button id="cart" data-toggle="dropdown" class="dropdown-toggle">Cart</button>
<div class="dropdown-menu"><a class="dropdown-item" href="/cart">View cart</a></div>
<input id="dob" value="">
<input id="phone" value="9876543210">
<form id="signup"><input id="email" name="email" class="required email" value=""></form>
<a href="/img/kettle.jpg" data-lightbox="kettle">Kettle</a>
<a href="/img/teapot.jpg" id="zoom">Teapot</a>
<div id="slides" class="owl-carousel"><div>Diwali offers</div><div>New arrivals</div><div>Free delivery over 499</div></div>
<select id="city"><option>Pune</option><option>Patna</option></select>
<div id="offers"><div>1</div><div>2</div><div>3</div></div>
</body>
</html> pages.mjs
// What record.mjs runs. PROBES: for each plugin of site-plugins.json, its file in the npm package (and the files it
// needs first), a few lines that set it up on site/plugins.html the way the shop does and return what it made, and,
// where a newer release exists, the file of that release. COOKIES: the cookie runs of site/cookies.html.
export const PROBES = {
bootstrap: { file: 'bootstrap-4.6.2/package/dist/js/bootstrap.js', needs: ['popper.js-1.16.1/package/dist/umd/popper.js'], init: "$('#cart').dropdown(); return typeof $.fn.dropdown;" },
'bootstrap-datepicker': { file: 'bootstrap-datepicker-1.10.1/package/dist/js/bootstrap-datepicker.js', init: "$('#dob').datepicker({ format: 'dd/mm/yyyy' }); $('#dob').datepicker('setDate', new Date(2026, 0, 26)); return $('#dob').val();" },
'@fancyapps/fancybox': { file: 'fancyapps-fancybox-3.5.7/package/dist/jquery.fancybox.js', init: "$.fancybox.open({ src: '/img/kettle.jpg', type: 'image' }); return $('.fancybox-container').length + ' open';" },
'jquery.cookie': { file: 'jquery.cookie-1.4.1/package/jquery.cookie.js', init: "$.cookie('city', 'Pune', { path: '/' }); return $.cookie('city');" },
'jquery-mask-plugin': { file: 'jquery-mask-plugin-1.14.16/package/dist/jquery.mask.js', init: "$('#phone').mask('00000 00000'); return $('#phone').val();" },
'jquery-validation': { file: 'jquery-validation-1.19.5/package/dist/jquery.validate.js', latest: 'jquery-validation-1.22.1/package/dist/jquery.validate.js', init: "$('#signup').validate(); return 'valid() ' + $('#signup').valid();" },
lightbox2: { file: 'lightbox2-2.12.0/package/dist/js/lightbox.js', init: "$('[data-lightbox]').first().trigger('click'); return $('#lightbox').length + ' lightbox';" },
'magnific-popup': { file: 'magnific-popup-1.2.0/package/dist/jquery.magnific-popup.js', init: "$('#zoom').magnificPopup({ type: 'image' }); $('#zoom').trigger('click'); return $('.mfp-wrap').length + ' open';" },
'owl.carousel': { file: 'owl.carousel-2.3.4/package/dist/owl.carousel.js', init: "$('#slides').owlCarousel({ items: 1 }); return $('#slides .owl-item').length + ' slides';" },
select2: { file: 'select2-4.0.13/package/dist/js/select2.full.js', latest: 'select2-4.1.0/package/dist/js/select2.full.js', init: "$('#city').select2(); return $('.select2-container').length + ' box';" },
'slick-carousel': { file: 'slick-carousel-1.8.1/package/slick/slick.js', latest: 'slick-carousel-2.0.0/package/slick/slick.js', init: "$('#offers').slick(); return $('#offers .slick-slide').length + ' slides';" },
toastr: { file: 'toastr-2.1.4/package/build/toastr.min.js', init: "toastr.success('Saved'); return $('.toast').length + ' toast';" },
};
export const COOKIES = [
{ id: 'plugin-three', label: 'jquery.cookie 1.4.1 on jQuery 3.7.1', files: ['vendor:jquery-3.7.1.min.js', 'packs:jquery.cookie-1.4.1/package/jquery.cookie.js'] },
{ id: 'plugin-four', label: 'jquery.cookie 1.4.1 on jQuery 4.0.0', files: ['vendor:jquery-4.0.0.min.js', 'packs:jquery.cookie-1.4.1/package/jquery.cookie.js'] },
{ id: 'first-four', label: 'js-cookie 3.0.8, first adapter, on 4.0.0', flag: true, files: ['vendor:jquery-4.0.0.min.js', 'packs:js-cookie-3.0.8/package/dist/js.cookie.js', 'site/cookie-adapter-first.js'] },
{ id: 'fixed-four', label: 'js-cookie 3.0.8, fixed adapter, on 4.0.0', flag: true, files: ['vendor:jquery-4.0.0.min.js', 'packs:js-cookie-3.0.8/package/dist/js.cookie.js', 'site/cookie-adapter.js'] },
{ id: 'fixed-three', label: 'js-cookie 3.0.8, fixed adapter, on 3.7.1', flag: true, files: ['vendor:jquery-3.7.1.min.js', 'packs:js-cookie-3.0.8/package/dist/js.cookie.js', 'site/cookie-adapter.js'] },
{ id: 'flag-off', label: 'Flag off: the fixed adapter does nothing', flag: false, files: ['vendor:jquery-3.7.1.min.js', 'packs:jquery.cookie-1.4.1/package/jquery.cookie.js', 'packs:js-cookie-3.0.8/package/dist/js.cookie.js', 'site/cookie-adapter.js'] },
]; // What each plugin declares about jQuery in its package.json, against what it did, set up as the shop does, on
// jQuery 4.0.0 alone and with jQuery Migrate 4.0.2 (plugins-snapshot.json, recorded.mjs).
import { acceptsFour, onFour } from './rules.mjs';
import { recording, site, snapshot } from './show.mjs';
const said = (j) => (!j ? 'declares no jQuery range' : `${j.field === 'peerDependencies' ? 'peer ' : ''}${j.range}: ${acceptsFour(j.range) ? 'includes' : 'excludes'} 4.0.0`);
for (const p of site) {
const runs = recording.plugins[p.name].installed;
const result = onFour(runs);
const thrown = runs.four.lines.find(([kind, text]) => kind === 'error' || /^setup threw /.test(text));
const warns = (runs['four-migrate'].result.migrate ?? []).filter((m) => m.startsWith('WARN')).length;
console.log(`${p.name} ${p.installed}: ${said(snapshot[p.name].installed.jquery)}`);
if (result === 'clean') console.log(' ran on 4.0.0 alone');
else console.log(` 4.0.0 alone: ${thrown[1].replace(/^setup threw |^Uncaught /, '')}; with Migrate: ${result === 'needs Migrate' ? `ran, ${warns} WARN` : 'still fails'}`);
const latest = recording.plugins[p.name].latest;
if (latest) console.log(` ${snapshot[p.name].latest.version}: ${said(snapshot[p.name].latest.jquery)}; ${onFour(latest) === 'clean' ? 'ran on 4.0.0 alone' : onFour(latest)}`);
} Output
bootstrap 4.6.2: peer 1.9.1 - 3: excludes 4.0.0 4.0.0 alone: Error: Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0; with Migrate: still fails bootstrap-datepicker 1.10.1: >=3.4.0 <4.0.0: excludes 4.0.0 ran on 4.0.0 alone @fancyapps/fancybox 3.5.7: peer >=1.9.0: includes 4.0.0 4.0.0 alone: TypeError: $.isArray is not a function; with Migrate: ran, 4 WARN jquery.cookie 1.4.1: declares no jQuery range 4.0.0 alone: TypeError: $.isFunction is not a function; with Migrate: ran, 1 WARN jquery-mask-plugin 1.14.16: declares no jQuery range ran on 4.0.0 alone jquery-validation 1.19.5: peer ^1.7 || ^2.0 || ^3.1: excludes 4.0.0 ran on 4.0.0 alone 1.22.1: peer ^1.7 || ^2.0 || ^3.1 || ^4.0: includes 4.0.0; ran on 4.0.0 alone lightbox2 2.12.0: ^3.7.1: excludes 4.0.0 ran on 4.0.0 alone magnific-popup 1.2.0: declares no jQuery range ran on 4.0.0 alone owl.carousel 2.3.4: >=1.8.3: includes 4.0.0 4.0.0 alone: TypeError: $.camelCase is not a function; with Migrate: ran, 3 WARN select2 4.0.13: declares no jQuery range 4.0.0 alone: TypeError: $.isArray is not a function; with Migrate: ran, 1 WARN 4.1.0: declares no jQuery range; ran on 4.0.0 alone slick-carousel 1.8.1: peer >=1.8.0: includes 4.0.0 4.0.0 alone: TypeError: $.type is not a function; with Migrate: ran, 1 WARN 2.0.0: peer >=1.8.0: includes 4.0.0; ran on 4.0.0 alone toastr 2.1.4: >=1.12.0: includes 4.0.0 ran on 4.0.0 alone
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node ranges.mjs
The declared range and the run disagree both ways. bootstrap-datepicker 1.10.1, jquery-validation 1.19.5 and lightbox2
2.12.0 exclude 4.0.0 and ran on it alone. fancyBox 3.5.7, Owl Carousel 2.3.4 and slick 1.8.1 include it, with open
ranges such as >=1.8.0, and threw on their first removed helper. Only Bootstrap 4.6.2 enforced its range: its own
start-up code refused jQuery 4.0.0, so even Migrate could not help. The same open range of slick was false for 1.8.1
and true for 2.0.0, whose package says it is compatible with jQuery 4
(slick).
Key idea
Read the declared range for what npm will do, then run the plugin on jQuery 4.0.0, alone and with Migrate, for what it does: only the run decides.
Checkpoint
Check: Declared ranges against a real run
The licence decides what you may patch and ship
Before you patch a plugin, bundle it into a theme or keep it on a commercial site, read its licence. MIT, the licence of seven of the shop’s twelve plugins, lets you change and ship the code as long as its notice stays with every copy (MIT License). Others ask for more:
| Licence | In the shop | What it asks of a changed copy you ship |
|---|---|---|
| MIT | 7 plugins | keep the copyright and permission notice |
| Apache-2.0 | bootstrap-datepicker | also mark each changed file as changed (Apache 2.0, section 4(b)) |
| GPL-3.0 | fancyBox 3.5.7 | its own terms for passing changed code on (GPL 3) |
| not declared | toastr, lightbox2 | look it up: both say MIT in their README |
| in a file of its own | Owl Carousel | look it up: its LICENSE file |
fancyBox 3 adds a condition of its own: its README offers GPLv3 for open-source applications and asks commercial applications, sites and themes among them, for a commercial licence (fancyBox 3). For a shop, that settles the plugin before any question about jQuery. Two plugins declare no licence in their package.json at all, which is a gap in the data, not a licence: a person reads the project’s own files and records what they found, and where. Do the same for every plugin you bundle, because the files you ship are copies the licence applies to.
Open-Source Licence Report and NOTICE File Generator Read the licence of every package in your lockfile at once, and flag the ones that need a person to read them.Checkpoint
Check: What a licence allows
Upgrade, keep, patch, replace or remove: one decision per plugin
The facts and the runs turn into one decision per plugin, by rules applied in order, the first that fits deciding:
- Remove what no page template uses any more.
- Upgrade when a newer release fixes an advisory of yours, or runs on jQuery 4.0.0 alone.
- Keep what already runs on jQuery 4.0.0 alone and has no advisory, and plan the move if its project has ended.
- Replace what has ended or named a successor, what breaks even with Migrate, and what its licence keeps you from patching.
- Patch the rest: a plugin that runs with Migrate, with no release to move to, under a licence that allows a recorded patch, as this module’s second lesson did with its hint plugin.
rules.mjs holds the same rules as code. Applied to the shop:
// The decision for each plugin of the shop, by the rules of rules.mjs, with the rule that decided it, and the count of
// each decision.
import { decide, onFour } from './rules.mjs';
import { recording, site, snapshot } from './show.mjs';
const count = {};
for (const p of site) {
const runs = recording.plugins[p.name];
const [decision, why] = decide({ pages: p.pages, facts: snapshot[p.name], installedRun: onFour(runs.installed), latestRun: onFour(runs.latest) });
count[decision] = (count[decision] ?? 0) + 1;
console.log(`${`${p.name} ${p.installed}`.padEnd(30)}${decision.padEnd(9)}${why}`);
}
console.log(Object.entries(count).map(([d, n]) => `${d} ${n}`).join(', ')); Output
bootstrap 4.6.2 replace npm: "This version of Bootstrap is no longer supported"
bootstrap-datepicker 1.10.1 keep it runs on 4.0.0 alone
@fancyapps/fancybox 3.5.7 replace repository archived ("jQuery lightbox script for displaying images, videos and more")
jquery.cookie 1.4.1 replace repository archived ("No longer maintained, superseded by JS Cookie")
jquery-mask-plugin 1.14.16 remove no page template uses it
jquery-validation 1.19.5 upgrade CVE-2025-3573 is fixed in 1.20.0
lightbox2 2.12.0 keep it runs on 4.0.0 alone; plan the move (npm: "Lightbox2 is in maintenance mode")
magnific-popup 1.2.0 keep it runs on 4.0.0 alone
owl.carousel 2.3.4 replace repository: "DEPRECATED jQuery Responsive Carousel"
select2 4.0.13 upgrade 4.1.0 runs on 4.0.0 alone
slick-carousel 1.8.1 upgrade 2.0.0 runs on 4.0.0 alone
toastr 2.1.4 keep it runs on 4.0.0 alone
replace 4, keep 4, remove 1, upgrade 3
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node decide.mjs
Four replacements, three upgrades, four plugins kept and one removed, and no patch: every plugin that needed Migrate had a newer release or an announced end. Bootstrap’s replacement is Bootstrap 5, which no longer uses jQuery (migration guide); version 4 reached its end of life (release schedule). Try the rules on a plugin of your own:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Way out | Work now | Code you own after | Risk to pages |
|---|---|---|---|
| Remove it Takes: deleting its tags and calls | Good delete tags and dead calls: jquery-mask-plugin, on 0 templates | Good nothing left to own | Mixed only if a search for its calls finds none |
| Upgrade it Takes: a newer release of the same plugin | Good select2 4.1.0 and slick 2.0.0 ran on 4.0.0 alone | Good none; the project keeps the code | Mixed a major release can change options: read its notes |
| Keep it Takes: a test on each new jQuery | Good nothing, while it runs on 4.0.0 alone | Good none, until the project stops | Mixed lightbox2 runs, but npm says it is in maintenance mode |
| Patch it Takes: a recorded diff you carry | Mixed a few lines, found with Migrate's WARN list | Mixed the diff, re-applied at each update | Mixed low with the same checks on both releases |
| Replace it Takes: new code or a successor, behind a flag | Poor new code, checks that pin the old behaviour, a flag | Mixed the adapter, or none with a maintained successor | Mixed the first jquery.cookie adapter failed 1 of 6 checks |
How the answers decide
- “Does any page template still call it?” Yes: rules out Remove it. Pages still call it, so removing it breaks them: search for its calls and tags first.
- “Does any page template still call it?” No, a search finds no call: rules out Upgrade it, Keep it, Patch it and Replace it. Nothing calls it, so any work on it is waste: remove it, as the shop did with jquery-mask-plugin.
- “Does a newer release of it run on jQuery 4.0.0 alone, or fix an advisory of yours?” No, or there is none: rules out Upgrade it. There is no release to move to that runs on jQuery 4.0.0 or fixes your advisory.
- “Does the release you run already run on jQuery 4.0.0 alone?” No, it needs Migrate or fails: rules out Keep it. It throws on jQuery 4.0.0 without Migrate, as fancyBox 3.5.7 and jquery.cookie 1.4.1 did.
- “Has the project ended, or named a successor, on npm or in its repository?” Yes, deprecated or archived and “Does the release you run already run on jQuery 4.0.0 alone?” No, it needs Migrate or fails: rules out Patch it. A patch keeps alive a plugin its own project has ended; move to the successor instead.
- “Has the project ended, or named a successor, on npm or in its repository?” Yes, deprecated or archived and “Does the release you run already run on jQuery 4.0.0 alone?” Yes, with nothing in Migrate's list: adds a caveat to Keep it. It runs today, but its project has ended or named a successor: keep it, and plan the move.
Replacing everything that is old
Wrong: replace every plugin with an old last release. Right: toastr 2.1.4 and magnific-popup 1.2.0 are years old and ran on jQuery 4.0.0 alone. Spend the release on what is broken, exposed or ended, and keep what works.
Checkpoint
Check: One decision per plugin
Replace behind a flag, after pinning the old behaviour
jquery.cookie 1.4.1 is called from 20 of the shop’s templates and throws on jQuery 4.0.0, at $.isFunction(). Its
successor, js-cookie, is a different library with its own defaults, so the replacement has two parts. First, six
checks pin what the pages ask of $.cookie and $.removeCookie, written against the old plugin on jQuery 3.7.1 before
anything changes: characterization test: A check written against code as it behaves today, before anything changes, so that a replacement can be held to the same behaviour: here six checks of $.cookie written against jquery.cookie 1.4.1.. Then an
adapter: Code that keeps an old interface, such as $.cookie(), and runs a different implementation behind it, so callers do not change when the implementation does. keeps those two calls and runs js-cookie behind them, switched on by a
feature flag: A setting that decides at run time which of two versions of some code a page gets, so a change can be switched on, watched and switched off again without a new release., so no page changes and the switch can go back:
site/cookie-checks.js
// Six checks that pin what the shop's pages ask of $.cookie and $.removeCookie, written against jquery.cookie 1.4.1 on
// jQuery 3.7.1 before anything changed. Each prints PASS or FAIL with what it found.
jQuery(function ($) {
function check(name, test) {
var result;
try {
result = test();
} catch (error) {
result = error.name + ': ' + error.message;
}
console.log((result === true ? 'PASS ' : 'FAIL ') + name + (result === true ? '' : ' (' + result + ')'));
}
var shown = function (value) {
return value === undefined ? 'undefined' : JSON.stringify(value);
};
check('writes a space as %20', function () {
$.cookie('city', 'Navi Mumbai', { path: '/' });
return document.cookie.indexOf('city=Navi%20Mumbai') !== -1 || 'document.cookie is ' + document.cookie;
});
check('reads it back', function () {
var value = $.cookie('city');
return value === 'Navi Mumbai' || 'got ' + shown(value);
});
check('reads a + the server wrote as a space', function () {
document.cookie = 'greeting=Namaste+ji; path=/';
var value = $.cookie('greeting');
return value === 'Namaste ji' || 'got ' + shown(value);
});
check('keeps an encoded plus', function () {
document.cookie = 'code=A%2BB; path=/';
var value = $.cookie('code');
return value === 'A+B' || 'got ' + shown(value);
});
check('a missing cookie is undefined', function () {
var value = $.cookie('nothing-here');
return value === undefined || 'got ' + shown(value);
});
check('removeCookie deletes it and says so', function () {
var removed = $.removeCookie('city', { path: '/' });
return (removed === true && $.cookie('city') === undefined) || 'returned ' + shown(removed) + ', then ' + shown($.cookie('city'));
});
$.cookie('consent', 'yes', { expires: 7, path: '/' });
}); first adapter · site/cookie-adapter-first.js
// cookie-adapter.js, first try: $.cookie and $.removeCookie on top of js-cookie, so the pages' calls stay as they are.
// It runs only when the shop's flag says so; otherwise jquery.cookie stays.
(function ($, Cookies) {
if (!window.shopFlags || !window.shopFlags.jsCookie) return;
$.cookie = function (name, value, options) {
if (value === undefined) return Cookies.get(name);
Cookies.set(name, value, options);
return document.cookie;
};
$.removeCookie = function (name, options) {
if (Cookies.get(name) === undefined) return false;
Cookies.remove(name, options);
return Cookies.get(name) === undefined;
};
})(jQuery, window.Cookies); site/cookie-adapter.js
// cookie-adapter.js: $.cookie and $.removeCookie on top of js-cookie, so the pages' calls stay as they are, with one
// difference from js-cookie's defaults: like jquery.cookie, it reads a + the server wrote as a space.
// It runs only when the shop's flag says so; otherwise jquery.cookie stays.
(function ($, Cookies) {
if (!window.shopFlags || !window.shopFlags.jsCookie) return;
var jar = Cookies.withConverter({
read: function (value) {
return Cookies.converter.read(value.replace(/\+/g, ' '));
},
});
$.cookie = function (name, value, options) {
if (value === undefined) return jar.get(name);
jar.set(name, value, options);
return document.cookie;
};
$.removeCookie = function (name, options) {
if (jar.get(name) === undefined) return false;
jar.remove(name, options);
return jar.get(name) === undefined;
};
})(jQuery, window.Cookies); site/cookies.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Shop: cookie checks</title>
</head>
<body>
<p>The page that runs the cookie checks. The server prints shopFlags first, then jQuery and either jquery.cookie or
js-cookie with the adapter, then the checks. This is the page with the flag on.</p>
<script>window.shopFlags = { jsCookie: true };</script>
<script src="https://code.jquery.com/jquery-4.0.0.min.js" integrity="sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=" crossorigin="anonymous"></script>
<!-- Flag on: js-cookie and the adapter. Flag off: /js/jquery.cookie.js in their place. -->
<script src="/js/js.cookie.js"></script>
<script src="cookie-adapter.js"></script>
<script src="cookie-checks.js"></script>
</body>
</html> // The six cookie checks against jquery.cookie and its replacement (site/cookie-checks.js, recorded.mjs), and how long
// the consent cookie the checks set at the end lives, as the browser's cookie jar stores it.
import { recording } from './show.mjs';
for (const run of Object.values(recording.cookies)) {
const checks = run.lines.filter(([, text]) => /^(PASS|FAIL) /.test(text)).map(([, text]) => text);
const passed = checks.filter((c) => c.startsWith('PASS')).length;
const days = run.result.consentDays;
console.log(`${run.label}: ${passed} of ${checks.length} pass${days === null ? '' : `, consent cookie for ${days} days`}`);
for (const c of checks.filter((c) => c.startsWith('FAIL'))) console.log(` ${c}`);
} Output
jquery.cookie 1.4.1 on jQuery 3.7.1: 6 of 6 pass, consent cookie for 7 days jquery.cookie 1.4.1 on jQuery 4.0.0: 1 of 6 pass FAIL writes a space as %20 (TypeError: $.isFunction is not a function) FAIL reads it back (got undefined) FAIL reads a + the server wrote as a space (TypeError: $.isFunction is not a function) FAIL keeps an encoded plus (TypeError: $.isFunction is not a function) FAIL removeCookie deletes it and says so (returned false, then undefined) js-cookie 3.0.8, first adapter, on 4.0.0: 5 of 6 pass, consent cookie for 7 days FAIL reads a + the server wrote as a space (got "Namaste+ji") js-cookie 3.0.8, fixed adapter, on 4.0.0: 6 of 6 pass, consent cookie for 7 days js-cookie 3.0.8, fixed adapter, on 3.7.1: 6 of 6 pass, consent cookie for 7 days Flag off: the fixed adapter does nothing: 6 of 6 pass, consent cookie for 7 days
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node cookies.mjs
The old plugin passed 6 of 6 on jQuery 3.7.1 and 1 of 6 on 4.0.0. The first adapter passed 5 of 6: js-cookie read the
server’s Namaste+ji with its plus, where jquery.cookie had read a space, because js-cookie’s default decoding is
meant for cookies js-cookie wrote itself, and its README points to a converter for the rest
(js-cookie). With a read converter that turns + into a space, the adapter
passed 6 of 6 on both releases, and with the flag off the old plugin still did. Step through the whole switch:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Find what the pages ask. A search for $.cookie( and $.removeCookie( lists every call the pages make. Replacing a plugin means keeping exactly those behaviours, not every feature it has.
- Pin it before you change it. Six checks, written against jquery.cookie 1.4.1 on jQuery 3.7.1, all pass: spaces written as %20, a + read as a space, an encoded plus kept, removeCookie answering true.
- The adapter. The adapter defines $.cookie and $.removeCookie on top of js-cookie, the successor the old plugin's repository names, so no page has to change.
- The first run fails one. On jQuery 4.0.0 the first adapter passed 5 of 6: js-cookie read Namaste+ji with its +, where jquery.cookie had read a space.
- Fixed, then run again. A read converter that turns + into a space before decoding gives 6 of 6, on jQuery 4.0.0 and on 3.7.1.
- Behind a flag. shopFlags.jsCookie decides which file the page gets. With the flag off, the adapter does nothing and jquery.cookie still passes 6 of 6, so the switch can go back in a minute.
- The old file goes. Once every page runs with the flag on and the errors stay at none, delete jquery.cookie, then the flag.
Key idea
Pin what the pages use with checks written against the old plugin, switch behind a flag that can go back, and remove the old file only when the flag has been on everywhere.
Checkpoint
Check: Replacing behind a flag
Practise: read cookies the way the old plugin did
Some teams replace jquery.cookie with a few lines of their own instead of a library. Write that reader: it must return what jquery.cookie 1.4.1 returned for the same cookie text, server-written pluses and quoted values included. The sample tests use values recorded from the plugin itself:
Exercise · Medium · JavaScript
Read a cookie the way jquery.cookie did, without jQuery
The shop is replacing jquery.cookie 1.4.1. Its pages read cookies with $.cookie(name), and some of the cookies were written by the server, not by the plugin. Write readCookie(header, name) in cookie.mjs, a reader without jQuery that returns what jquery.cookie 1.4.1 returned for the same cookies. header is the text of document.cookie, such as city=Navi%20Mumbai; lang=hi. The rules the plugin follows:
- The header is a list of
name=valueparts separated by;. An empty header holds no cookie. - A part's name is everything before its first
=, decoded withdecodeURIComponent; its value is everything after it, so a value may hold=itself. - The first part whose decoded name is exactly
namedecides. When no part has that name, the result isundefined. - A value that starts with
"is a quoted value: drop its first and last character, then turn\"into"and\\into\. - Every
+in the value becomes a space (some back ends, PHP'ssetcookie()among them, write a space that way), then the value is decoded withdecodeURIComponent. A value that cannot be decoded givesundefined.
The sample tests give each case with the value jquery.cookie 1.4.1 returned for it on jQuery 3.7.1.
Starter code · cookie.mjs
/**
* Reads one cookie from the text of document.cookie the way jquery.cookie 1.4.1 read it with $.cookie(name).
* @param {string} header the text of document.cookie
* @param {string} name
* @returns {string | undefined}
*/
export function readCookie(header, name) {
for (const part of header.split('; ')) {
const [key, value] = part.split('=');
if (key === name) return decodeURIComponent(value);
}
return undefined;
} The sample tests · cookie.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { readCookie } from './cookie.mjs';
// Each expected value is what jquery.cookie 1.4.1 returned for the same cookie text, on jQuery 3.7.1.
test('a value with an encoded space', () => {
assert.equal(readCookie('city=Navi%20Mumbai; lang=hi', 'city'), 'Navi Mumbai');
});
test('a + the server wrote is a space, an encoded plus stays a plus', () => {
assert.equal(readCookie('greeting=Namaste+ji', 'greeting'), 'Namaste ji');
assert.equal(readCookie('code=A%2BB', 'code'), 'A+B');
});
test('a value may hold = itself', () => {
assert.equal(readCookie('token=a=b=c', 'token'), 'a=b=c');
});
test('a quoted value', () => {
assert.equal(readCookie('note="say \\"hi\\""', 'note'), 'say "hi"');
});
test('the first exact name decides', () => {
const header = 'cart=2; cart-id=9; cart=5';
assert.equal(readCookie(header, 'cart'), '2');
assert.equal(readCookie(header, 'cart-id'), '9');
assert.equal(readCookie('pin%20code=560034', 'pin code'), '560034', 'names are decoded too');
});
test('missing, empty and undecodable give undefined', () => {
assert.equal(readCookie('lang=hi', 'city'), undefined);
assert.equal(readCookie('', 'city'), undefined);
assert.equal(readCookie('bad=%E0%A4%A', 'bad'), undefined);
});
test('a value in another script, the rupee sign', () => {
assert.equal(readCookie('a=1; price=%E2%82%B9499', 'price'), '₹499');
}); A hint
Loop over header.split('; '). For each part, take const [raw, ...rest] = part.split('=') and compare decodeURIComponent(raw) with the name; the value is rest.join('='). Replace the pluses before you decode, with value.replace(/\+/g, ' '), and put the decodeURIComponent call in a try block, returning undefined from the catch.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then decide for plugins the shop does not have. Your weakest of the five ideas comes first, and each choice explains the rule it follows or breaks:
Practice that adapts to you
Practise this lesson
10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): what does a plugin’s declared jQuery range tell you, and what does it not?
It tells you what the author promised when they published that release, and what npm will do with it. A range in
dependencies that excludes your jQuery makes npm install a private copy for the plugin; one in peerDependencies
makes npm stop the install. It does not tell you whether the code runs. Old ranges such as >=1.8.0 include every
later jQuery, tested or not: slick 1.8.1 declares that and throws on jQuery 4.0.0. Strict ranges can be too cautious:
lightbox2 2.12.0 declares ^3.7.1 and ran on 4.0.0 alone. So read the range for npm’s behaviour, and run the plugin
on the new jQuery, alone and with Migrate, for the answer.
Two harder questions follow, about deciding for three plugins and replacing one under live traffic. Decide what you would say before you open each first step:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
Before a move to jQuery 4, a site runs Bootstrap 4.6.2's JavaScript, Owl Carousel 2.3.4 and bootstrap-datepicker 1.10.1. The team wants one rule: replace everything older than jQuery 4. What do you decide for each of the three, and why is their rule wrong?
-
Mid to staff
A shop's 20 page templates call $.cookie() from jquery.cookie 1.4.1, which throws on jQuery 4. Some of the cookies are written by the PHP back end. How do you replace the plugin on the live site without logging anyone out or losing a basket?
Key takeaways
- Four published facts rate a plugin: its newest release and when it came out, its declared jQuery range, the advisories of the release in use and its licence, with the repository saying whether the project has ended.
- A declared range says what npm will do; only a run on jQuery 4.0.0, alone and with Migrate, says what the plugin does, and in the shop the two disagreed for six of the eight plugins that declare a range.
- The licence decides what you may patch and ship: MIT keeps its notice, Apache-2.0 marks changed files, and fancyBox 3 asks commercial sites for a commercial licence.
- One decision per plugin, by rules in order: remove the unused, upgrade to a release that runs or fixes an advisory, keep what runs, replace what has ended or breaks, and patch only what is left.
- Replace a plugin behind an adapter and a flag, after checks written against the old plugin pin what the pages use; the first adapter failed one of six, which is why the checks come first.
Terms in this lesson
- adapter
- Code that keeps an old interface, such as $.cookie(), and runs a different implementation behind it, so callers do not change when the implementation does.
- archived repository
- A repository its owners have made read-only: no new issues, changes or releases. On GitHub it says so at the top, and it usually means the project has ended.
- characterization test
- A check written against code as it behaves today, before anything changes, so that a replacement can be held to the same behaviour: here six checks of $.cookie written against jquery.cookie 1.4.1.
- declared range
- The releases of another package that a package's package.json says it works with, in dependencies or peerDependencies, such as >=1.8.0 or ^3.7.1. npm acts on it; whether the code works needs a run.
- feature flag
- A setting that decides at run time which of two versions of some code a page gets, so a change can be switched on, watched and switched off again without a new release.
References
- npm registry: package documents (releases, ranges, licences, deprecations) (npm)
- package.json: dependencies and peerDependencies (npm)
- semver: the ranges npm understands (npm)
- OSV API: query by package and version (OSV (Open Source Vulnerabilities))
- jQuery 4.0.0 Released (OpenJS Foundation (jQuery blog))
- jquery-cookie: repository (archived) (jquery-cookie project)
- js-cookie: README (encoding and converters) (js-cookie project)
- Owl Carousel 2: repository (Owl Carousel 2 project)
- fancyBox 3: package and licence (npm (Fancyapps))
- slick: package (version 2.0 and jQuery 4) (npm (slick project))
- Bootstrap release and support schedule (Bootstrap)
- Bootstrap 5.3: Migrating to v5 (Bootstrap)
- The MIT License (Open Source Initiative)
- Apache License, Version 2.0 (The Apache Software Foundation)
- GNU General Public License, version 3 (Free Software Foundation)
- RFC 6265: HTTP State Management Mechanism (IETF)
- jsdom (jsdom project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress