diff --git a/node_modules/@npmcli/package-json/lib/index.js b/node_modules/@npmcli/package-json/lib/index.js index 756837cdde58..53558a3977e4 100644 --- a/node_modules/@npmcli/package-json/lib/index.js +++ b/node_modules/@npmcli/package-json/lib/index.js @@ -34,7 +34,23 @@ class PackageJson { 'bin', ]) + // npm pkg fix + static fixSteps = Object.freeze([ + 'binRefs', + 'bundleDependencies', + 'bundleDependenciesFalse', + 'fixNameField', + 'fixVersionField', + 'fixRepositoryField', + 'fixBinField', + 'fixDependencies', + 'fixScriptsField', + 'devDependencies', + 'scriptpath', + ]) + static prepareSteps = Object.freeze([ + '_id', '_attributes', 'bundledDependencies', 'bundleDependencies', @@ -52,37 +68,67 @@ class PackageJson { 'binRefs', ]) - // default behavior, just loads and parses - static async load (path) { - return await new PackageJson(path).load() + // create a new empty package.json, so we can save at the given path even + // though we didn't start from a parsed file + static async create (path, opts = {}) { + const p = new PackageJson() + await p.create(path) + if (opts.data) { + return p.update(opts.data) + } + return p + } + + // Loads a package.json at given path and JSON parses + static async load (path, opts = {}) { + const p = new PackageJson() + // Avoid try/catch if we aren't going to create + if (!opts.create) { + return p.load(path) + } + + try { + return await p.load(path) + } catch (err) { + if (!err.message.startsWith('Could not read package.json')) { + throw err + } + return await p.create(path) + } + } + + // npm pkg fix + static async fix (path, opts) { + const p = new PackageJson() + await p.load(path, true) + return p.fix(opts) } // read-package-json compatible behavior static async prepare (path, opts) { - return await new PackageJson(path).prepare(opts) + const p = new PackageJson() + await p.load(path, true) + return p.prepare(opts) } // read-package-json-fast compatible behavior static async normalize (path, opts) { - return await new PackageJson(path).normalize(opts) + const p = new PackageJson() + await p.load(path) + return p.normalize(opts) } - #filename #path - #manifest = {} + #manifest #readFileContent = '' - #fromIndex = false + #canSave = true - constructor (path) { + // Load content from given path + async load (path, parseIndex) { this.#path = path - this.#filename = resolve(path, 'package.json') - } - - async load (parseIndex) { let parseErr try { - this.#readFileContent = - await readFile(this.#filename, 'utf8') + this.#readFileContent = await readFile(this.filename, 'utf8') } catch (err) { err.message = `Could not read package.json: ${err}` if (!parseIndex) { @@ -92,7 +138,7 @@ class PackageJson { } if (parseErr) { - const indexFile = resolve(this.#path, 'index.js') + const indexFile = resolve(this.path, 'index.js') let indexFileContent try { indexFileContent = await readFile(indexFile, 'utf8') @@ -100,16 +146,22 @@ class PackageJson { throw parseErr } try { - this.#manifest = fromComment(indexFileContent) + this.fromComment(indexFileContent) } catch (err) { throw parseErr } - this.#fromIndex = true + // This wasn't a package.json so prevent saving + this.#canSave = false return this } + return this.fromJSON(this.#readFileContent) + } + + // Load data from a JSON string/buffer + fromJSON (data) { try { - this.#manifest = parseJSON(this.#readFileContent) + this.#manifest = parseJSON(data) } catch (err) { err.message = `Invalid package.json: ${err}` throw err @@ -117,6 +169,27 @@ class PackageJson { return this } + // Load data from a comment + // /**package { "name": "foo", "version": "1.2.3", ... } **/ + fromComment (data) { + data = data.split(/^\/\*\*package(?:\s|$)/m) + + if (data.length < 2) { + throw new Error('File has no package in comments') + } + data = data[1] + data = data.split(/\*\*\/$/m) + + if (data.length < 2) { + throw new Error('File has no package in comments') + } + data = data[0] + data = data.replace(/^\s*\*/mg, '') + + this.#manifest = parseJSON(data) + return this + } + get content () { return this.#manifest } @@ -125,26 +198,33 @@ class PackageJson { return this.#path } + get filename () { + if (this.path) { + return resolve(this.path, 'package.json') + } + return undefined + } + + create (path) { + this.#path = path + this.#manifest = {} + return this + } + + // This should be the ONLY way to set content in the manifest update (content) { - // validates both current manifest and content param - const invalidContent = - typeof this.#manifest !== 'object' - || typeof content !== 'object' - if (invalidContent) { - throw Object.assign( - new Error(`Can't update invalid package.json data`), - { code: 'EPACKAGEJSONUPDATE' } - ) + if (!this.content) { + throw new Error('Can not update without content. Please `load` or `create`') } for (const step of knownSteps) { - this.#manifest = step({ content, originalContent: this.#manifest }) + this.#manifest = step({ content, originalContent: this.content }) } // unknown properties will just be overwitten for (const [key, value] of Object.entries(content)) { if (!knownKeys.has(key)) { - this.#manifest[key] = value + this.content[key] = value } } @@ -152,23 +232,23 @@ class PackageJson { } async save () { - if (this.#fromIndex) { + if (!this.#canSave) { throw new Error('No package.json to save to') } const { [Symbol.for('indent')]: indent, [Symbol.for('newline')]: newline, - } = this.#manifest + } = this.content const format = indent === undefined ? ' ' : indent const eol = newline === undefined ? '\n' : newline const fileContent = `${ - JSON.stringify(this.#manifest, null, format) + JSON.stringify(this.content, null, format) }\n` .replace(/\n/g, eol) if (fileContent.trim() !== this.#readFileContent.trim()) { - return await writeFile(this.#filename, fileContent) + return await writeFile(this.filename, fileContent) } } @@ -176,7 +256,6 @@ class PackageJson { if (!opts.steps) { opts.steps = this.constructor.normalizeSteps } - await this.load() await normalize(this, opts) return this } @@ -185,29 +264,16 @@ class PackageJson { if (!opts.steps) { opts.steps = this.constructor.prepareSteps } - await this.load(true) await normalize(this, opts) return this } -} - -// /**package { "name": "foo", "version": "1.2.3", ... } **/ -function fromComment (data) { - data = data.split(/^\/\*\*package(?:\s|$)/m) - if (data.length < 2) { - throw new Error('File has no package in comments') - } - data = data[1] - data = data.split(/\*\*\/$/m) - - if (data.length < 2) { - throw new Error('File has no package in comments') + async fix (opts = {}) { + // This one is not overridable + opts.steps = this.constructor.fixSteps + await normalize(this, opts) + return this } - data = data[0] - data = data.replace(/^\s*\*/mg, '') - - return parseJSON(data) } module.exports = PackageJson diff --git a/node_modules/@npmcli/package-json/lib/normalize.js b/node_modules/@npmcli/package-json/lib/normalize.js index 9594ef3d7ff4..726b3f031115 100644 --- a/node_modules/@npmcli/package-json/lib/normalize.js +++ b/node_modules/@npmcli/package-json/lib/normalize.js @@ -1,20 +1,40 @@ const fs = require('fs/promises') const { glob } = require('glob') const normalizePackageBin = require('npm-normalize-package-bin') -const normalizePackageData = require('normalize-package-data') +const legacyFixer = require('normalize-package-data/lib/fixer.js') +const legacyMakeWarning = require('normalize-package-data/lib/make_warning.js') const path = require('path') const log = require('proc-log') const git = require('@npmcli/git') -const normalize = async (pkg, { strict, steps, root }) => { +// 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') + } const data = pkg.content const scripts = data.scripts || {} const pkgId = `${data.name ?? ''}@${data.version ?? ''}` + legacyFixer.warn = function () { + changes?.push(legacyMakeWarning.apply(null, arguments)) + } + + // name and version are load bearing so we have to clean them up first + if (steps.includes('fixNameField') || steps.includes('normalizeData')) { + legacyFixer.fixNameField(data, { strict, allowLegacyCase }) + } + + if (steps.includes('fixVersionField') || steps.includes('normalizeData')) { + legacyFixer.fixVersionField(data, strict) + } // remove attributes that start with "_" if (steps.includes('_attributes')) { for (const key in data) { if (key.startsWith('_')) { + changes?.push(`"${key}" was removed`) delete pkg.content[key] } } @@ -23,6 +43,7 @@ const normalize = async (pkg, { strict, steps, root }) => { // build the "_id" attribute if (steps.includes('_id')) { if (data.name && data.version) { + changes?.push(`"_id" was set to ${pkgId}`) data._id = pkgId } } @@ -32,20 +53,25 @@ const normalize = async (pkg, { strict, steps, root }) => { if (data.bundleDependencies === undefined && data.bundledDependencies !== undefined) { data.bundleDependencies = data.bundledDependencies } + changes?.push(`Deleted incorrect "bundledDependencies"`) delete data.bundledDependencies } // expand "bundleDependencies: true or translate from object" if (steps.includes('bundleDependencies')) { const bd = data.bundleDependencies if (bd === false && !steps.includes('bundleDependenciesDeleteFalse')) { + changes?.push(`"bundleDependencies" was changed from "false" to "[]"`) data.bundleDependencies = [] } else if (bd === true) { + changes?.push(`"bundleDependencies" was auto-populated from "dependencies"`) data.bundleDependencies = Object.keys(data.dependencies || {}) } else if (bd && typeof bd === 'object') { if (!Array.isArray(bd)) { + changes?.push(`"bundleDependencies" was changed from an object to an array`) data.bundleDependencies = Object.keys(bd) } } else { + changes?.push(`"bundleDependencies" was removed`) delete data.bundleDependencies } } @@ -58,9 +84,11 @@ const normalize = async (pkg, { strict, steps, root }) => { if (data.dependencies && data.optionalDependencies && typeof data.optionalDependencies === 'object') { for (const name in data.optionalDependencies) { + changes?.push(`optionalDependencies entry "${name}" was removed`) delete data.dependencies[name] } if (!Object.keys(data.dependencies).length) { + changes?.push(`empty "optionalDependencies" was removed`) delete data.dependencies } } @@ -74,6 +102,8 @@ const normalize = async (pkg, { strict, steps, root }) => { 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"`) } } } @@ -84,6 +114,7 @@ const normalize = async (pkg, { strict, steps, root }) => { 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 } @@ -96,11 +127,14 @@ const normalize = async (pkg, { strict, steps, root }) => { for (const name in data.scripts) { if (typeof data.scripts[name] !== 'string') { delete data.scripts[name] + changes?.push(`invalid scripts entry "${name}" was removed`) } else if (steps.includes('scriptpath')) { data.scripts[name] = data.scripts[name].replace(spre, '') + changes?.push(`scripts entry "${name}" was fixed to remove node_modules/.bin reference`) } } } else { + changes?.push(`removed invalid "scripts"`) delete data.scripts } } @@ -108,6 +142,7 @@ const normalize = async (pkg, { strict, steps, root }) => { if (steps.includes('funding')) { if (data.funding && typeof data.funding === 'string') { data.funding = { url: data.funding } + changes?.push(`"funding" was changed to an object with a url attribute`) } } @@ -119,6 +154,7 @@ const normalize = async (pkg, { strict, steps, root }) => { .map(line => line.replace(/^\s*#.*$/, '').trim()) .filter(line => line) data.contributors = authors + changes.push('"contributors" was auto-populated with the contents of the "AUTHORS" file') } catch { // do nothing } @@ -145,6 +181,12 @@ const normalize = async (pkg, { strict, steps, root }) => { const readmeData = await fs.readFile(path.join(pkg.path, readmeFile), 'utf8') data.readme = readmeData data.readmeFilename = readmeFile + changes?.push(`"readme" was set to the contents of ${readmeFile}`) + changes?.push(`"readmeFilename" was set to ${readmeFile}`) + } + if (!data.readme) { + // this.warn('missingReadme') + data.readme = 'ERROR: No README data found!' } } @@ -270,9 +312,47 @@ const normalize = async (pkg, { strict, steps, root }) => { } } - // "normalizeData" from read-package-json + // "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')) { + legacyFixer.fixRepositoryField(data) + } + + if (steps.includes('fixBinField') || steps.includes('normalizeData')) { + legacyFixer.fixBinField(data) + } + + if (steps.includes('fixDependencies') || steps.includes('normalizeData')) { + legacyFixer.fixDependencies(data, strict) + } + + if (steps.includes('fixScriptsField') || steps.includes('normalizeData')) { + legacyFixer.fixScriptsField(data) + } + if (steps.includes('normalizeData')) { - normalizePackageData(data, strict) + const legacySteps = [ + 'fixDescriptionField', + 'fixModulesField', + 'fixFilesField', + 'fixManField', + 'fixBugsField', + 'fixKeywordsField', + 'fixBundleDependenciesField', + 'fixHomepageField', + 'fixReadmeField', + 'fixLicenseField', + 'fixPeople', + 'fixTypos', + ] + for (const legacyStep of legacySteps) { + legacyFixer[legacyStep](data) + } } // Warn if the bin references don't point to anything. This might be better diff --git a/node_modules/@npmcli/package-json/package.json b/node_modules/@npmcli/package-json/package.json index a4e2cbab4c0b..4b9584dcad37 100644 --- a/node_modules/@npmcli/package-json/package.json +++ b/node_modules/@npmcli/package-json/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/package-json", - "version": "3.1.1", + "version": "4.0.0", "description": "Programmatic API to update package.json", "main": "lib/index.js", "files": [ diff --git a/package-lock.json b/package-lock.json index ac4ba66c3c26..4fd280b49c3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,7 +88,7 @@ "@npmcli/arborist": "^6.2.10", "@npmcli/config": "^6.2.1", "@npmcli/map-workspaces": "^3.0.4", - "@npmcli/package-json": "^3.1.1", + "@npmcli/package-json": "^4.0.0", "@npmcli/run-script": "^6.0.2", "abbrev": "^2.0.0", "archy": "~1.0.0", @@ -2486,9 +2486,9 @@ } }, "node_modules/@npmcli/package-json": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-3.1.1.tgz", - "integrity": "sha512-+UW0UWOYFKCkvszLoTwrYGrjNrT8tI5Ckeb/h+Z1y1fsNJEctl7HmerA5j2FgmoqFaLI2gsA1X9KgMFqx/bRmA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-4.0.0.tgz", + "integrity": "sha512-ZeXtZBQ/xjSUmrZj9R1Y2gsQRfkdhP5H31SCieJbAd8bHbn4YRglOoajcEZTJTM9m9BuEE7KiDcMPEoD/OgJkw==", "inBundle": true, "dependencies": { "@npmcli/git": "^4.1.0", @@ -2586,6 +2586,23 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@npmcli/template-oss/node_modules/@npmcli/package-json": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-3.1.1.tgz", + "integrity": "sha512-+UW0UWOYFKCkvszLoTwrYGrjNrT8tI5Ckeb/h+Z1y1fsNJEctl7HmerA5j2FgmoqFaLI2gsA1X9KgMFqx/bRmA==", + "dev": true, + "dependencies": { + "@npmcli/git": "^4.1.0", + "glob": "^10.2.2", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "npm-normalize-package-bin": "^3.0.1", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/@octokit/auth-token": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.3.tgz", @@ -15589,7 +15606,7 @@ "@npmcli/metavuln-calculator": "^5.0.0", "@npmcli/name-from-folder": "^2.0.0", "@npmcli/node-gyp": "^3.0.0", - "@npmcli/package-json": "^3.0.0", + "@npmcli/package-json": "^4.0.0", "@npmcli/query": "^3.0.0", "@npmcli/run-script": "^6.0.0", "bin-links": "^4.0.1", diff --git a/package.json b/package.json index d335bb8dabed..f5f555bd8ee5 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@npmcli/arborist": "^6.2.10", "@npmcli/config": "^6.2.1", "@npmcli/map-workspaces": "^3.0.4", - "@npmcli/package-json": "^3.1.1", + "@npmcli/package-json": "^4.0.0", "@npmcli/run-script": "^6.0.2", "abbrev": "^2.0.0", "archy": "~1.0.0", diff --git a/workspaces/arborist/package.json b/workspaces/arborist/package.json index 15c0640fb90b..ed13180bbb65 100644 --- a/workspaces/arborist/package.json +++ b/workspaces/arborist/package.json @@ -10,7 +10,7 @@ "@npmcli/metavuln-calculator": "^5.0.0", "@npmcli/name-from-folder": "^2.0.0", "@npmcli/node-gyp": "^3.0.0", - "@npmcli/package-json": "^3.0.0", + "@npmcli/package-json": "^4.0.0", "@npmcli/query": "^3.0.0", "@npmcli/run-script": "^6.0.0", "bin-links": "^4.0.1",