Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 15 minutes
  • Examples run with Node.js 24.21.0, jQuery 4.0.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

The shop's plugins and the snapshot of what npm, OSV and GitHub say

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'];
}
Four facts about each plugin JavaScript · facts.mjs
// 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

  1. Question 1 of 2 owl.carousel's newest release is 2.3.4, from long before jQuery 4.0.0 came out. What does that tell you on its own?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: That its authors cannot have tested that release against jQuery 4.0.0, so it must be run on it

    The age of the last release is a question, not an answer: it says whether its authors could have tested it against jQuery 4, and the run answers whether it works.

  2. Question 2 of 2 Why does the scorecard ask OSV about the release the shop runs, not about the plugin's newest one?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Because an advisory affects a range of releases, and only the running one is exposed

    Query by the version the pages load. The answer then says what to fix now and which release fixes it.

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:

The page and the set-up of each plugin

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, and what it did on jQuery 4.0.0 JavaScript · ranges.mjs
// 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

  1. Question 1 of 2 lightbox2 2.12.0 declares jquery ^3.7.1, which excludes 4.0.0. What happened when the shop's page ran it on 4.0.0?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It ran on 4.0.0 alone, so the range was stricter than the code

    A declared range is the author's statement, and may be old or cautious. The run on 4.0.0 is the fact; a range that excludes 4.0.0 still decides what npm installs.

  2. Question 2 of 2 slick-carousel 1.8.1 and 2.0.0 both declare jquery >=1.8.0, which includes 4.0.0. What did the runs show?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 1.8.1 threw $.type is not a function on 4.0.0 alone; 2.0.0 ran on 4.0.0 alone

    An open range such as >=1.8.0 includes every future release by definition, whether or not anyone tried them. Treat it as a signal, and run each plugin on jQuery 4.0.0.

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

  1. One question The shop is a commercial site and uses fancyBox 3.5.7, licensed GPL-3.0. What does the plugin's own README add?

    Choose one answer.

    Show the answer: One question

    Answer: That commercial applications, sites among them, need a commercial licence

    Read the licence field and the plugin's own licence text before you patch, bundle or keep a plugin: for fancyBox 3 on a commercial site, the licence question comes before the jQuery question.

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:

  1. Remove what no page template uses any more.
  2. Upgrade when a newer release fixes an advisory of yours, or runs on jQuery 4.0.0 alone.
  3. Keep what already runs on jQuery 4.0.0 alone and has no advisory, and plan the move if its project has ended.
  4. Replace what has ended or named a successor, what breaks even with Migrate, and what its licence keeps you from patching.
  5. 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, and the rule that made it JavaScript · decide.mjs
// 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:

Interactive · answer and compare Which way out fits this plugin? Pick one plugin of your site and answer four questions about it. The table rules out what does not fit, and says why.
Does any page template still call it?
Does a newer release of it run on jQuery 4.0.0 alone, or fix an advisory of yours?
Does the release you run already run on jQuery 4.0.0 alone?
Has the project ended, or named a successor, on npm or in its repository?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Five ways out for a plugin, against the work now, the code you own afterwards and the risk to pages
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

  1. Question 1 of 2 jquery-validation 1.19.5 ran on jQuery 4.0.0 alone. Why is the decision still upgrade, not keep?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Because CVE-2025-3573 affects 1.19.5 and is fixed in 1.20.0, and 1.22.1 also declares jQuery 4

    Running is not the only test: a published advisory with a fixed release is a reason to upgrade on its own.

  2. Question 2 of 2 lightbox2 2.12.0 runs on jQuery 4.0.0 alone, but npm says it is in maintenance mode and names Lightbox3. What did the rules decide?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Keep it, and plan the move to its successor

    Keep what works, mark what has ended, and spend this release on what is broken or exposed.

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:

The checks and the two adapters

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 checks against the old plugin and its replacement JavaScript · cookies.mjs
// 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:

