Files
wikideck/node_modules/update-browserslist-db/index.js
T
2026-10-19 12:16:49 -04:00

688 lines
21 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
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) {
let yarnMajorVersion = getYarnMajor()
if (yarnMajorVersion !== 1) {
throw new BrowserslistUpdateError(
'This project has a Yarn v1 (classic) lockfile, but the installed ' +
`Yarn is Berry v${yarnMajorVersion}. Install and activate Yarn classic ` +
'to update caniuse-lite.'
)
}
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) {
// ENOENT / status 127 are the POSIX "command not found" signals. cmd.exe
// instead reports `'<binary>' is not recognized as an internal or external
// command`. Anchor that phrase to the binary we actually launched so an
// unrelated "is not recognized" line in a real failure's output (e.g. a
// nested tool) is not mistaken for a missing package manager.
let binary = lock.mode === 'yarn' ? YARN_CMD : lock.mode
let missingOnWindows = new RegExp(
"'" + binary + "' is not recognized as an internal or external command"
)
if (
e.code === 'ENOENT' ||
e.status === 127 ||
missingOnWindows.test(commandErrorText(e))
) {
throw new BrowserslistUpdateError(
'Cannot find ' +
lock.mode +
' binary in PATH. Please install ' +
lock.mode +
' or run updates manually.'
)
}
throw e
}
}
function getYarnMajor() {
let version = execSync(YARN_CMD + ' --version').toString().trim()
return Number(version.split('.')[0])
}
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) {
// Split entries on blank lines using LF. A CRLF lockfile has `\r\n\r\n`
// between entries, which `\n{2,}` cannot match, so the whole file would
// collapse into one block and no version be detected. CRLF can occur on any
// OS (git autocrlf, editor settings), so normalize regardless of platform;
// updateLockfile restores the original EOL on the returned content.
let normalized = lock.content.replace(/\r\n/g, '\n')
let blocks = normalized.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 quotePnpmArg(arg) {
// cmd.exe expands %VAR% and !VAR! even inside double quotes. It also ignores
// the backslash in `\"`, so JSON values end up outside its quotes, where
// ^ & | < > ( ) run commands or redirect output. Refuse rather than run
// a mangled command.
if (/[%!^&|<>()]/.test(arg)) {
throw new BrowserslistUpdateError(
'Cannot safely pass any of %!^&|<>() to pnpm on Windows: ' + arg
)
}
if (!/[\s"]/.test(arg)) return arg
// Escape embedded quotes (doubling any backslash run that precedes them) and
// double a trailing backslash run so it cannot escape the closing quote.
return '"' + arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1') + '"'
}
function runPnpm(args, options = {}) {
// Node on Windows cannot launch pnpm's `.cmd` shim without a shell
if (process.platform === 'win32') {
return execFileSync('pnpm', args.map(quotePnpmArg), {
...options,
shell: true
})
}
return execFileSync('pnpm', args, options)
}
function getPnpmConfig(name) {
let output = runPnpm([
'config',
'get',
name,
'--json'
]).toString()
return output.trim() === 'undefined' ? undefined : JSON.parse(output)
}
function getPnpmPackageTimes(name) {
let times
try {
times = JSON.parse(
runPnpm(['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 packageFile = join(packageDir, 'package.json')
let originalPackage = existsSync(packageFile)
? readFileSync(packageFile)
: undefined
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 {
runPnpm(
[
'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'
)
runPnpm(['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'
)
runPnpm(['install', '--no-frozen-lockfile'], {
cwd: packageDir
})
} catch (error) /* c8 ignore start */ {
restoreFile(workspaceFile, originalWorkspace)
restoreFile(packageFile, originalPackage)
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 {
runPnpm(['up', '--depth=9999', '--no-save', ...packages], {
stdio: 'pipe'
})
} catch (error) /* c8 ignore start */ {
let text = commandErrorText(error)
// pnpm versions before 12.4.2 can fail `up --no-save` under strict
// minimumReleaseAge. The error differs by version: 10 uses
// NO_MATURE_MATCHING_VERSION, 11 uses MINIMUM_RELEASE_AGE_VIOLATION, and
// 12.0–12.4.1 uses STRICT_MIN_RELEASE_AGE_REQUIRES_SAVE. In each case,
// resolve and pin the newest mature versions ourselves. pnpm 12.4.2+ does
// this natively and succeeds before reaching this catch.
if (
text.includes('ERR_PNPM_STRICT_MIN_RELEASE_AGE_REQUIRES_SAVE') ||
text.includes('ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION') ||
text.includes('ERR_PNPM_NO_MATURE_MATCHING_VERSION')
) {
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')
}
}
}