File size: 5,183 Bytes
f76c374 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | /**
* Invariants tying ``allowScripts`` to the lockfile it gates.
*
* npm's ``allowScripts`` allowlist is keyed by exact ``name@version``, so an
* entry silently stops matching the moment that dependency is bumped. Nothing
* else in the build notices: npm downgrades the blocked script to a warning
* buried in install output, and the failure only surfaces much later as a
* missing native artifact.
*
* That has now bitten twice on Windows. ``get-windows`` was added to
* ``apps/desktop`` without an allow entry, so its node-pre-gyp install script
* never downloaded the win32 binding and ``hermes desktop`` died in
* ``stage-native-deps``. In the same window, a CVE sweep moved Electron to
* 40.10.6 and left the ``electron@40.10.2`` pin behind, blocking Electron's
* own postinstall on any clean install.
*
* Two contracts keep the allowlist honest:
*
* - Every versioned pin names a version the lockfile actually resolves, so a
* dependency bump that orphans its pin fails here instead of in a user's
* build.
* - Every package the lockfile marks as having an install script is covered
* by a decision — allowed at its exact version, or denied by name.
*
* A bare-name key (no ``@version``) is a deliberate standing decision that
* survives version bumps, which is how ``unicode-animations: false`` stays a
* permanent denial.
*/
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import { describe, test } from 'vitest'
const REPO_ROOT = path.resolve(__dirname, '..')
const MANIFESTS = [
{ name: 'root', dir: '.' },
{ name: 'website', dir: 'website' }
]
function manifestLabel(dir: string): string {
return path.join(dir === '.' ? '' : dir, 'package.json')
}
interface LockPackage {
name?: string
version?: string
hasInstallScript?: boolean
}
function readJson(filePath: string): Record<string, unknown> {
return JSON.parse(fs.readFileSync(filePath, 'utf-8'))
}
function packageNameFor(lockPath: string, entry: LockPackage): string {
return entry.name ?? lockPath.split('node_modules/').pop() ?? lockPath
}
/** Every version of every package the lockfile installs, keyed by name. */
function installedVersions(lock: Record<string, unknown>): Map<string, Set<string>> {
const versions = new Map<string, Set<string>>()
for (const [lockPath, entry] of Object.entries(
(lock.packages ?? {}) as Record<string, LockPackage>
)) {
if (!lockPath || !entry.version) {
continue
}
const name = packageNameFor(lockPath, entry)
const seen = versions.get(name) ?? new Set<string>()
seen.add(entry.version)
versions.set(name, seen)
}
return versions
}
function splitPin(key: string): { name: string; version: string } | null {
// Scoped packages carry a leading @, so match the LAST @ as the separator.
const match = key.match(/^(.+)@([^@]+)$/)
return match ? { name: match[1], version: match[2] } : null
}
describe.each(MANIFESTS)('$name allowScripts', ({ dir }) => {
const manifestPath = path.join(REPO_ROOT, dir, 'package.json')
const lockPath = path.join(REPO_ROOT, dir, 'package-lock.json')
const label = manifestLabel(dir)
test('every versioned pin matches a version in the lockfile', () => {
if (!fs.existsSync(lockPath)) {
return
}
const allow = (readJson(manifestPath).allowScripts ?? {}) as Record<string, boolean>
const versions = installedVersions(readJson(lockPath))
const stale: string[] = []
for (const key of Object.keys(allow)) {
const pin = splitPin(key)
if (!pin) {
continue
}
const installed = versions.get(pin.name)
if (!installed?.has(pin.version)) {
stale.push(` "${key}" — lockfile resolves ${pin.name} to ${installed ? [...installed].join(', ') : '<nothing>'}`)
}
}
assert.deepEqual(
stale,
[],
`Stale allowScripts entries in ${label}:\n${stale.join('\n')}\n` +
"npm matches these by exact version, so each package's install script is " +
'silently blocked. Update the pin to the installed version, or drop the ' +
'entry if the dependency is gone.'
)
})
test('every package with an install script has an allowScripts decision', () => {
if (!fs.existsSync(lockPath)) {
return
}
const allow = (readJson(manifestPath).allowScripts ?? {}) as Record<string, boolean>
const lock = readJson(lockPath)
const uncovered: string[] = []
for (const [entryPath, entry] of Object.entries(
(lock.packages ?? {}) as Record<string, LockPackage>
)) {
if (!entryPath || !entry.hasInstallScript) {
continue
}
const name = packageNameFor(entryPath, entry)
if (!(`${name}@${entry.version}` in allow) && !(name in allow)) {
uncovered.push(` ${name}@${entry.version}`)
}
}
assert.deepEqual(
uncovered,
[],
`Packages with install scripts and no allowScripts decision in ${label}:\n` +
`${uncovered.join('\n')}\n` +
'npm blocks these. Add "<name>@<version>": true to allow, or "<name>": false ' +
'to deny permanently.'
)
})
})
|