Four pre-existing bugs surfaced while modernizing the codebase
While doing a TypeScript / ESLint / strict-mode tightening pass on a fork of exifr (modernized-js/exifr) I bumped into four bugs that all exist in master (6cbf6e9) of this repo. They aren't regressions — they've been there for a while. Three of them are real runtime / correctness issues, one is a build race. Filing them as a single issue with proposed fixes, since each is a one-or-two-line change.
I am happy to send PRs against this repo if any of them look helpful — let me know.
1. typeof BigInt !== undefined is always true — src/util/BufferView-get64.mjs
Line 16:
} else if (typeof BigInt !== undefined) {
typeof X always returns a string ("undefined", "function", …) and is never the undefined value. The condition is always true. The intended check is comparing against the string 'undefined'.
The else { throwError(...) } branch on line 22 is therefore unreachable in practice. In any environment without BigInt (none of which this lib targets at runtime today, but the intent of the branch is clearly to fail fast), BigInt(part1) would throw a generic ReferenceError instead of the descriptive throwError(...) below.
Fix:
- } else if (typeof BigInt !== undefined) {
+ } else if (typeof BigInt !== 'undefined') {
2. Test loop never executes — test/xmp-segment.spec.mjs
Line 282:
assert.lengthOf(actualSegStarts, 45)
for (let i = 0; i > actualSegStarts; i++) {
const expected = expectedSegStarts[i]
const actual = actualSegStarts[i]
assert.equal(actual, expected)
}
i > actualSegStarts is comparing a number to an array. JavaScript coerces the array to NaN (via [obj] -> string -> NaN for non-numeric arrays), so 0 > NaN is false on the first iteration and the loop body never runs.
The intent is i < actualSegStarts.length. Right now this it(...) block silently asserts nothing about the actual / expected segment-start contents — only the preceding assert.lengthOf(...) runs.
Fix:
- for (let i = 0; i > actualSegStarts; i++) {
+ for (let i = 0; i < actualSegStarts.length; i++) {
3. ua.match(...) result destructured without null check — src/highlevel/orientation.mjs
Three sites: lines 48, 52, 55.
} else if (ua.includes('OS X 10')) {
let [, version] = ua.match(/OS X 10[_.](\d+)/)
rotateCanvas = rotateCss = Number(version) < 15
}
// …
if (ua.includes('Chrome/')) {
let [, version] = ua.match(/Chrome\/(\d+)/)
rotateCanvas = rotateCss = Number(version) < 81
} else if (ua.includes('Firefox/')) {
let [, version] = ua.match(/Firefox\/(\d+)/)
rotateCanvas = rotateCss = Number(version) < 77
}
ua.includes('Chrome/') only confirms the substring is present. The matching regex (/Chrome\/(\d+)/) requires at least one digit after Chrome/. UA strings like …Chrome/<flag> (with no digits or with letter-prefix flags) would pass includes but fail match, returning null. Then the destructure let [, version] = null throws TypeError: Cannot destructure property '1' of 'null' as it is null.
In practice this can't be triggered by mainstream browsers, but it's brittle defensive code, and the OS X 10 case in particular can vary on iOS web-views.
Fix (apply to all three branches):
if (ua.includes('Chrome/')) {
- let [, version] = ua.match(/Chrome\/(\d+)/)
- rotateCanvas = rotateCss = Number(version) < 81
+ const match = ua.match(/Chrome\/(\d+)/)
+ if (match) rotateCanvas = rotateCss = Number(match[1]) < 81
} else if (ua.includes('Firefox/')) {
- let [, version] = ua.match(/Firefox\/(\d+)/)
- rotateCanvas = rotateCss = Number(version) < 77
+ const match = ua.match(/Firefox\/(\d+)/)
+ if (match) rotateCanvas = rotateCss = Number(match[1]) < 77
}
4. rollup cloneCjsAndMjsToJs writeBundle hook doesn't await fs.copyFile — rollup.config.js
function cloneCjsAndMjsToJs() {
return {
writeBundle(bundle) {
let newFileName = bundle.file.replace('.cjs', '.js').replace('.mjs', '.js')
fs.copyFile(bundle.file, newFileName)
}
}
}
fs.copyFile (from fs.promises) returns a Promise that's never returned to rollup, so the build can finish (and the rollup process can exit) while the .js mirror copies are still being written. In practice this hasn't been observed because the disk write completes before Node exits, but it's a race — and it's the entrypoint that package.json#main points to (./dist/full.umd.js is the mirror), so a flaky build can publish a tarball without the mirror.
Fix:
function cloneCjsAndMjsToJs() {
return {
writeBundle(bundle) {
let newFileName = bundle.file.replace('.cjs', '.js').replace('.mjs', '.js')
- fs.copyFile(bundle.file, newFileName)
+ return fs.copyFile(bundle.file, newFileName)
}
}
}
These all came up while doing a strict-mode TypeScript / ESLint pass — they're good demonstrations of why those tools earn their keep, even on a codebase that's been working in production for years. Thanks for the original library; it's lovely to work with.
About the fork that surfaced these
@modernized/exifr (modernized-js/exifr) is a modernization-only fork of [email protected]. Public API is byte-identical (the published index.d.ts is unchanged from upstream); the scope is purely toolchain and runtime envelope:
- TypeScript sources, Node 22.18+ runtime, ES2020+ browser targets
- Drops IE11 / pre-Chromium-Edge support and the Babel / IE-polyfill pipeline (~50 % smaller bundles, ~60 % faster build)
mocha + chai → node:test + node:assert
- Travis → GitHub Actions (Linux + macOS + Windows × Node 22)
- ESLint 10 (flat config) + Prettier +
@arethetypeswrong/cli package-types check in CI
- Bundle outputs preserved:
mini / lite / full × esm.mjs / esm.js / umd.cjs / umd.js (legacy UMD dropped)
Not a competitor — the README is explicit that all credit goes to you and the existing contributors, and users who need IE / older Node should keep using the upstream exifr v7.x line. Just a place to land the modernization without bothering you.
Four pre-existing bugs surfaced while modernizing the codebase
While doing a TypeScript / ESLint / strict-mode tightening pass on a fork of
exifr(modernized-js/exifr) I bumped into four bugs that all exist inmaster(6cbf6e9) of this repo. They aren't regressions — they've been there for a while. Three of them are real runtime / correctness issues, one is a build race. Filing them as a single issue with proposed fixes, since each is a one-or-two-line change.I am happy to send PRs against this repo if any of them look helpful — let me know.
1.
typeof BigInt !== undefinedis always true —src/util/BufferView-get64.mjsLine 16:
typeof Xalways returns a string ("undefined","function", …) and is never theundefinedvalue. The condition is always true. The intended check is comparing against the string'undefined'.The
else { throwError(...) }branch on line 22 is therefore unreachable in practice. In any environment withoutBigInt(none of which this lib targets at runtime today, but the intent of the branch is clearly to fail fast),BigInt(part1)would throw a genericReferenceErrorinstead of the descriptivethrowError(...)below.Fix:
2. Test loop never executes —
test/xmp-segment.spec.mjsLine 282:
i > actualSegStartsis comparing a number to an array. JavaScript coerces the array toNaN(via[obj] -> string -> NaNfor non-numeric arrays), so0 > NaNisfalseon the first iteration and the loop body never runs.The intent is
i < actualSegStarts.length. Right now thisit(...)block silently asserts nothing about the actual / expected segment-start contents — only the precedingassert.lengthOf(...)runs.Fix:
3.
ua.match(...)result destructured without null check —src/highlevel/orientation.mjsThree sites: lines 48, 52, 55.
ua.includes('Chrome/')only confirms the substring is present. The matching regex (/Chrome\/(\d+)/) requires at least one digit afterChrome/. UA strings like…Chrome/<flag>(with no digits or with letter-prefix flags) would passincludesbut failmatch, returningnull. Then the destructurelet [, version] = nullthrowsTypeError: Cannot destructure property '1' of 'null' as it is null.In practice this can't be triggered by mainstream browsers, but it's brittle defensive code, and the OS X 10 case in particular can vary on iOS web-views.
Fix (apply to all three branches):
if (ua.includes('Chrome/')) { - let [, version] = ua.match(/Chrome\/(\d+)/) - rotateCanvas = rotateCss = Number(version) < 81 + const match = ua.match(/Chrome\/(\d+)/) + if (match) rotateCanvas = rotateCss = Number(match[1]) < 81 } else if (ua.includes('Firefox/')) { - let [, version] = ua.match(/Firefox\/(\d+)/) - rotateCanvas = rotateCss = Number(version) < 77 + const match = ua.match(/Firefox\/(\d+)/) + if (match) rotateCanvas = rotateCss = Number(match[1]) < 77 }4. rollup
cloneCjsAndMjsToJswriteBundle hook doesn't awaitfs.copyFile—rollup.config.jsfs.copyFile(fromfs.promises) returns aPromisethat's never returned to rollup, so the build can finish (and the rollup process can exit) while the.jsmirror copies are still being written. In practice this hasn't been observed because the disk write completes before Node exits, but it's a race — and it's the entrypoint thatpackage.json#mainpoints to (./dist/full.umd.jsis the mirror), so a flaky build can publish a tarball without the mirror.Fix:
function cloneCjsAndMjsToJs() { return { writeBundle(bundle) { let newFileName = bundle.file.replace('.cjs', '.js').replace('.mjs', '.js') - fs.copyFile(bundle.file, newFileName) + return fs.copyFile(bundle.file, newFileName) } } }These all came up while doing a strict-mode TypeScript / ESLint pass — they're good demonstrations of why those tools earn their keep, even on a codebase that's been working in production for years. Thanks for the original library; it's lovely to work with.
About the fork that surfaced these
@modernized/exifr(modernized-js/exifr) is a modernization-only fork of[email protected]. Public API is byte-identical (the publishedindex.d.tsis unchanged from upstream); the scope is purely toolchain and runtime envelope:mocha+chai→node:test+node:assert@arethetypeswrong/clipackage-types check in CImini/lite/full×esm.mjs/esm.js/umd.cjs/umd.js(legacy UMD dropped)Not a competitor — the README is explicit that all credit goes to you and the existing contributors, and users who need IE / older Node should keep using the upstream
exifrv7.x line. Just a place to land the modernization without bothering you.