You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

@npmcli/package-json

Package Overview
Dependencies
Maintainers
6
Versions
27
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@npmcli/package-json - npm Package Compare versions

Comparing version
6.2.0
to
7.0.0
+24
-14
lib/index.js

@@ -8,3 +8,3 @@ const { readFile, writeFile } = require('node:fs/promises')

const updateWorkspaces = require('./update-workspaces.js')
const normalize = require('./normalize.js')
const { normalize, syncNormalize } = require('./normalize.js')
const { read, parse } = require('./read-package.js')

@@ -29,13 +29,2 @@ const { packageSort } = require('./sort.js')

class PackageJson {
static normalizeSteps = Object.freeze([
'_id',
'_attributes',
'bundledDependencies',
'bundleDependencies',
'optionalDedupe',
'scripts',
'funding',
'bin',
])
// npm pkg fix

@@ -45,5 +34,3 @@ static fixSteps = Object.freeze([

'bundleDependencies',
'bundleDependenciesFalse',
'fixName',
'fixNameField',
'fixVersionField',

@@ -56,2 +43,14 @@ 'fixRepositoryField',

static normalizeSteps = Object.freeze([
'_id',
'_attributes',
'bundledDependencies',
'bundleDependencies',
'optionalDedupe',
'scripts',
'funding',
'bin',
'binDir',
])
static prepareSteps = Object.freeze([

@@ -171,3 +170,7 @@ '_id',

// Manually set data from an existing object
fromContent (data) {
if (!data || typeof data !== 'object') {
throw new Error('Content data must be an object')
}
this.#manifest = data

@@ -267,2 +270,9 @@ this.#canSave = false

// steps is NOT overrideable here because this is a legacy function that's not being used in new places
syncNormalize (opts = {}) {
opts.steps = this.constructor.normalizeSteps.filter(s => s !== '_attributes')
syncNormalize(this, opts)
return this
}
async normalize (opts = {}) {

@@ -269,0 +279,0 @@ if (!opts.steps) {

// Originally normalize-package-data
const url = require('node:url')
const { URL } = require('node:url')
const hostedGitInfo = require('hosted-git-info')

@@ -126,4 +126,3 @@ const validateLicense = require('validate-npm-package-license')

data.bugs = { email: data.bugs }
/* eslint-disable-next-line node/no-deprecated-api */
} else if (url.parse(data.bugs).protocol) {
} else if (URL.canParse(data.bugs)) {
data.bugs = { url: data.bugs }

@@ -144,4 +143,3 @@ } else {

if (oldBugs.url) {
/* eslint-disable-next-line node/no-deprecated-api */
if (typeof (oldBugs.url) === 'string' && url.parse(oldBugs.url).protocol) {
if (URL.canParse(oldBugs.url)) {
data.bugs.url = oldBugs.url

@@ -221,4 +219,3 @@ } else {

} else {
/* eslint-disable-next-line node/no-deprecated-api */
if (!url.parse(data.homepage).protocol) {
if (!URL.canParse(data.homepage)) {
data.homepage = 'http://' + data.homepage

@@ -225,0 +222,0 @@ }

@@ -70,3 +70,3 @@ const valid = require('semver/functions/valid')

if (binTarget !== pkg.bin[binKey]) {
changes?.push(`"bin[${base}]" script name was cleaned`)
changes?.push(`"bin[${base}]" script name ${binTarget} was invalid and removed`)
}

@@ -137,11 +137,5 @@ pkg.bin[base] = binTarget

// We don't want the `changes` array in here by default because this is a hot
// path for parsing packuments during install. So the calling method passes it
// in if it wants to track changes.
const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase }) => {
if (!pkg.content) {
throw new Error('Can not normalize without content')
}
// Only steps that can be ran synchronously. There are some object constructors (i.e. Aborist Node) that need synchronous normalization so here we are.
function syncSteps (pkg, { strict, steps, changes, allowLegacyCase }) {
const data = pkg.content
const scripts = data.scripts || {}
const pkgId = `${data.name ?? ''}@${data.version ?? ''}`

@@ -200,2 +194,3 @@

}
// remove attributes that start with "_"

@@ -220,10 +215,10 @@ if (steps.includes('_attributes')) {

// fix bundledDependencies typo
// normalize bundleDependencies
if (steps.includes('bundledDependencies')) {
if (data.bundleDependencies === undefined && data.bundledDependencies !== undefined) {
data.bundleDependencies = data.bundledDependencies
changes?.push(`Deleted incorrect "bundledDependencies"`)
}
changes?.push(`Deleted incorrect "bundledDependencies"`)
delete data.bundledDependencies
}
// expand "bundleDependencies: true or translate from object"

@@ -267,28 +262,2 @@ if (steps.includes('bundleDependencies')) {

// add "install" attribute if any "*.gyp" files exist
if (steps.includes('gypfile')) {
if (!scripts.install && !scripts.preinstall && data.gypfile !== false) {
const files = await lazyLoadGlob()('*.gyp', { cwd: pkg.path })
if (files.length) {
scripts.install = 'node-gyp rebuild'
data.scripts = scripts
data.gypfile = true
changes?.push(`"scripts.install" was set to "node-gyp rebuild"`)
changes?.push(`"gypfile" was set to "true"`)
}
}
}
// add "start" attribute if "server.js" exists
if (steps.includes('serverjs') && !scripts.start) {
try {
await fs.access(path.join(pkg.path, 'server.js'))
scripts.start = 'node server.js'
data.scripts = scripts
changes?.push('"scripts.start" was set to "node server.js"')
} catch {
// do nothing
}
}
// strip "node_modules/.bin" from scripts entries

@@ -321,2 +290,133 @@ // remove invalid scripts entries (non-strings)

// "normalizeData" from "read-package-json", which was just a call through to
// "normalize-package-data". We only call the "fixer" functions because
// outside of that it was also clobbering _id (which we already conditionally
// do) and also adding the gypfile script (which we also already
// conditionally do)
// Some steps are isolated so we can do a limited subset of these in `fix`
if (steps.includes('fixRepositoryField') || steps.includes('normalizeData')) {
if (data.repositories) {
changes?.push(`"repository" was set to the first entry in "repositories" (${data.repository})`)
data.repository = data.repositories[0]
}
if (data.repository) {
if (typeof data.repository === 'string') {
changes?.push('"repository" was changed from a string to an object')
data.repository = {
type: 'git',
url: data.repository,
}
}
if (data.repository.url) {
const hosted = lazyHostedGitInfo().fromUrl(data.repository.url)
let r
if (hosted) {
if (hosted.getDefaultRepresentation() === 'shortcut') {
r = hosted.https()
} else {
r = hosted.toString()
}
if (r !== data.repository.url) {
changes?.push(`"repository.url" was normalized to "${r}"`)
data.repository.url = r
}
}
}
}
}
if (steps.includes('fixDependencies') || steps.includes('normalizeData')) {
// peerDependencies?
// devDependencies is meaningless here, it's ignored on an installed package
for (const type of ['dependencies', 'devDependencies', 'optionalDependencies']) {
if (data[type]) {
let secondWarning = true
if (typeof data[type] === 'string') {
changes?.push(`"${type}" was converted from a string into an object`)
data[type] = data[type].trim().split(/[\n\r\s\t ,]+/)
secondWarning = false
}
if (Array.isArray(data[type])) {
if (secondWarning) {
changes?.push(`"${type}" was converted from an array into an object`)
}
const o = {}
for (const d of data[type]) {
if (typeof d === 'string') {
const dep = d.trim().split(/(:?[@\s><=])/)
const dn = dep.shift()
const dv = dep.join('').replace(/^@/, '').trim()
o[dn] = dv
}
}
data[type] = o
}
}
}
// normalize-package-data used to put optional dependencies BACK into
// dependencies here, we no longer do this
for (const deps of ['dependencies', 'devDependencies']) {
if (deps in data) {
if (!data[deps] || typeof data[deps] !== 'object') {
changes?.push(`Removed invalid "${deps}"`)
delete data[deps]
} else {
for (const d in data[deps]) {
const r = data[deps][d]
if (typeof r !== 'string') {
changes?.push(`Removed invalid "${deps}.${d}"`)
delete data[deps][d]
}
const hosted = lazyHostedGitInfo().fromUrl(data[deps][d])?.toString()
if (hosted && hosted !== data[deps][d]) {
changes?.push(`Normalized git reference to "${deps}.${d}"`)
data[deps][d] = hosted.toString()
}
}
}
}
}
}
// TODO some of this is duplicated in other steps here, a future breaking change may be able to remove the duplicates involved in this step
if (steps.includes('normalizeData')) {
const { normalizeData } = require('./normalize-data.js')
normalizeData(data, changes)
}
}
// Steps that require await, distinct from sync-steps.js
async function asyncSteps (pkg, { steps, root, changes }) {
const data = pkg.content
const scripts = data.scripts || {}
const pkgId = `${data.name ?? ''}@${data.version ?? ''}`
// add "install" attribute if any "*.gyp" files exist
if (steps.includes('gypfile')) {
if (!scripts.install && !scripts.preinstall && data.gypfile !== false) {
const files = await lazyLoadGlob()('*.gyp', { cwd: pkg.path })
if (files.length) {
scripts.install = 'node-gyp rebuild'
data.scripts = scripts
data.gypfile = true
changes?.push(`"scripts.install" was set to "node-gyp rebuild"`)
changes?.push(`"gypfile" was set to "true"`)
}
}
}
// add "start" attribute if "server.js" exists
if (steps.includes('serverjs') && !scripts.start) {
try {
await fs.access(path.join(pkg.path, 'server.js'))
scripts.start = 'node server.js'
data.scripts = scripts
changes?.push('"scripts.start" was set to "node server.js"')
} catch {
// do nothing
}
}
// populate "authors" attribute

@@ -382,18 +482,15 @@ if (steps.includes('authors') && !data.contributors) {

if (steps.includes('bin') || steps.includes('binDir') || steps.includes('binRefs')) {
normalizePackageBin(data, changes)
}
// expand "directories.bin"
if (steps.includes('binDir') && data.directories?.bin && !data.bin) {
const binsDir = path.resolve(pkg.path, secureAndUnixifyPath(data.directories.bin))
const bins = await lazyLoadGlob()('**', { cwd: binsDir })
const binPath = secureAndUnixifyPath(data.directories.bin)
const bins = await lazyLoadGlob()('**', { cwd: path.resolve(pkg.path, binPath) })
data.bin = bins.reduce((acc, binFile) => {
if (binFile && !binFile.startsWith('.')) {
const binName = path.basename(binFile)
acc[binName] = path.join(data.directories.bin, binFile)
// binPath is already cleaned and unixified, no need to path.join here.
acc[binName] = `${binPath}/${secureAndUnixifyPath(binFile)}`
}
return acc
}, {})
// *sigh*
} else if (steps.includes('bin') || steps.includes('binDir') || steps.includes('binRefs')) {
normalizePackageBin(data, changes)

@@ -496,100 +593,2 @@ }

// "normalizeData" from "read-package-json", which was just a call through to
// "normalize-package-data". We only call the "fixer" functions because
// outside of that it was also clobbering _id (which we already conditionally
// do) and also adding the gypfile script (which we also already
// conditionally do)
// Some steps are isolated so we can do a limited subset of these in `fix`
if (steps.includes('fixRepositoryField') || steps.includes('normalizeData')) {
if (data.repositories) {
changes?.push(`"repository" was set to the first entry in "repositories" (${data.repository})`)
data.repository = data.repositories[0]
}
if (data.repository) {
if (typeof data.repository === 'string') {
changes?.push('"repository" was changed from a string to an object')
data.repository = {
type: 'git',
url: data.repository,
}
}
if (data.repository.url) {
const hosted = lazyHostedGitInfo().fromUrl(data.repository.url)
let r
if (hosted) {
if (hosted.getDefaultRepresentation() === 'shortcut') {
r = hosted.https()
} else {
r = hosted.toString()
}
if (r !== data.repository.url) {
changes?.push(`"repository.url" was normalized to "${r}"`)
data.repository.url = r
}
}
}
}
}
if (steps.includes('fixDependencies') || steps.includes('normalizeData')) {
// peerDependencies?
// devDependencies is meaningless here, it's ignored on an installed package
for (const type of ['dependencies', 'devDependencies', 'optionalDependencies']) {
if (data[type]) {
let secondWarning = true
if (typeof data[type] === 'string') {
changes?.push(`"${type}" was converted from a string into an object`)
data[type] = data[type].trim().split(/[\n\r\s\t ,]+/)
secondWarning = false
}
if (Array.isArray(data[type])) {
if (secondWarning) {
changes?.push(`"${type}" was converted from an array into an object`)
}
const o = {}
for (const d of data[type]) {
if (typeof d === 'string') {
const dep = d.trim().split(/(:?[@\s><=])/)
const dn = dep.shift()
const dv = dep.join('').replace(/^@/, '').trim()
o[dn] = dv
}
}
data[type] = o
}
}
}
// normalize-package-data used to put optional dependencies BACK into
// dependencies here, we no longer do this
for (const deps of ['dependencies', 'devDependencies']) {
if (deps in data) {
if (!data[deps] || typeof data[deps] !== 'object') {
changes?.push(`Removed invalid "${deps}"`)
delete data[deps]
} else {
for (const d in data[deps]) {
const r = data[deps][d]
if (typeof r !== 'string') {
changes?.push(`Removed invalid "${deps}.${d}"`)
delete data[deps][d]
}
const hosted = lazyHostedGitInfo().fromUrl(data[deps][d])?.toString()
if (hosted && hosted !== data[deps][d]) {
changes?.push(`Normalized git reference to "${deps}.${d}"`)
data[deps][d] = hosted.toString()
}
}
}
}
}
}
// TODO some of this is duplicated in other steps here, a future breaking change may be able to remove the duplicates involved in this step
if (steps.includes('normalizeData')) {
const { normalizeData } = require('./normalize-data.js')
normalizeData(data, changes)
}
// Warn if the bin references don't point to anything. This might be better

@@ -609,2 +608,16 @@ // in normalize-package-data if it had access to the file path.

module.exports = normalize
// We don't want the `changes` array in here by default because this is a hot path for parsing packuments during install. The calling method passes it in if it wants to track changes.
async function normalize (pkg, opts) {
if (!pkg.content) {
throw new Error('Can not normalize without content')
}
await asyncSteps(pkg, opts)
// the normalizeData part of this needs to be the last thing ran, so sync comes second
syncSteps(pkg, opts)
}
function syncNormalize (pkg, opts) {
syncSteps(pkg, opts)
}
module.exports = { normalize, syncNormalize }
{
"name": "@npmcli/package-json",
"version": "6.2.0",
"version": "7.0.0",
"description": "Programmatic API to update package.json",

@@ -33,4 +33,4 @@ "keywords": [

"@npmcli/git": "^6.0.0",
"glob": "^10.2.2",
"hosted-git-info": "^8.0.0",
"glob": "^11.0.3",
"hosted-git-info": "^9.0.0",
"json-parse-even-better-errors": "^4.0.0",

@@ -43,13 +43,11 @@ "proc-log": "^5.0.0",

"@npmcli/eslint-config": "^5.1.0",
"@npmcli/template-oss": "4.23.6",
"read-package-json": "^7.0.0",
"read-package-json-fast": "^4.0.0",
"@npmcli/template-oss": "4.25.0",
"tap": "^16.0.1"
},
"engines": {
"node": "^18.17.0 || >=20.5.0"
"node": "^20.17.0 || >=22.9.0"
},
"templateOSS": {
"//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
"version": "4.23.6",
"version": "4.25.0",
"publish": "true"

@@ -56,0 +54,0 @@ },

@@ -147,2 +147,6 @@ # @npmcli/package-json

### `PackageJson.syncNormalize()`
This calls normalize synchronously. Most consumers of this package should avoid using this. It was added because some parts of npm were normalizing package content in class constructors and needed this affordance. It will silently ignore any asynchronous steps asked for. Again, this is a compatiblity affordance for some code in npm that is currently impossible to change without a significant semver major change, and is best not used.
### **static** `async PackageJson.prepare(path, opts = {})`

@@ -237,9 +241,19 @@

### `async PackageJson.save()`
### `async PackageJson.save([options])`
Saves the current `content` to the same location used when calling
`load()`.
Saves the current `content` to the same location used when calling `load()`.
- `options`: `Object` (optional)
- `sort`: `Boolean` (optional) — If true, sorts the keys in the resulting `package.json` file for consistency and readability.
> [!NOTE]
> The sort order for `package.json` is based on the conventions from
> [sort-package-json](https://github.com/keithamus/sort-package-json/blob/main/defaultRules.md),
> cross-checked with the official npm types and documentation:
> - https://github.com/npm/types/blob/main/types/index.d.ts#L104
> - https://docs.npmjs.com/cli/configuring-npm/package-json
## LICENSE
[ISC](./LICENSE)