diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index c36bebc96..0066c6ccb 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -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 diff --git a/frontend-modern/package-lock.json b/frontend-modern/package-lock.json index 1cd175933..7bc5c96d0 100644 --- a/frontend-modern/package-lock.json +++ b/frontend-modern/package-lock.json @@ -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" }, diff --git a/frontend-modern/package.json b/frontend-modern/package.json index 6289dc6c4..b52ba77fd 100644 --- a/frontend-modern/package.json +++ b/frontend-modern/package.json @@ -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", diff --git a/frontend-modern/src/security/__tests__/dependencySecurity.test.ts b/frontend-modern/src/security/__tests__/dependencySecurity.test.ts index 774241c3b..aafd006d8 100644 --- a/frontend-modern/src/security/__tests__/dependencySecurity.test.ts +++ b/frontend-modern/src/security/__tests__/dependencySecurity.test.ts @@ -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'); + }); });