build(frontend): align @types/node 26 with the ES2022 lib declaration

Dependabot #2099 bumps @types/node 20.19.43 to 26.5.1 in frontend-modern, but the PR is blocked twice: Frontend tsc fails with TS2550 on .at() (26 removed the compatibility polyfill), and Canonical Governance blocks the package.json/lock change for a missing deployment-installability contract update and frontend dependency security proof.

Carry the bump with its required governance artifacts in one commit. tsconfig already declares the ES2022 lib (95db35b6c3), which the source's .at() usage needs; this adds the manifest and lock bump (matching tests/integration, which already pins @types/node ^26.5.1), a Completion Obligations entry, and a dependencySecurity proof that keeps the manifest range, the locked @types/node/undici-types versions and the declared lib in step. Verified offline: the guard passes and the proof test passes.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot] 2026-09-19 21:49:17 +01:00
parent 95db35b6c3
commit 698d08d2e9
4 changed files with 52 additions and 9 deletions

View file

@ -2366,6 +2366,17 @@ artifact-selection behaviour.
`scripts/trigger-release.sh` and `scripts/trigger-stable-patch.sh` must send
the exact remote candidate SHA they already verified; branch ancestry or a
later branch tip is not equivalent release admission.
18. Keep the frontend type-surface dependency and its compiler lib aligned.
`frontend-modern/package.json` and `frontend-modern/package-lock.json` must
pin a single `@types/node` line at or above the reviewed floor with its
`undici-types` companion resolved consistently, and
`frontend-modern/tsconfig.json` must declare the `ES2022` lib whenever the
source uses `Array.prototype.at()`/`String.prototype.at()`. `@types/node`
before 26 shipped an `.at()` compatibility polyfill that masked the missing
lib; a bump that removes it must keep
`frontend-modern/src/security/__tests__/dependencySecurity.test.ts` proving
the manifest range, the locked `@types/node`/`undici-types` versions and the
`ES2022` lib declaration stay in step.
## Current State

View file

@ -22,7 +22,7 @@
"@solidjs/testing-library": "^0.8.5",
"@tailwindcss/typography": "^0.5.19",
"@testing-library/jest-dom": "^6.5.0",
"@types/node": "^20.10.0",
"@types/node": "^26.5.1",
"@types/qrcode": "^1.5.6",
"@typescript-eslint/eslint-plugin": "^8.24.0",
"@typescript-eslint/parser": "^8.24.0",
@ -1969,13 +1969,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.43",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
"version": "26.5.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.1.tgz",
"integrity": "sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
"undici-types": "~8.9.0"
}
},
"node_modules/@types/qrcode": {
@ -6473,9 +6473,9 @@
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"version": "8.9.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
"integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
"dev": true,
"license": "MIT"
},

View file

@ -73,7 +73,7 @@
"@solidjs/testing-library": "^0.8.5",
"@tailwindcss/typography": "^0.5.19",
"@testing-library/jest-dom": "^6.5.0",
"@types/node": "^20.10.0",
"@types/node": "^26.5.1",
"@types/qrcode": "^1.5.6",
"@typescript-eslint/eslint-plugin": "^8.24.0",
"@typescript-eslint/parser": "^8.24.0",

View file

@ -20,6 +20,13 @@ const lock = JSON.parse(
readFileSync(new URL('../../../package-lock.json', import.meta.url), 'utf8'),
) as PackageLock;
const tsconfig = readFileSync(new URL('../../../tsconfig.json', import.meta.url), 'utf8');
const libMatch = /"lib"\s*:\s*\[([^\]]*)\]/.exec(tsconfig);
const declaredLib = (libMatch?.[1] ?? '')
.split(',')
.map((entry) => entry.trim().replace(/^"|"$/g, ''))
.filter(Boolean);
const parseVersion = (version: string): [number, number, number] => {
const [major = 0, minor = 0, patch = 0] = version
.split('-', 1)[0]
@ -139,4 +146,29 @@ describe('frontend dependency security floors', () => {
expect(atLeast(version, [4, 28, 7]), `browserslist ${version} is vulnerable`).toBe(true);
}
});
it('keeps @types/node and its undici-types companion aligned with the declared ES2022 lib', () => {
// @types/node 20 shipped an .at() compatibility polyfill that masked the
// missing ES2022 lib; 26 drops it, so tsconfig must declare ES2022 itself
// (Dependabot #2099). Keep the manifest range, the lock and the lib in step.
const range = manifest.devDependencies['@types/node'];
expect(/^\^26\.\d+\.\d+$/.test(range), `unexpected @types/node range ${range}`).toBe(true);
expect(atLeast(range.slice(1), [26, 5, 1])).toBe(true);
const nodeTypes = lockedVersions('@types/node');
expect(nodeTypes).toHaveLength(1);
expect(nodeTypes[0]).not.toContain('-');
expect(
atLeast(nodeTypes[0], [26, 5, 1]),
`@types/node ${nodeTypes[0]} is below the reviewed floor`,
).toBe(true);
const undici = lockedVersions('undici-types');
expect(undici).not.toHaveLength(0);
for (const version of undici) {
expect(
atLeast(version, [8, 9, 0]),
`undici-types ${version} must satisfy @types/node 26`,
).toBe(true);
}
expect(declaredLib).toContain('ES2022');
});
});