@npmcli/package-json
Advanced tools
+24
-14
@@ -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 @@ } |
+157
-144
@@ -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 } |
+6
-8
| { | ||
| "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 @@ }, |
+17
-3
@@ -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) |
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
52881
3.41%3
-40%1307
1.16%258
5.74%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated