update-browserslist-db 1.3.2 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +277 -13
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
- let { execSync } = require('child_process')
1
+ let { execFileSync, execSync } = require('child_process')
2
2
  let escalade = require('escalade/sync')
3
- let { existsSync, readFileSync, writeFileSync } = require('fs')
3
+ let { existsSync, readFileSync, unlinkSync, writeFileSync } = require('fs')
4
4
  let { dirname, join } = require('path')
5
5
  let pico = require('picocolors')
6
6
 
@@ -17,7 +17,6 @@ function BrowserslistUpdateError(message) {
17
17
 
18
18
  BrowserslistUpdateError.prototype = Error.prototype
19
19
 
20
- // Check if HADOOP_HOME is set to determine if this is running in a Hadoop environment
21
20
  const YARN_CMD = process.env.HADOOP_HOME ? 'yarnpkg' : 'yarn'
22
21
 
23
22
  /* c8 ignore next 3 */
@@ -70,6 +69,14 @@ function getLatestInfo(lock) {
70
69
  try {
71
70
  if (lock.mode === 'yarn') {
72
71
  if (lock.version === 1) {
72
+ let yarnMajorVersion = getYarnMajor()
73
+ if (yarnMajorVersion !== 1) {
74
+ throw new BrowserslistUpdateError(
75
+ 'This project has a Yarn v1 (classic) lockfile, but the installed ' +
76
+ `Yarn is Berry v${yarnMajorVersion}. Install and activate Yarn classic ` +
77
+ 'to update caniuse-lite.'
78
+ )
79
+ }
73
80
  return JSON.parse(
74
81
  execSync(YARN_CMD + ' info caniuse-lite --json').toString()
75
82
  ).data
@@ -96,7 +103,20 @@ function getLatestInfo(lock) {
96
103
 
97
104
  return JSON.parse(execSync('npm show caniuse-lite --json').toString())
98
105
  } catch (e) {
99
- if (e.code === 'ENOENT' || e.status === 127) {
106
+ // ENOENT / status 127 are the POSIX "command not found" signals. cmd.exe
107
+ // instead reports `'<binary>' is not recognized as an internal or external
108
+ // command`. Anchor that phrase to the binary we actually launched so an
109
+ // unrelated "is not recognized" line in a real failure's output (e.g. a
110
+ // nested tool) is not mistaken for a missing package manager.
111
+ let binary = lock.mode === 'yarn' ? YARN_CMD : lock.mode
112
+ let missingOnWindows = new RegExp(
113
+ "'" + binary + "' is not recognized as an internal or external command"
114
+ )
115
+ if (
116
+ e.code === 'ENOENT' ||
117
+ e.status === 127 ||
118
+ missingOnWindows.test(commandErrorText(e))
119
+ ) {
100
120
  throw new BrowserslistUpdateError(
101
121
  'Cannot find ' +
102
122
  lock.mode +
@@ -109,6 +129,11 @@ function getLatestInfo(lock) {
109
129
  }
110
130
  }
111
131
 
132
+ function getYarnMajor() {
133
+ let version = execSync(YARN_CMD + ' --version').toString().trim()
134
+ return Number(version.split('.')[0])
135
+ }
136
+
112
137
  function getBrowsers() {
113
138
  let browserslist = require('browserslist')
114
139
  return browserslist().reduce((result, entry) => {
@@ -171,7 +196,13 @@ function deletePackage(node, metadata) {
171
196
  let yarnVersionRe = /version "(.*?)"/
172
197
 
173
198
  function updateYarnLockfile(lock, latest) {
174
- let blocks = lock.content.split(/(\n{2,})/).map(block => {
199
+ // Split entries on blank lines using LF. A CRLF lockfile has `\r\n\r\n`
200
+ // between entries, which `\n{2,}` cannot match, so the whole file would
201
+ // collapse into one block and no version be detected. CRLF can occur on any
202
+ // OS (git autocrlf, editor settings), so normalize regardless of platform;
203
+ // updateLockfile restores the original EOL on the returned content.
204
+ let normalized = lock.content.replace(/\r\n/g, '\n')
205
+ let blocks = normalized.split(/(\n{2,})/).map(block => {
175
206
  return block.split('\n')
176
207
  })
177
208
  let versions = {}
@@ -224,7 +255,7 @@ function updatePackageManually(print, lock, latest) {
224
255
  let caniuseVersions = Object.keys(lockfileData.versions).sort()
225
256
  if (caniuseVersions.length === 1 && caniuseVersions[0] === latest.version) {
226
257
  print(
227
- 'Installed version: ' +
258
+ 'Installed version: ' +
228
259
  pico.bold(pico.green(caniuseVersions[0])) +
229
260
  '\n' +
230
261
  pico.bold(pico.green('caniuse-lite is up to date')) +
@@ -238,7 +269,7 @@ function updatePackageManually(print, lock, latest) {
238
269
  }
239
270
  print(
240
271
  'Installed version' +
241
- (caniuseVersions.length === 1 ? ': ' : 's: ') +
272
+ (caniuseVersions.length === 1 ? ': ' : 's: ') +
242
273
  pico.bold(pico.red(caniuseVersions.join(', '))) +
243
274
  '\n' +
244
275
  'Removing old caniuse-lite from lock file\n'
@@ -328,6 +359,241 @@ function updateBun(print, lock, latest) {
328
359
  updateWith(print, 'bun install', 'Removing the temporary override')
329
360
  }
330
361
 
362
+ function commandErrorText(error) {
363
+ return [error.message, error.stdout, error.stderr]
364
+ .filter(Boolean)
365
+ .map(i => i.toString())
366
+ .join('\n')
367
+ }
368
+
369
+ function quotePnpmArg(arg) {
370
+ // cmd.exe expands %VAR% and !VAR! even inside double quotes. It also ignores
371
+ // the backslash in `\"`, so JSON values end up outside its quotes, where
372
+ // ^ & | < > ( ) run commands or redirect output. Refuse rather than run
373
+ // a mangled command.
374
+ if (/[%!^&|<>()]/.test(arg)) {
375
+ throw new BrowserslistUpdateError(
376
+ 'Cannot safely pass any of %!^&|<>() to pnpm on Windows: ' + arg
377
+ )
378
+ }
379
+ if (!/[\s"]/.test(arg)) return arg
380
+ // Escape embedded quotes (doubling any backslash run that precedes them) and
381
+ // double a trailing backslash run so it cannot escape the closing quote.
382
+ return '"' + arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1') + '"'
383
+ }
384
+
385
+ function runPnpm(args, options = {}) {
386
+ // Node on Windows cannot launch pnpm's `.cmd` shim without a shell
387
+ if (process.platform === 'win32') {
388
+ return execFileSync('pnpm', args.map(quotePnpmArg), {
389
+ ...options,
390
+ shell: true
391
+ })
392
+ }
393
+
394
+ return execFileSync('pnpm', args, options)
395
+ }
396
+
397
+ function getPnpmConfig(name) {
398
+ let output = runPnpm([
399
+ 'config',
400
+ 'get',
401
+ name,
402
+ '--json'
403
+ ]).toString()
404
+ return output.trim() === 'undefined' ? undefined : JSON.parse(output)
405
+ }
406
+
407
+ function getPnpmPackageTimes(name) {
408
+ let times
409
+ try {
410
+ times = JSON.parse(
411
+ runPnpm(['info', name, 'time', '--json']).toString()
412
+ )
413
+ } catch (error) /* c8 ignore start */ {
414
+ throw new BrowserslistUpdateError(
415
+ 'Cannot read publication dates of ' +
416
+ name +
417
+ ' from pnpm. Refusing to bypass pnpm minimumReleaseAge.\n' +
418
+ commandErrorText(error)
419
+ )
420
+ } /* c8 ignore end */
421
+ if (!times || typeof times !== 'object' || Array.isArray(times)) {
422
+ /* c8 ignore next 6 */
423
+ throw new BrowserslistUpdateError(
424
+ 'pnpm returned no publication dates for ' +
425
+ name +
426
+ '. Refusing to bypass pnpm minimumReleaseAge.'
427
+ )
428
+ }
429
+ return times
430
+ }
431
+
432
+ function getMatureVersion(name, minimumReleaseAge) {
433
+ let cutoff = Date.now() - minimumReleaseAge * 60 * 1000
434
+ let times = getPnpmPackageTimes(name)
435
+
436
+ let mature, matureTime
437
+ for (let version of Object.keys(times)) {
438
+ let time = Date.parse(times[version])
439
+ if (!/^\d+\.\d+\.\d+$/.test(version) || !(time <= cutoff)) continue
440
+ if (matureTime === undefined || time > matureTime) {
441
+ mature = version
442
+ matureTime = time
443
+ }
444
+ }
445
+
446
+ if (!mature) {
447
+ throw new BrowserslistUpdateError(
448
+ 'Cannot find a ' +
449
+ name +
450
+ ' version old enough for pnpm minimumReleaseAge (' +
451
+ minimumReleaseAge +
452
+ ' minutes). Wait for a release to pass the age gate and try again.'
453
+ )
454
+ }
455
+
456
+ return mature
457
+ }
458
+
459
+ function restoreFile(file, original) {
460
+ if (original === undefined) {
461
+ if (existsSync(file)) unlinkSync(file)
462
+ } else {
463
+ writeFileSync(file, original)
464
+ }
465
+ }
466
+
467
+ function updatePnpmStrict(print, lock, packages) {
468
+ let minimumReleaseAge = getPnpmConfig('minimumReleaseAge')
469
+ if (
470
+ typeof minimumReleaseAge !== 'number' ||
471
+ !Number.isFinite(minimumReleaseAge) ||
472
+ minimumReleaseAge <= 0
473
+ ) {
474
+ throw new BrowserslistUpdateError(
475
+ 'Cannot read a valid pnpm minimumReleaseAge. Refusing to disable pnpm security checks.'
476
+ )
477
+ }
478
+
479
+ let versions = {}
480
+ for (let name of packages) {
481
+ versions[name] = getMatureVersion(name, minimumReleaseAge)
482
+ }
483
+
484
+ let originalOverrides = getPnpmConfig('overrides')
485
+ if (
486
+ originalOverrides === undefined ||
487
+ originalOverrides === null ||
488
+ Array.isArray(originalOverrides) ||
489
+ typeof originalOverrides !== 'object'
490
+ ) {
491
+ originalOverrides = {}
492
+ }
493
+ let temporaryOverrides = { ...originalOverrides, ...versions }
494
+ let packageDir = dirname(lock.file)
495
+ let workspaceFile = join(packageDir, 'pnpm-workspace.yaml')
496
+ let originalWorkspace = existsSync(workspaceFile)
497
+ ? readFileSync(workspaceFile)
498
+ : undefined
499
+ let originalLockfile = readFileSync(lock.file)
500
+ let packageFile = join(packageDir, 'package.json')
501
+ let originalPackage = existsSync(packageFile)
502
+ ? readFileSync(packageFile)
503
+ : undefined
504
+ let command = 'pnpm config set overrides'
505
+
506
+ print(
507
+ 'Strict pnpm minimumReleaseAge detected\n' +
508
+ 'Latest policy-compliant: ' +
509
+ pico.bold(pico.green(versions['caniuse-lite'])) +
510
+ '\n' +
511
+ Object.keys(versions)
512
+ .filter(name => name !== 'caniuse-lite')
513
+ .map(name => ' ' + name + ': ' + versions[name] + '\n')
514
+ .join('')
515
+ )
516
+
517
+ try {
518
+ runPnpm(
519
+ [
520
+ 'config',
521
+ 'set',
522
+ 'overrides',
523
+ JSON.stringify(temporaryOverrides),
524
+ '--json',
525
+ '--location',
526
+ 'project'
527
+ ],
528
+ { cwd: packageDir }
529
+ )
530
+
531
+ command = 'pnpm install --lockfile-only'
532
+ print(
533
+ 'Updating caniuse-lite version\n' +
534
+ pico.yellow('$ ' + command) +
535
+ ' (with temporary policy-compliant overrides)\n'
536
+ )
537
+ runPnpm(['install', '--lockfile-only'], { cwd: packageDir })
538
+
539
+ restoreFile(workspaceFile, originalWorkspace)
540
+ // The lockfile no longer matches the restored overrides, and pnpm
541
+ // freezes the lockfile by default in CI
542
+ command = 'pnpm install --no-frozen-lockfile'
543
+ print(
544
+ 'Removing temporary pnpm overrides\n' + pico.yellow('$ ' + command) + '\n'
545
+ )
546
+ runPnpm(['install', '--no-frozen-lockfile'], {
547
+ cwd: packageDir
548
+ })
549
+ } catch (error) /* c8 ignore start */ {
550
+ restoreFile(workspaceFile, originalWorkspace)
551
+ restoreFile(packageFile, originalPackage)
552
+ writeFileSync(lock.file, originalLockfile)
553
+ print(pico.red(commandErrorText(error)))
554
+ print(
555
+ pico.red('\n\nProblem with `' + command + '` call. Run it manually.\n')
556
+ )
557
+ process.exit(1)
558
+ } /* c8 ignore end */
559
+ }
560
+
561
+ function updatePnpm(print, lock) {
562
+ let lockContent = readFileSync(lock.file).toString()
563
+ let packages = lockContent.includes('baseline-browser-mapping')
564
+ ? ['caniuse-lite', 'baseline-browser-mapping']
565
+ : ['caniuse-lite']
566
+ let command = 'pnpm up --depth=9999 --no-save ' + packages.join(' ')
567
+
568
+ print('Updating caniuse-lite version\n' + pico.yellow('$ ' + command) + '\n')
569
+ try {
570
+ runPnpm(['up', '--depth=9999', '--no-save', ...packages], {
571
+ stdio: 'pipe'
572
+ })
573
+ } catch (error) /* c8 ignore start */ {
574
+ let text = commandErrorText(error)
575
+ // pnpm versions before 12.4.2 can fail `up --no-save` under strict
576
+ // minimumReleaseAge. The error differs by version: 10 uses
577
+ // NO_MATURE_MATCHING_VERSION, 11 uses MINIMUM_RELEASE_AGE_VIOLATION, and
578
+ // 12.0–12.4.1 uses STRICT_MIN_RELEASE_AGE_REQUIRES_SAVE. In each case,
579
+ // resolve and pin the newest mature versions ourselves. pnpm 12.4.2+ does
580
+ // this natively and succeeds before reaching this catch.
581
+ if (
582
+ text.includes('ERR_PNPM_STRICT_MIN_RELEASE_AGE_REQUIRES_SAVE') ||
583
+ text.includes('ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION') ||
584
+ text.includes('ERR_PNPM_NO_MATURE_MATCHING_VERSION')
585
+ ) {
586
+ updatePnpmStrict(print, lock, packages)
587
+ } else {
588
+ print(pico.red(text))
589
+ print(
590
+ pico.red('\n\nProblem with `' + command + '` call. Run it manually.\n')
591
+ )
592
+ process.exit(1)
593
+ }
594
+ } /* c8 ignore end */
595
+ }
596
+
331
597
  function updateWith(print, cmd, message = 'Updating caniuse-lite version') {
332
598
  print(message + '\n' + pico.yellow('$ ' + cmd) + '\n')
333
599
  try {
@@ -361,16 +627,14 @@ module.exports = function updateDB(print = defaultPrint) {
361
627
  listError = e
362
628
  }
363
629
 
364
- print('Latest version: ' + pico.bold(pico.green(latest.version)) + '\n')
630
+ print(
631
+ 'Registry latest: ' + pico.bold(pico.green(latest.version)) + '\n'
632
+ )
365
633
 
366
634
  if (lock.mode === 'yarn' && lock.version !== 1) {
367
635
  updateWith(print, YARN_CMD + ' up -R caniuse-lite baseline-browser-mapping')
368
636
  } else if (lock.mode === 'pnpm') {
369
- let lockContent = readFileSync(lock.file).toString()
370
- let packages = lockContent.includes('baseline-browser-mapping')
371
- ? 'caniuse-lite baseline-browser-mapping'
372
- : 'caniuse-lite'
373
- updateWith(print, 'pnpm up --depth=9999 --no-save ' + packages)
637
+ updatePnpm(print, lock)
374
638
  } else if (lock.mode === 'bun') {
375
639
  updateBun(print, lock, latest)
376
640
  } else if (lock.mode === 'deno') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "update-browserslist-db",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "CLI tool to update caniuse-lite to refresh target browsers from Browserslist config",
5
5
  "keywords": [
6
6
  "browsers",