Skip to content

Bug fixes, modern toolchain, 0 vulnerabilities

Choose a tag to compare

@Noodlex Noodlex released this 12 May 18:08
· 30 commits to master since this release

A maintenance pass that closes a backlog of bugs, modernizes the test/build
toolchain, and clears every known vulnerability without breaking drop-in
compatibility with upstream Backbone.

Fixed

  • Memory leak when a non-Backbone listenee's on throws. Events.listenTo
    used to insert the new Listening bookkeeping before calling obj.on(...),
    so a throw left an orphaned reference in _listeningTo. The bookkeeping is
    now rolled back when obj.on throws.
  • Memory leak when a callback re-enters stopListening during a global
    stop.
    Events.stopListening() (with no argument) snapshots all listenee
    ids; if an off handler removed a later id from the snapshot, the outer
    loop hit break and abandoned every remaining listenee. Changed to
    continue.
  • Backbone.sync was mutating the caller's options. Repeated calls with
    the same options compounded the error / beforeSend wrappers, firing
    callbacks and triggers multiple times. sync now operates on a shallow
    clone of the caller's options while still publishing xhr and the
    jQuery textStatus / errorThrown fields on the caller object so the
    public contract is preserved.
  • Collection.set/add silently corrupted state when options.at coerced
    to NaN.
    The model was registered in _byId but never spliced into
    this.models. A non-numeric at is now treated as if it were omitted
    (append at the end).
  • Router.execute threw TypeError when this[name] resolved to a
    non-function
    (e.g. a string handler). It now silently no-ops and the
    route: (empty name) trigger is skipped so listeners no longer see a
    spurious event.
  • View.undelegate threw if $el had been nulled out by a subclass
    _removeElement. Now mirrors undelegateEvents's if (this.$el) guard.
  • History.start() produced a cryptic ReferenceError outside the
    browser
    despite the constructor specifically guarding for non-browser
    use. It now throws a clear Backbone.history.start() requires a browser window error instead.
  • Backbone.sync was emitting the request event with the cloned options
    object
    (the internal clone introduced to stop error/beforeSend from
    compounding). Listeners that mutate opts for instrumentation
    (e.g. tagging a start timestamp on request, reading it on sync) saw
    their mutations land on the discarded clone. request now fires with the
    caller's own options, matching upstream behavior.
  • Backbone.history.stop() threw ReferenceError outside a browser
    when called before any successful start() (typical in test cleanup).
    Now early-returns when History.started is false, mirroring the guard
    in start() and navigate().
  • require('jquery') swallowed every error, not just MODULE_NOT_FOUND.
    ESM/CJS mismatches and corrupt installs now propagate instead of leaving
    Backbone half-initialized. jQuery 4's jQuery requires a window with a document is also tolerated (Node-only Backbone use without DOM is
    unaffected when jQuery 4 is present as a transitive dependency).
  • debugInfo mis-identified bundled browser builds as Node when bundlers
    (Webpack, etc.) injected a polyfilled process. Detection now requires
    process.versions.node.
  • Backbone.VERSION was out of sync with package.json after the 1.6.1
    / 1.6.2 fork bumps. All three (source header, Backbone.VERSION, and
    package.json) are aligned again.

Changed

  • changeId guard expression simplified to
    !_.isEqual(prevId, this.id) && !(prevId == null && this.id == null).
    Behavior unchanged, readability improved.
  • Collection.set(null) returns the collection instead of undefined,
    matching Model.set(null) and keeping add(null) chainable.
  • extend() static-merge priority is now staticProps > child's own preexisting > parent. Previously parent statics could silently clobber
    statics the caller had already attached to a user-supplied constructor.
    The common cases (no preexisting constructor statics) are unchanged —
    parent statics still flow into the child to fill gaps.

Tooling

  • Test runner: PhantomJS → puppeteer-bundled Chrome Headless.
    karma-phantomjs-launcher + phantomjs-prebuilt (archived since 2018,
    unreliable install on modern systems) replaced with karma-chrome-launcher
    • puppeteer, which ships its own Chromium binary. npm test no longer
      requires a system Chrome.
  • .gitattributes added with * text=auto eol=lf to keep line endings
    consistent across OSes regardless of each developer's core.autocrlf
    setting. Fixes the cascade of linebreak-style errors on Windows
    checkouts.
  • ESLint 8 → 10, including the migration from three nested .eslintrc
    files to one flat eslint.config.js. Stylistic rules (indent, quotes,
    semi, linebreak-style, …) moved to @stylistic/eslint-plugin. Deprecated
    rules (no-catch-shadow, no-native-reassign, no-negated-in-lhs,
    no-new-object, no-spaced-func) replaced or removed.
  • All other devDependencies bumped: rollup 3→4, puppeteer 22→24
    (Chromium 127→148), cpy-cli 3→7, replace-in-file 7→8, plus every
    in-range patch/minor.
  • .idea/ and .claude/ added to .gitignore.

Security

  • underscore 1.13.2 → 1.13.8 patches the only CVE that reached
    consumers of this package
    (GHSA-qpx9-hpmf-5gmw:
    unbounded recursion DoS in _.flatten / _.isEqual).
  • The dependency range is now ^1.8.3 instead of >=1.8.3, so a future
    underscore 2.x won't be pulled in untested.
  • npm audit is clean: 0 vulnerabilities. Down from 29 (2 low, 10
    moderate, 16 high, 1 critical) at the start of this release cycle.

Compatibility

  • Verified against jQuery 4.0 (stable since January 2026) in addition
    to the existing 1.8.x → 3.x range. Swapping jQuery 4 into the test
    vendor directory: 450 karma tests green, 4 Node CoffeeScript tests
    green. The only Backbone-side adjustment was widening the Node loader
    guard (see Fixed) to absorb jQuery 4's jQuery requires a window with a document error when loaded server-side without a DOM. None
    of the 8 jQuery APIs Backbone consumes ($(), $.ajax, .on/.off
    with namespaces, .find, .remove, .attr, [0]) was affected by
    the 4.0 upgrade.

Added (tests)

Regression coverage was added for every behavioral fix above:

  • changeId only fires on real id changes (no spurious null/undefined
    transitions)
  • listenTo cleans up _listeningTo when obj.on throws
  • stopListening keeps cleaning remaining listenees after re-entrancy
  • Backbone.sync does not mutate the caller-supplied options
  • Backbone.sync emits request with the caller options (mutations from a
    request listener persist on the same object)
  • Collection.add(model, {at: 'invalid'}) appends instead of corrupting
    the index
  • Collection.set(null) and Collection.add(null) return the collection
  • Router.execute ignores non-function callbacks instead of throwing
  • extend() static merge priority is what the docstring claims

448 → 450 karma tests, all green.