Interactive · stage by stage Replacing jquery.cookie without changing a page Step through the switch from the old plugin to its successor, from the checks to the day the old file goes.
Replacing jquery.cookie without changing a page, step 7 of 7. Showing Find every call, Pin the old behaviour (6 of 6 on the old plugin), Write the adapter, Run the same checks (6 of 6 on both releases), Switch with a flag, Remove the old plugin. In focus: Remove the old plugin.a check failsFind every call$.cookie and $.removeCookiePin the old behaviour6 of 6 on the old pluginWrite the adapterjs-cookie, same callsRun the same checks6 of 6 on both releasesSwitch with a flagoff: the old pluginRemove the old pluginonce the flag is on for all

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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

  1. Question 1 of 2 The first adapter on top of js-cookie passed 5 of 6 checks. Which failed, and why?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Reading Namaste+ji, because js-cookie keeps the + while jquery.cookie read it as a space

    Only a check that pins the old behaviour finds such a difference before visitors do: a server-written + read as a space, here fixed with a read converter.

  2. Question 2 of 2 What does the flag give the switch from jquery.cookie to the adapter?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A way back in a minute, because with the flag off the page gets the old plugin and passes the same checks

    Pin the behaviour, switch behind a flag, watch, and remove the old plugin only when the flag has been on everywhere without trouble.

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:

JSON Diff & Compare Save the scorecard of two audits as JSON and compare them: which plugins moved from replace to done, and which new ones appeared.

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.

  1. Question 1 of 10 Which of these are among the four facts the scorecard reads for each plugin?

    Choose every answer that is right.

    Show the answer: Question 1 of 10

    Answer:

    • The jQuery range its package.json declares
    • The advisories that affect the release in use, and its licence
    • Its newest release and when it came out

    Maintenance, declared compatibility, advisories and licence come from npm and OSV; a run on jQuery 4.0.0 then checks the declared range.

  2. Question 2 of 10 jquery.cookie's npm page declares no deprecation. Where did the scorecard find that the project has ended?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: In its GitHub repository, which is archived and described as superseded by JS Cookie

    npm, the repository and the project's own README each carry part of the story; read all three.

  3. Question 3 of 10 bootstrap-datepicker 1.10.1 declares jquery >=3.4.0 <4.0.0 under dependencies, not peerDependencies. What does npm do next to jQuery 4.0.0?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: Installs a private copy of jQuery 3 inside the plugin's folder

    The plugin ran on 4.0.0 alone on the shop's page, so the copy npm adds is waste and a trap for bundlers, which may give the plugin its own jQuery.

  4. Question 4 of 10 Why did Bootstrap 4.6.2 fail on jQuery 4.0.0 even with Migrate 4.0.2 loaded?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: Its own start-up code checks the jQuery version and throws below 1.9.1 or from 4.0.0 on

    Here the range and the code agree, and Bootstrap 4 reached its end of life: the way out is Bootstrap 5, which does not use jQuery.

  5. Question 5 of 10 What must a patched copy of an Apache-2.0 plugin, such as bootstrap-datepicker, carry that MIT does not ask for?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: Prominent notices in the modified files stating that you changed them

    A patch note at the top of the file, as this module's hint plugin got, satisfies both habits at once.

  6. Question 6 of 10 toastr 2.1.4 has no licence field in its package.json. What does the audit do?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: Reads the licence in the project's own files, here MIT in its README, and records where it came from

    The scorecard marks the field as not declared so a person looks it up; lightbox2 is in the same position, with MIT in its README.

  7. Question 7 of 10 jquery-mask-plugin runs on jQuery 4.0.0 alone. Why is its decision remove?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: No page template of the shop uses it any more

    Unused plugins cost download time and audit time and can carry future advisories: remove them first.

  8. Question 8 of 10 A plugin needs Migrate on jQuery 4.0.0, has no newer release, no end announced and an MIT licence. What do the rules decide?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: Patch it, with a recorded diff and the same checks on both releases

    In the shop, no plugin landed there: each one that needed Migrate had a newer release or an announced end. Patching is right only when neither exists.

  9. Question 9 of 10 Which behaviours do the characterization checks pin?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: The ones the shop's pages use, found by searching for $.cookie( and $.removeCookie(

    Written against the old plugin first, the checks become the definition of done for the replacement.

  10. Question 10 of 10 Why does the replacement keep $.cookie and $.removeCookie as an adapter instead of rewriting every page to call js-cookie?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: Because then no page changes, and the switch is one file behind a flag

    Change one thing at a time: first the implementation behind the old calls, later, if wanted, the calls themselves.

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.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.