Files
client/node_modules/update-browserslist-db/index.js
T
2026-10-07 18:56:27 +02:00

615 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
let { execFileSync, execSync } = require('child_process')
let escalade = require('escalade/sync')
let { existsSync, readFileSync, unlinkSync, writeFileSync } = require('fs')
let { dirname, join } = require('path')
let pico = require('picocolors')
let { detectEOL, detectIndent } = require('./utils')
function BrowserslistUpdateError(message) {
this.name = 'BrowserslistUpdateError'
this.message = message
this.browserslist = true
if (Error.captureStackTrace) {
Error.captureStackTrace(this, BrowserslistUpdateError)
}
}
BrowserslistUpdateError.prototype = Error.prototype
// Check if HADOOP_HOME is set to determine if this is running in a Hadoop environment
const YARN_CMD = process.env.HADOOP_HOME ? 'yarnpkg' : 'yarn'
/* c8 ignore next 3 */
function defaultPrint(str) {
process.stdout.write(str)
}
function detectLockfile() {
let packageDir = escalade('.', (dir, names) => {
return names.indexOf('package.json') !== -1 ? dir : ''
})
if (!packageDir) {
throw new BrowserslistUpdateError(
'Cannot find package.json. ' +
'Is this the right directory to run `npx update-browserslist-db` in?'
)
}
let lockfileNpm = join(packageDir, 'package-lock.json')
let lockfileShrinkwrap = join(packageDir, 'npm-shrinkwrap.json')
let lockfileYarn = join(packageDir, 'yarn.lock')
let lockfilePnpm = join(packageDir, 'pnpm-lock.yaml')
let lockfileBun = join(packageDir, 'bun.lock')
let lockfileBunBinary = join(packageDir, 'bun.lockb')
let lockfileDeno = join(packageDir, 'deno.lock')
if (existsSync(lockfilePnpm)) {
return { file: lockfilePnpm, mode: 'pnpm' }
} else if (existsSync(lockfileBun) || existsSync(lockfileBunBinary)) {
return { file: lockfileBun, mode: 'bun' }
} else if (existsSync(lockfileNpm)) {
return { file: lockfileNpm, mode: 'npm' }
} else if (existsSync(lockfileYarn)) {
let lock = { file: lockfileYarn, mode: 'yarn' }
lock.content = readFileSync(lock.file).toString()
lock.version = /# yarn lockfile v1/.test(lock.content) ? 1 : 2
return lock
} else if (existsSync(lockfileShrinkwrap)) {
return { file: lockfileShrinkwrap, mode: 'npm' }
} else if (existsSync(lockfileDeno)) {
return { file: lockfileDeno, mode: 'deno' }
}
throw new BrowserslistUpdateError(
'No lockfile found. Run "npm install", "yarn install", "pnpm install", "bun install" or "deno install"'
)
}
function getLatestInfo(lock) {
try {
if (lock.mode === 'yarn') {
if (lock.version === 1) {
return JSON.parse(
execSync(YARN_CMD + ' info caniuse-lite --json').toString()
).data
} else {
return JSON.parse(
execSync(YARN_CMD + ' npm info caniuse-lite --json').toString()
)
}
}
if (lock.mode === 'pnpm') {
return JSON.parse(execSync('pnpm info caniuse-lite --json').toString())
}
if (lock.mode === 'bun') {
return JSON.parse(execSync('bun info caniuse-lite --json').toString())
}
if (lock.mode === 'deno') {
let result = JSON.parse(
execSync(
'deno run -A npm:npm show caniuse-lite version --json'
).toString()
)
return { version: Array.isArray(result) ? result[0] : result }
}
return JSON.parse(execSync('npm show caniuse-lite --json').toString())
} catch (e) {
if (e.code === 'ENOENT' || e.status === 127) {
throw new BrowserslistUpdateError(
'Cannot find ' +
lock.mode +
' binary in PATH. Please install ' +
lock.mode +
' or run updates manually.'
)
}
throw e
}
}
function getBrowsers() {
let browserslist = require('browserslist')
return browserslist().reduce((result, entry) => {
if (!result[entry[0]]) {
result[entry[0]] = []
}
result[entry[0]].push(entry[1])
return result
}, {})
}
function diffBrowsers(old, current) {
let browsers = Object.keys(old).concat(
Object.keys(current).filter(browser => old[browser] === undefined)
)
return browsers
.map(browser => {
let oldVersions = old[browser] || []
let currentVersions = current[browser] || []
let common = oldVersions.filter(v => currentVersions.includes(v))
let added = currentVersions.filter(v => !common.includes(v))
let removed = oldVersions.filter(v => !common.includes(v))
return removed
.map(v => pico.red('- ' + browser + ' ' + v))
.concat(added.map(v => pico.green('+ ' + browser + ' ' + v)))
})
.reduce((result, array) => result.concat(array), [])
.join('\n')
}
function updateNpmLockfile(lock, latest) {
let metadata = { latest, versions: [] }
let content = deletePackage(JSON.parse(lock.content), metadata)
metadata.content = JSON.stringify(content, null, detectIndent(lock.content))
return metadata
}
function deletePackage(node, metadata) {
if (node.dependencies) {
if (node.dependencies['caniuse-lite']) {
let version = node.dependencies['caniuse-lite'].version
metadata.versions[version] = true
delete node.dependencies['caniuse-lite']
}
for (let i in node.dependencies) {
node.dependencies[i] = deletePackage(node.dependencies[i], metadata)
}
}
if (node.packages) {
for (let path in node.packages) {
if (path.endsWith('/caniuse-lite')) {
metadata.versions[node.packages[path].version] = true
delete node.packages[path]
}
}
}
return node
}
let yarnVersionRe = /version "(.*?)"/
function updateYarnLockfile(lock, latest) {
let blocks = lock.content.split(/(\n{2,})/).map(block => {
return block.split('\n')
})
let versions = {}
blocks.forEach(lines => {
if (lines[0].indexOf('caniuse-lite@') !== -1) {
let match = yarnVersionRe.exec(lines[1])
versions[match[1]] = true
if (match[1] !== latest.version) {
lines[1] = lines[1].replace(
/version "[^"]+"/,
'version "' + latest.version + '"'
)
lines[2] = lines[2].replace(
/resolved "[^"]+"/,
'resolved "' + latest.dist.tarball + '"'
)
if (lines.length === 4) {
lines[3] = latest.dist.integrity
? lines[3].replace(
/integrity .+/,
'integrity ' + latest.dist.integrity
)
: ''
}
}
}
})
let content = blocks.map(lines => lines.join('\n')).join('')
return { content, versions }
}
function updateLockfile(lock, latest) {
if (!lock.content) lock.content = readFileSync(lock.file).toString()
let updatedLockFile
if (lock.mode === 'yarn') {
updatedLockFile = updateYarnLockfile(lock, latest)
} else {
updatedLockFile = updateNpmLockfile(lock, latest)
}
updatedLockFile.content = updatedLockFile.content.replace(
/\n/g,
detectEOL(lock.content)
)
return updatedLockFile
}
function updatePackageManually(print, lock, latest) {
let lockfileData = updateLockfile(lock, latest)
let caniuseVersions = Object.keys(lockfileData.versions).sort()
if (caniuseVersions.length === 1 && caniuseVersions[0] === latest.version) {
print(
'Installed version: ' +
pico.bold(pico.green(caniuseVersions[0])) +
'\n' +
pico.bold(pico.green('caniuse-lite is up to date')) +
'\n'
)
return
}
if (caniuseVersions.length === 0) {
caniuseVersions[0] = 'none'
}
print(
'Installed version' +
(caniuseVersions.length === 1 ? ': ' : 's: ') +
pico.bold(pico.red(caniuseVersions.join(', '))) +
'\n' +
'Removing old caniuse-lite from lock file\n'
)
writeFileSync(lock.file, lockfileData.content)
let install =
lock.mode === 'yarn' ? YARN_CMD + ' add -W' : lock.mode + ' install'
print(
'Installing new caniuse-lite version\n' +
pico.yellow('$ ' + install + ' caniuse-lite baseline-browser-mapping') +
'\n'
)
try {
execSync(install + ' caniuse-lite baseline-browser-mapping')
} catch (e) /* c8 ignore start */ {
print(
pico.red(
'\n' +
e.stack +
'\n\n' +
'Problem with `' +
install +
' caniuse-lite` call. ' +
'Run it manually.\n'
)
)
process.exit(1)
} /* c8 ignore end */
let del =
lock.mode === 'yarn' ? YARN_CMD + ' remove -W' : lock.mode + ' uninstall'
print(
'Cleaning package.json dependencies from caniuse-lite\n' +
pico.yellow('$ ' + del + ' caniuse-lite baseline-browser-mapping') +
'\n'
)
execSync(del + ' caniuse-lite baseline-browser-mapping')
}
/**
* Bun cannot update a transitive dependency by name: `bun update caniuse-lite`
* adds caniuse-lite to package.json as a new direct dependency at the latest
* version and leaves every nested copy - the ones that actually get resolved at
* runtime - on the old version. A temporary `overrides` entry reaches those,
* and the resolutions survive once it is taken back out.
*/
function updateBun(print, lock, latest) {
let pkgFile = join(dirname(lock.file), 'package.json')
let original = readFileSync(pkgFile)
let pkg = JSON.parse(original.toString())
let withOverride = {
...pkg,
overrides: {
...pkg.overrides,
'baseline-browser-mapping': 'latest',
'caniuse-lite': latest.version
}
}
// The file is restored byte for byte below, so its formatting does not matter.
writeFileSync(pkgFile, JSON.stringify(withOverride, null, 2) + '\n')
print(
'Updating caniuse-lite version\n' +
pico.yellow('$ bun install') +
' (with a temporary caniuse-lite override)\n'
)
try {
execSync('bun install')
} catch (e) /* c8 ignore start */ {
writeFileSync(pkgFile, original)
print(pico.red(e.stdout.toString()))
print(
pico.red(
'\n' +
e.stack +
'\n\n' +
'Problem with `bun install` call. ' +
'Run it manually.\n'
)
)
process.exit(1)
} /* c8 ignore end */
writeFileSync(pkgFile, original)
updateWith(print, 'bun install', 'Removing the temporary override')
}
function commandErrorText(error) {
return [error.message, error.stdout, error.stderr]
.filter(Boolean)
.map(i => i.toString())
.join('\n')
}
function getPnpmConfig(name) {
let output = execFileSync('pnpm', [
'config',
'get',
name,
'--json'
]).toString()
return output.trim() === 'undefined' ? undefined : JSON.parse(output)
}
function getPnpmPackageTimes(name) {
let times
try {
times = JSON.parse(
execFileSync('pnpm', ['info', name, 'time', '--json']).toString()
)
} catch (error) /* c8 ignore start */ {
throw new BrowserslistUpdateError(
'Cannot read publication dates of ' +
name +
' from pnpm. Refusing to bypass pnpm minimumReleaseAge.\n' +
commandErrorText(error)
)
} /* c8 ignore end */
if (!times || typeof times !== 'object' || Array.isArray(times)) {
/* c8 ignore next 6 */
throw new BrowserslistUpdateError(
'pnpm returned no publication dates for ' +
name +
'. Refusing to bypass pnpm minimumReleaseAge.'
)
}
return times
}
function getMatureVersion(name, minimumReleaseAge) {
let cutoff = Date.now() - minimumReleaseAge * 60 * 1000
let times = getPnpmPackageTimes(name)
let mature, matureTime
for (let version of Object.keys(times)) {
let time = Date.parse(times[version])
if (!/^\d+\.\d+\.\d+$/.test(version) || !(time <= cutoff)) continue
if (matureTime === undefined || time > matureTime) {
mature = version
matureTime = time
}
}
if (!mature) {
throw new BrowserslistUpdateError(
'Cannot find a ' +
name +
' version old enough for pnpm minimumReleaseAge (' +
minimumReleaseAge +
' minutes). Wait for a release to pass the age gate and try again.'
)
}
return mature
}
function restoreFile(file, original) {
if (original === undefined) {
if (existsSync(file)) unlinkSync(file)
} else {
writeFileSync(file, original)
}
}
function updatePnpmStrict(print, lock, packages) {
let minimumReleaseAge = getPnpmConfig('minimumReleaseAge')
if (
typeof minimumReleaseAge !== 'number' ||
!Number.isFinite(minimumReleaseAge) ||
minimumReleaseAge <= 0
) {
throw new BrowserslistUpdateError(
'Cannot read a valid pnpm minimumReleaseAge. Refusing to disable pnpm security checks.'
)
}
let versions = {}
for (let name of packages) {
versions[name] = getMatureVersion(name, minimumReleaseAge)
}
let originalOverrides = getPnpmConfig('overrides')
if (
originalOverrides === undefined ||
originalOverrides === null ||
Array.isArray(originalOverrides) ||
typeof originalOverrides !== 'object'
) {
originalOverrides = {}
}
let temporaryOverrides = { ...originalOverrides, ...versions }
let packageDir = dirname(lock.file)
let workspaceFile = join(packageDir, 'pnpm-workspace.yaml')
let originalWorkspace = existsSync(workspaceFile)
? readFileSync(workspaceFile)
: undefined
let originalLockfile = readFileSync(lock.file)
let command = 'pnpm config set overrides'
print(
'Strict pnpm minimumReleaseAge detected\n' +
'Latest policy-compliant: ' +
pico.bold(pico.green(versions['caniuse-lite'])) +
'\n' +
Object.keys(versions)
.filter(name => name !== 'caniuse-lite')
.map(name => ' ' + name + ': ' + versions[name] + '\n')
.join('')
)
try {
execFileSync(
'pnpm',
[
'config',
'set',
'overrides',
JSON.stringify(temporaryOverrides),
'--json',
'--location',
'project'
],
{ cwd: packageDir }
)
command = 'pnpm install --lockfile-only'
print(
'Updating caniuse-lite version\n' +
pico.yellow('$ ' + command) +
' (with temporary policy-compliant overrides)\n'
)
execFileSync('pnpm', ['install', '--lockfile-only'], { cwd: packageDir })
restoreFile(workspaceFile, originalWorkspace)
// The lockfile no longer matches the restored overrides, and pnpm
// freezes the lockfile by default in CI
command = 'pnpm install --no-frozen-lockfile'
print(
'Removing temporary pnpm overrides\n' + pico.yellow('$ ' + command) + '\n'
)
execFileSync('pnpm', ['install', '--no-frozen-lockfile'], {
cwd: packageDir
})
} catch (error) /* c8 ignore start */ {
restoreFile(workspaceFile, originalWorkspace)
writeFileSync(lock.file, originalLockfile)
print(pico.red(commandErrorText(error)))
print(
pico.red('\n\nProblem with `' + command + '` call. Run it manually.\n')
)
process.exit(1)
} /* c8 ignore end */
}
function updatePnpm(print, lock) {
let lockContent = readFileSync(lock.file).toString()
let packages = lockContent.includes('baseline-browser-mapping')
? ['caniuse-lite', 'baseline-browser-mapping']
: ['caniuse-lite']
let command = 'pnpm up --depth=9999 --no-save ' + packages.join(' ')
print('Updating caniuse-lite version\n' + pico.yellow('$ ' + command) + '\n')
try {
execFileSync('pnpm', ['up', '--depth=9999', '--no-save', ...packages], {
stdio: 'pipe'
})
} catch (error) /* c8 ignore start */ {
let text = commandErrorText(error)
if (text.includes('ERR_PNPM_STRICT_MIN_RELEASE_AGE_REQUIRES_SAVE')) {
updatePnpmStrict(print, lock, packages)
} else {
print(pico.red(text))
print(
pico.red('\n\nProblem with `' + command + '` call. Run it manually.\n')
)
process.exit(1)
}
} /* c8 ignore end */
}
function updateWith(print, cmd, message = 'Updating caniuse-lite version') {
print(message + '\n' + pico.yellow('$ ' + cmd) + '\n')
try {
execSync(cmd)
} catch (e) /* c8 ignore start */ {
print(pico.red(e.stdout.toString()))
print(
pico.red(
'\n' +
e.stack +
'\n\n' +
'Problem with `' +
cmd +
'` call. ' +
'Run it manually.\n'
)
)
process.exit(1)
} /* c8 ignore end */
}
module.exports = function updateDB(print = defaultPrint) {
let lock = detectLockfile()
let latest = getLatestInfo(lock)
let listError
let oldList
try {
oldList = getBrowsers()
} catch (e) {
listError = e
}
print(
'Registry latest: ' + pico.bold(pico.green(latest.version)) + '\n'
)
if (lock.mode === 'yarn' && lock.version !== 1) {
updateWith(print, YARN_CMD + ' up -R caniuse-lite baseline-browser-mapping')
} else if (lock.mode === 'pnpm') {
updatePnpm(print, lock)
} else if (lock.mode === 'bun') {
updateBun(print, lock, latest)
} else if (lock.mode === 'deno') {
updateWith(print, 'deno add npm:caniuse-lite npm:baseline-browser-mapping')
updateWith(
print,
'deno remove caniuse-lite baseline-browser-mapping',
'Cleaning package.json dependencies from caniuse-lite'
)
} else {
updatePackageManually(print, lock, latest)
}
print('caniuse-lite has been successfully updated\n')
let newList
if (!listError) {
try {
newList = getBrowsers()
} catch (e) /* c8 ignore start */ {
listError = e
} /* c8 ignore end */
}
if (listError) {
if (listError.message.includes("Cannot find module 'browserslist'")) {
print(
pico.gray(
'Install `browserslist` to your direct dependencies ' +
'to see target browser changes\n'
)
)
} else {
print(
pico.gray(
'Problem with browser list retrieval.\n' +
'Target browser changes won’t be shown.\n'
)
)
}
} else {
let changes = diffBrowsers(oldList, newList)
if (changes) {
print('\nTarget browser changes:\n')
print(changes + '\n')
} else {
print('\n' + pico.green('No target browser changes') + '\n')
}
}
}