diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 0c8c7812b1831..0146c658fd828 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -216,11 +216,12 @@ graph LR; npmcli-mock-registry-->npmcli-eslint-config["@npmcli/eslint-config"]; npmcli-mock-registry-->npmcli-template-oss["@npmcli/template-oss"]; npmcli-mock-registry-->pacote; + npmcli-package-json-->hosted-git-info; npmcli-package-json-->json-parse-even-better-errors; npmcli-package-json-->normalize-package-data; - npmcli-package-json-->npm-normalize-package-bin; npmcli-package-json-->npmcli-git["@npmcli/git"]; npmcli-package-json-->proc-log; + npmcli-package-json-->semver; npmcli-run-script-->npmcli-node-gyp["@npmcli/node-gyp"]; npmcli-run-script-->npmcli-promise-spawn["@npmcli/promise-spawn"]; npmcli-run-script-->read-package-json-fast; @@ -700,11 +701,12 @@ graph LR; npmcli-mock-registry-->pacote; npmcli-mock-registry-->tap; npmcli-package-json-->glob; + npmcli-package-json-->hosted-git-info; npmcli-package-json-->json-parse-even-better-errors; npmcli-package-json-->normalize-package-data; - npmcli-package-json-->npm-normalize-package-bin; npmcli-package-json-->npmcli-git["@npmcli/git"]; npmcli-package-json-->proc-log; + npmcli-package-json-->semver; npmcli-promise-spawn-->which; npmcli-query-->postcss-selector-parser; npmcli-run-script-->node-gyp; diff --git a/node_modules/@npmcli/package-json/lib/index.js b/node_modules/@npmcli/package-json/lib/index.js index 53558a3977e4d..0cc41c685a39e 100644 --- a/node_modules/@npmcli/package-json/lib/index.js +++ b/node_modules/@npmcli/package-json/lib/index.js @@ -42,9 +42,7 @@ class PackageJson { 'fixNameField', 'fixVersionField', 'fixRepositoryField', - 'fixBinField', 'fixDependencies', - 'fixScriptsField', 'devDependencies', 'scriptpath', ]) diff --git a/node_modules/@npmcli/package-json/lib/normalize.js b/node_modules/@npmcli/package-json/lib/normalize.js index 726b3f031115b..204d4d8a8e7dd 100644 --- a/node_modules/@npmcli/package-json/lib/normalize.js +++ b/node_modules/@npmcli/package-json/lib/normalize.js @@ -1,11 +1,89 @@ +const semver = require('semver') const fs = require('fs/promises') const { glob } = require('glob') -const normalizePackageBin = require('npm-normalize-package-bin') 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 hostedGitInfo = require('hosted-git-info') + +// used to be npm-normalize-package-bin +function normalizePackageBin (pkg, changes) { + if (pkg.bin) { + if (typeof pkg.bin === 'string' && pkg.name) { + changes?.push('"bin" was converted to an object') + pkg.bin = { [pkg.name]: pkg.bin } + } else if (Array.isArray(pkg.bin)) { + changes?.push('"bin" was converted to an object') + pkg.bin = pkg.bin.reduce((acc, k) => { + acc[path.basename(k)] = k + return acc + }, {}) + } + if (typeof pkg.bin === 'object') { + for (const binKey in pkg.bin) { + if (typeof pkg.bin[binKey] !== 'string') { + delete pkg.bin[binKey] + changes?.push(`removed invalid "bin[${binKey}]"`) + continue + } + const base = path.join('/', path.basename(binKey.replace(/\\|:/g, '/'))).slice(1) + if (!base) { + delete pkg.bin[binKey] + changes?.push(`removed invalid "bin[${binKey}]"`) + continue + } + + const binTarget = path.join('/', pkg.bin[binKey].replace(/\\/g, '/')) + .replace(/\\/g, '/').slice(1) + + if (!binTarget) { + delete pkg.bin[binKey] + changes?.push(`removed invalid "bin[${binKey}]"`) + continue + } + + if (base !== binKey) { + delete pkg.bin[binKey] + changes?.push(`"bin[${binKey}]" was renamed to "bin[${base}]"`) + } + if (binTarget !== pkg.bin[binKey]) { + changes?.push(`"bin[${base}]" script name was cleaned`) + } + pkg.bin[base] = binTarget + } + + if (Object.keys(pkg.bin).length === 0) { + changes?.push('empty "bin" was removed') + delete pkg.bin + } + + return pkg + } + } + delete pkg.bin +} + +function isCorrectlyEncodedName (spec) { + return !spec.match(/[/@\s+%:]/) && + spec === encodeURIComponent(spec) +} + +function isValidScopedPackageName (spec) { + if (spec.charAt(0) !== '@') { + return false + } + + const rest = spec.slice(1).split('/') + if (rest.length !== 2) { + return false + } + + return rest[0] && rest[1] && + rest[0] === encodeURIComponent(rest[0]) && + rest[1] === encodeURIComponent(rest[1]) +} // 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 @@ -18,17 +96,49 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase }) 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 (!data.name && !strict) { + changes?.push('Missing "name" field was set to an empty string') + data.name = '' + } else { + if (typeof data.name !== 'string') { + throw new Error('name field must be a string.') + } + if (!strict) { + const name = data.name.trim() + if (data.name !== name) { + changes?.push(`Whitespace was trimmed from "name"`) + data.name = name + } + } + + if (data.name.startsWith('.') || + !(isValidScopedPackageName(data.name) || isCorrectlyEncodedName(data.name)) || + (strict && (!allowLegacyCase) && data.name !== data.name.toLowerCase()) || + data.name.toLowerCase() === 'node_modules' || + data.name.toLowerCase() === 'favicon.ico') { + throw new Error('Invalid name: ' + JSON.stringify(data.name)) + } + } } if (steps.includes('fixVersionField') || steps.includes('normalizeData')) { - legacyFixer.fixVersionField(data, strict) + // allow "loose" semver 1.0 versions in non-strict mode + // enforce strict semver 2.0 compliance in strict mode + const loose = !strict + if (!data.version) { + data.version = '' + } else { + if (!semver.valid(data.version, loose)) { + throw new Error(`Invalid version: "${data.version}"`) + } + const version = semver.clean(data.version, loose) + if (version !== data.version) { + changes?.push(`"version" was cleaned and set to "${version}"`) + data.version = version + } + } } // remove attributes that start with "_" if (steps.includes('_attributes')) { @@ -49,6 +159,7 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase }) } // fix bundledDependencies typo + // normalize bundleDependencies if (steps.includes('bundledDependencies')) { if (data.bundleDependencies === undefined && data.bundledDependencies !== undefined) { data.bundleDependencies = data.bundledDependencies @@ -70,7 +181,7 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase }) changes?.push(`"bundleDependencies" was changed from an object to an array`) data.bundleDependencies = Object.keys(bd) } - } else { + } else if ('bundleDependencies' in data) { changes?.push(`"bundleDependencies" was removed`) delete data.bundleDependencies } @@ -84,11 +195,11 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase }) if (data.dependencies && data.optionalDependencies && typeof data.optionalDependencies === 'object') { for (const name in data.optionalDependencies) { - changes?.push(`optionalDependencies entry "${name}" was removed`) + changes?.push(`optionalDependencies."${name}" was removed`) delete data.dependencies[name] } if (!Object.keys(data.dependencies).length) { - changes?.push(`empty "optionalDependencies" was removed`) + changes?.push(`Empty "optionalDependencies" was removed`) delete data.dependencies } } @@ -121,20 +232,21 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase }) } // strip "node_modules/.bin" from scripts entries + // remove invalid scripts entries (non-strings) if (steps.includes('scripts') || steps.includes('scriptpath')) { const spre = /^(\.[/\\])?node_modules[/\\].bin[\\/]/ if (typeof data.scripts === 'object') { 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')) { + changes?.push(`Invalid scripts."${name}" was removed`) + } else if (steps.includes('scriptpath') && spre.test(data.scripts[name])) { 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"`) + changes?.push(`Removed invalid "scripts"`) delete data.scripts } } @@ -154,7 +266,7 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase }) .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') + changes?.push('"contributors" was auto-populated with the contents of the "AUTHORS" file') } catch { // do nothing } @@ -201,7 +313,7 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase }) } if (steps.includes('bin') || steps.includes('binDir') || steps.includes('binRefs')) { - normalizePackageBin(data) + normalizePackageBin(data, changes) } // expand "directories.bin" @@ -216,7 +328,7 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase }) return acc }, {}) // *sigh* - normalizePackageBin(data) + normalizePackageBin(data, changes) } // populate "gitHead" attribute @@ -320,22 +432,96 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase }) // 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 (data.repositories) { + /* eslint-disable-next-line max-len */ + 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 = hostedGitInfo.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')) { - legacyFixer.fixDependencies(data, strict) - } + // 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 - if (steps.includes('fixScriptsField') || steps.includes('normalizeData')) { - legacyFixer.fixScriptsField(data) + 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 = hostedGitInfo.fromUrl(data[deps][d])?.toString() + if (hosted && hosted !== data[deps][d]) { + changes?.push(`Normalized git reference to "${deps}.${d}"`) + data[deps][d] = hosted.toString() + } + } + } + } + } } if (steps.includes('normalizeData')) { + legacyFixer.warn = function () { + changes?.push(legacyMakeWarning.apply(null, arguments)) + } + const legacySteps = [ 'fixDescriptionField', 'fixModulesField', diff --git a/node_modules/@npmcli/package-json/package.json b/node_modules/@npmcli/package-json/package.json index 4b9584dcad370..33215b638db6e 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": "4.0.0", + "version": "4.0.1", "description": "Programmatic API to update package.json", "main": "lib/index.js", "files": [ @@ -25,7 +25,7 @@ "license": "ISC", "devDependencies": { "@npmcli/eslint-config": "^4.0.0", - "@npmcli/template-oss": "4.15.1", + "@npmcli/template-oss": "4.17.0", "read-package-json": "^6.0.4", "read-package-json-fast": "^3.0.2", "tap": "^16.0.1" @@ -33,10 +33,11 @@ "dependencies": { "@npmcli/git": "^4.1.0", "glob": "^10.2.2", + "hosted-git-info": "^6.1.1", "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" + "proc-log": "^3.0.0", + "semver": "^7.5.3" }, "repository": { "type": "git", @@ -47,7 +48,7 @@ }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.15.1", + "version": "4.17.0", "publish": "true" }, "tap": { diff --git a/package-lock.json b/package-lock.json index caf3500a7db9c..ec5488b896ae3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -91,7 +91,7 @@ "@npmcli/config": "^6.2.1", "@npmcli/fs": "^3.1.0", "@npmcli/map-workspaces": "^3.0.4", - "@npmcli/package-json": "^4.0.0", + "@npmcli/package-json": "^4.0.1", "@npmcli/promise-spawn": "^6.0.2", "@npmcli/run-script": "^6.0.2", "abbrev": "^2.0.0", @@ -2488,17 +2488,18 @@ } }, "node_modules/@npmcli/package-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-4.0.0.tgz", - "integrity": "sha512-ZeXtZBQ/xjSUmrZj9R1Y2gsQRfkdhP5H31SCieJbAd8bHbn4YRglOoajcEZTJTM9m9BuEE7KiDcMPEoD/OgJkw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-4.0.1.tgz", + "integrity": "sha512-lRCEGdHZomFsURroh522YvA/2cVb9oPIJrjHanCJZkiasz1BzcnLr3tBJhlV7S86MBJBuAQ33is2D60YitZL2Q==", "inBundle": true, "dependencies": { "@npmcli/git": "^4.1.0", "glob": "^10.2.2", + "hosted-git-info": "^6.1.1", "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" + "proc-log": "^3.0.0", + "semver": "^7.5.3" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" diff --git a/package.json b/package.json index 5e3e7bc79b222..3308fd9166349 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@npmcli/config": "^6.2.1", "@npmcli/fs": "^3.1.0", "@npmcli/map-workspaces": "^3.0.4", - "@npmcli/package-json": "^4.0.0", + "@npmcli/package-json": "^4.0.1", "@npmcli/promise-spawn": "^6.0.2", "@npmcli/run-script": "^6.0.2", "abbrev": "^2.0.0",