build: put the @babel pins back, and make them fail loudly if dropped again (#653)

The pins that fixed the blank React /map were removed as collateral when the
Mantine commit rewrote `overrides` for TypeScript 5.  Nothing failed, so nothing
objected: the map kept working only because package-lock.json still happened to
resolve @babel/core to 7.20.12, and devices run `npm install`, not `npm ci`.

Verified on this tree rather than assumed.  Raising @babel/core to 7.29.7 and
reinstalling floats the whole pipeline (traverse/generator/types to 7.29.8) and
`npm run build` then fails outright: "'opera_mobile' is not a valid target",
from a helper-compilation-targets that floated away from the browserslist data
it was pinned against.  With the pins restored the build compiles, all 141
resolved copies match, and /map renders from the built bundle.

The test walks every path in the lockfile, not just node_modules/<name>: npm
nests a second copy whenever versions conflict, and a nested copy that escaped
the override would put the new pipeline back into the build while the top-level
entry still read correctly.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
lrnselfreliance 2026-08-08 18:51:57 -06:00 • committed by Roland
parent f1bc52d9c6
commit c560ba6fa5
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
3 changed files with 462 additions and 353 deletions

706
app/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -72,6 +72,22 @@
]
},
"overrides": {
"@babel/code-frame": "7.29.0",
"@babel/compat-data": "7.20.14",
"@babel/core": "7.20.12",
"@babel/generator": "7.29.1",
"@babel/helper-compilation-targets": "7.20.7",
"@babel/helper-globals": "7.28.0",
"@babel/helper-module-imports": "7.28.6",
"@babel/helper-module-transforms": "7.28.6",
"@babel/helper-string-parser": "7.27.1",
"@babel/helper-validator-identifier": "7.28.5",
"@babel/helper-validator-option": "7.18.6",
"@babel/helpers": "7.27.0",
"@babel/parser": "7.29.3",
"@babel/template": "7.28.6",
"@babel/traverse": "7.29.0",
"@babel/types": "7.29.0",
"react-scripts": {
"typescript": "$typescript"
},

View file

@ -0,0 +1,93 @@
import fs from 'fs';
import path from 'path';
/*
* The @babel core pipeline is pinned, and has to stay pinned.
*
* Floating it broke the React /map page: maplibre-gl's Web Worker is inlined into the
* production bundle, Babel 7.29 transpiles it differently, and CRA's Terser mangle pass then
* turns it into `ReferenceError: a is not defined`. The worker's style-layer index never
* builds, every vector source fails to parse, and the map renders blank with only the pins.
*
* Two properties make this worth a test rather than a comment:
*
* - It is invisible to everything else we run. The dev server is fine, the jest suite is
* fine, `npm run build` succeeds -- only the MINIFIED bundle is broken, and only on a page
* whose failure looks like missing map data rather than a JS error.
* - It has already regressed once by accident. The pins were added deliberately, then
* removed as collateral when the Mantine commit rewrote `overrides` for TypeScript 5.
* Nothing failed, so nothing objected; the map survived on lockfile inertia alone until
* someone re-resolved it.
*
* Verified rather than assumed, on this tree: raising @babel/core to 7.29.7 and reinstalling
* fails `npm run build` outright ("'opera_mobile' is not a valid target", from a
* helper-compilation-targets that floated away from the browserslist data it was pinned
* against). Restoring these pins builds, and /map renders from the built bundle.
*/
const APP = path.join(__dirname, '..');
const pkg = JSON.parse(fs.readFileSync(path.join(APP, 'package.json'), 'utf8'));
const lock = JSON.parse(fs.readFileSync(path.join(APP, 'package-lock.json'), 'utf8'));
/** The 16 packages that differ between the last-good tree and the one that broke the map. */
const PINNED = [
'@babel/code-frame', '@babel/compat-data', '@babel/core', '@babel/generator',
'@babel/helper-compilation-targets', '@babel/helper-globals', '@babel/helper-module-imports',
'@babel/helper-module-transforms', '@babel/helper-string-parser',
'@babel/helper-validator-identifier', '@babel/helper-validator-option', '@babel/helpers',
'@babel/parser', '@babel/template', '@babel/traverse', '@babel/types',
];
describe('@babel pins', () => {
it('names every package in the core pipeline', () => {
/*
* The whole pipeline, not just @babel/core. Pinning core alone was tried when this was
* first diagnosed and is not enough: the transform plugins resolve their own copies of
* traverse/generator/types, so the new AST pipeline comes back in underneath a pinned
* core.
*/
const overrides = pkg.overrides || {};
const missing = PINNED.filter(name => !overrides[name]);
expect(missing).toEqual([]);
});
it('pins them to an exact version, not a range', () => {
// `^7.20.12` is not a pin -- it resolves to 7.29 the moment anything re-resolves, which
// is exactly the event this guards against.
const loose = PINNED
.map(name => [name, (pkg.overrides || {})[name]])
.filter(([, spec]) => typeof spec !== 'string' || !/^\d+\.\d+\.\d+$/.test(spec));
expect(loose).toEqual([]);
});
it('resolves every installed copy to the pinned version', () => {
/*
* The manifest is the intent; the lockfile is what actually gets installed. npm nests
* a second copy of a dependency whenever versions conflict, and a nested copy that
* escaped the override would put the new pipeline back into the build while the
* top-level entry still read correctly.
*
* So this walks EVERY path in the lockfile, not just `node_modules/<name>`.
*/
const wanted = Object.fromEntries(PINNED.map(name => [name, pkg.overrides[name]]));
const mismatched = [];
let copies = 0;
for (const [location, info] of Object.entries(lock.packages || {})) {
for (const [name, version] of Object.entries(wanted)) {
if (location === `node_modules/${name}` || location.endsWith(`/node_modules/${name}`)) {
copies += 1;
if (info.version !== version) {
mismatched.push(`${location}: ${info.version} (pinned ${version})`);
}
}
}
}
// The premise: a lockfile these names never matched would report nothing mismatched.
expect(copies).toBeGreaterThan(100);
expect(mismatched).toEqual([]);
});
});