From 1464adc74d7a8702b11b179f815d48eab10ec1cf Mon Sep 17 00:00:00 2001 From: Gar Date: Wed, 15 Jan 2025 09:25:40 -0800 Subject: [PATCH 1/9] chore: scope test fixture package names --- tap-snapshots/test/index.js.test.cjs | 125 +----------------- .../all-fields-populated/package.json | 4 +- test/index.js | 12 +- 3 files changed, 11 insertions(+), 130 deletions(-) diff --git a/tap-snapshots/test/index.js.test.cjs b/tap-snapshots/test/index.js.test.cjs index ca55392..36b0301 100644 --- a/tap-snapshots/test/index.js.test.cjs +++ b/tap-snapshots/test/index.js.test.cjs @@ -27,137 +27,18 @@ exports[`test/index.js TAP load create:true existing parseable package.json > pa ` exports[`test/index.js TAP load custom formatting > should save back custom format to package.json 1`] = ` -{"name":"foo","version":"1.0.1","description":"Lorem ipsum dolor"} +{"name":"@npmcli/test","version":"1.0.1","description":"Lorem ipsum dolor"} ` exports[`test/index.js TAP load read, update content and write > should properly save content to a package.json 1`] = ` { - "name": "foo", + "name": "@npmcli/test", "version": "1.0.1", "description": "Lorem ipsum dolor" } ` -exports[`test/index.js TAP load sorts on save > should properly save content to a package.json 1`] = ` -{ - "name": "foo", - "version": "1.0.0", - "description": "A sample package", - "keywords": [ - "sample", - "package" - ], - "homepage": "https://example.com", - "bugs": { - "url": "https://example.com/bugs", - "email": "bugs@example.com" - }, - "repository": { - "type": "git", - "url": "https://example.com/repo.git" - }, - "funding": "https://example.com/funding", - "license": "MIT", - "author": "Author Name ", - "maintainers": [ - "Maintainer One ", - "Maintainer Two " - ], - "contributors": [ - "Contributor One ", - "Contributor Two " - ], - "type": "module", - "imports": { - "#dep": "./src/dep.js" - }, - "exports": { - ".": "./src/index.js" - }, - "main": "index.js", - "browser": "browser.js", - "types": "index.d.ts", - "bin": { - "my-cli": "./bin/cli.js" - }, - "man": [ - "./man/doc.1" - ], - "directories": { - "lib": "lib", - "bin": "bin", - "man": "man" - }, - "files": [ - "lib/**/*.js", - "bin/**/*.js" - ], - "workspaces": [ - "packages/*" - ], - "scripts": { - "start": "node index.js", - "test": "tap test/*.js" - }, - "config": { - "port": "8080" - }, - "dependencies": { - "some-dependency": "^1.0.0" - }, - "devDependencies": { - "some-dev-dependency": "^1.0.0" - }, - "peerDependencies": { - "some-peer-dependency": "^1.0.0" - }, - "peerDependenciesMeta": { - "some-peer-dependency": { - "optional": true - } - }, - "optionalDependencies": { - "some-optional-dependency": "^1.0.0" - }, - "bundledDependencies": [ - "some-bundled-dependency" - ], - "bundleDependencies": [ - "some-bundled-dependency" - ], - "engines": { - "node": ">=14.0.0" - }, - "os": [ - "darwin", - "linux" - ], - "cpu": [ - "x64", - "arm64" - ], - "publishConfig": { - "registry": "https://registry.example.com" - }, - "devEngines": { - "node": ">=14.0.0" - }, - "licenses": [ - { - "type": "MIT", - "url": "https://opensource.org/licenses/MIT" - } - ], - "overrides": { - "some-dependency": { - "some-sub-dependency": "1.0.0" - } - } -} - -` - exports[`test/index.js TAP load update long package.json > should only update the defined property 1`] = ` { "version": "7.18.1", @@ -649,7 +530,7 @@ exports[`test/index.js TAP load update long package.json > should properly write exports[`test/index.js TAP read package > must match snapshot 1`] = ` Object { - "name": "foo", + "name": "@npmcli/test", "version": "1.0.0", } ` diff --git a/test/fixtures/all-fields-populated/package.json b/test/fixtures/all-fields-populated/package.json index 508db47..6b27829 100644 --- a/test/fixtures/all-fields-populated/package.json +++ b/test/fixtures/all-fields-populated/package.json @@ -1,5 +1,5 @@ { - "name": "foo", + "name": "@npmcli/test", "version": "1.0.0", "private": true, "description": "A sample package", @@ -93,4 +93,4 @@ "some-sub-dependency": "1.0.0" } } -} \ No newline at end of file +} diff --git a/test/index.js b/test/index.js index baf0564..00c8557 100644 --- a/test/index.js +++ b/test/index.js @@ -22,7 +22,7 @@ t.test('load', t => { t.test('read a valid package.json', async t => { const path = t.testdir({ 'package.json': JSON.stringify({ - name: 'foo', + name: '@npmcli/test', version: '1.0.0', }), }) @@ -30,14 +30,14 @@ t.test('load', t => { const pj = await PackageJson.load(path) t.same( pj.content, - { name: 'foo', version: '1.0.0' }, + { name: '@npmcli/test', version: '1.0.0' }, 'should return content for a valid package.json' ) }) t.test('read, update content and write', async t => { const path = t.testdir({ 'package.json': JSON.stringify({ - name: 'foo', + name: '@npmcli/test', version: '1.0.0', }, null, 8), }) @@ -65,7 +65,7 @@ t.test('load', t => { ) }) t.test('do not overwite unchanged file on EOF line added/removed', async t => { - const originalPackageJsonContent = '{\n "name": "foo"\n}' + const originalPackageJsonContent = '{\n "name": "@npmcli/test"\n}' const path = t.testdir({ 'package.json': originalPackageJsonContent, }) @@ -132,7 +132,7 @@ t.test('load', t => { t.test('custom formatting', async t => { const path = t.testdir({ 'package.json': JSON.stringify({ - name: 'foo', + name: '@npmcli/test', version: '1.0.0', }, null, 0), }) @@ -243,7 +243,7 @@ t.test('read package', async t => { const { readPackage } = require('../lib/read-package') const path = t.testdir({ 'package.json': JSON.stringify({ - name: 'foo', + name: '@npmcli/test', version: '1.0.0', }), }) From 2d320bc4e4b8609037b19d31420464dd3c67b9d5 Mon Sep 17 00:00:00 2001 From: Gar Date: Wed, 15 Jan 2025 09:30:23 -0800 Subject: [PATCH 2/9] fix: save when reverting content When a package.json is changed, saved, and then reverted back to what it was before, save was not running because this module was not updating its local copy of what the contents of the file was. This has been fixed. --- lib/index.js | 4 +++- test/index.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/index.js b/lib/index.js index 23f326d..828b899 100644 --- a/lib/index.js +++ b/lib/index.js @@ -252,7 +252,9 @@ class PackageJson { .replace(/\n/g, eol) if (fileContent.trim() !== this.#readFileContent.trim()) { - return await writeFile(this.filename, fileContent) + const written = await writeFile(this.filename, fileContent) + this.#readFileContent = fileContent + return written } } diff --git a/test/index.js b/test/index.js index 00c8557..d724286 100644 --- a/test/index.js +++ b/test/index.js @@ -327,3 +327,31 @@ t.test('empty props at bottom', async t => { JSON.parse(fs.readFileSync(resolve(path, 'package.json'), 'utf8')) ) }) + +t.test('reversion can still save', async t => { + const path = t.testdir({ + 'package.json': JSON.stringify({ + name: '@npmcli/test', + version: '1.0.0', + }), + }) + const pkgJson = await PackageJson.load(path) + pkgJson.update({ version: '2.0.0' }) + await pkgJson.save() + t.strictSame( + { + name: '@npmcli/test', + version: '2.0.0', + }, + JSON.parse(fs.readFileSync(resolve(path, 'package.json'), 'utf8')) + ) + pkgJson.update({ version: '1.0.0' }) + await pkgJson.save() + t.strictSame( + { + name: '@npmcli/test', + version: '1.0.0', + }, + JSON.parse(fs.readFileSync(resolve(path, 'package.json'), 'utf8')) + ) +}) From d722a1f3ea30fffbc0ddc1fe01e02599f240f381 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jan 2025 10:21:42 -0800 Subject: [PATCH 3/9] chore: bump @npmcli/template-oss from 4.23.5 to 4.23.6 (#137) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [@npmcli/template-oss](https://github.com/npm/template-oss) from 4.23.5 to 4.23.6.
Release notes

Sourced from @​npmcli/template-oss's releases.

v4.23.6

4.23.6 (2024-12-11)

Bug Fixes

Changelog

Sourced from @​npmcli/template-oss's changelog.

4.23.6 (2024-12-11)

Bug Fixes

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@npmcli/template-oss&package-manager=npm_and_yarn&previous-version=4.23.5&new-version=4.23.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: npm CLI robot --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 97070e2..8404e03 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^5.0.0", - "@npmcli/template-oss": "4.23.5", + "@npmcli/template-oss": "4.23.6", "read-package-json": "^7.0.0", "read-package-json-fast": "^4.0.0", "tap": "^16.0.1" @@ -49,7 +49,7 @@ }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.23.5", + "version": "4.23.6", "publish": "true" }, "tap": { From 35152b6a647f0c6ff1caa4342ad5cec43b7881cd Mon Sep 17 00:00:00 2001 From: Gar Date: Thu, 9 Jan 2025 08:55:43 -0800 Subject: [PATCH 4/9] deps: remove normalize-package-data --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 8404e03..b914f5e 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "glob": "^10.2.2", "hosted-git-info": "^8.0.0", "json-parse-even-better-errors": "^4.0.0", - "normalize-package-data": "^7.0.0", "proc-log": "^5.0.0", "semver": "^7.5.3" }, From 6ea3a9d336b42b613474b04dfe4eb44b65d95612 Mon Sep 17 00:00:00 2001 From: Gar Date: Thu, 9 Jan 2025 10:54:32 -0800 Subject: [PATCH 5/9] deps: add validate-npm-package-license@3.0.4 --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index b914f5e..cfc8d2d 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "hosted-git-info": "^8.0.0", "json-parse-even-better-errors": "^4.0.0", "proc-log": "^5.0.0", - "semver": "^7.5.3" + "semver": "^7.5.3", + "validate-npm-package-license": "^3.0.4" }, "devDependencies": { "@npmcli/eslint-config": "^5.0.0", From 2a7bbe5bc797f1bdba7c183b417e1a2bbfc33973 Mon Sep 17 00:00:00 2001 From: Gar Date: Tue, 14 Jan 2025 15:37:02 -0800 Subject: [PATCH 6/9] fix: inline normalize-package-data logic This was accomplished by pulling in the code as-is, then adding full test coverage. Only then was dead and unused code pruned. After that some refactoring was done to further clean up the code. --- lib/normalize-data.js | 257 +++++++++++ lib/normalize.js | 27 +- tap-snapshots/test/normalize-data.js.test.cjs | 235 ++++++++++ .../{legacy => normalize-data}/package.json | 0 test/index.js | 4 +- test/normalize-data.js | 427 ++++++++++++++++++ 6 files changed, 924 insertions(+), 26 deletions(-) create mode 100644 lib/normalize-data.js create mode 100644 tap-snapshots/test/normalize-data.js.test.cjs rename test/fixtures/{legacy => normalize-data}/package.json (100%) create mode 100644 test/normalize-data.js diff --git a/lib/normalize-data.js b/lib/normalize-data.js new file mode 100644 index 0000000..79b0baf --- /dev/null +++ b/lib/normalize-data.js @@ -0,0 +1,257 @@ +// Originally normalize-package-data + +const url = require('node:url') +const hostedGitInfo = require('hosted-git-info') +const validateLicense = require('validate-npm-package-license') + +const typos = { + dependancies: 'dependencies', + dependecies: 'dependencies', + depdenencies: 'dependencies', + devEependencies: 'devDependencies', + depends: 'dependencies', + 'dev-dependencies': 'devDependencies', + devDependences: 'devDependencies', + devDepenencies: 'devDependencies', + devdependencies: 'devDependencies', + repostitory: 'repository', + repo: 'repository', + prefereGlobal: 'preferGlobal', + hompage: 'homepage', + hampage: 'homepage', + autohr: 'author', + autor: 'author', + contributers: 'contributors', + publicationConfig: 'publishConfig', + script: 'scripts', +} + +const isEmail = str => str.includes('@') && (str.indexOf('@') < str.lastIndexOf('.')) + +// Extracts description from contents of a readme file in markdown format +function extractDescription (description) { + // the first block of text before the first heading that isn't the first line heading + const lines = description.trim().split('\n') + let start = 0 + // skip initial empty lines and lines that start with # + while (lines[start]?.trim().match(/^(#|$)/)) { + start++ + } + let end = start + 1 + // keep going till we get to the end or an empty line + while (end < lines.length && lines[end].trim()) { + end++ + } + return lines.slice(start, end).join(' ').trim() +} + +function stringifyPerson (person) { + if (typeof person !== 'string') { + const name = person.name || '' + const u = person.url || person.web + const wrappedUrl = u ? (' (' + u + ')') : '' + const e = person.email || person.mail + const wrappedEmail = e ? (' <' + e + '>') : '' + person = name + wrappedEmail + wrappedUrl + } + const matchedName = person.match(/^([^(<]+)/) + const matchedUrl = person.match(/\(([^()]+)\)/) + const matchedEmail = person.match(/<([^<>]+)>/) + const parsed = {} + if (matchedName?.[0].trim()) { + parsed.name = matchedName[0].trim() + } + if (matchedEmail) { + parsed.email = matchedEmail[1] + } + if (matchedUrl) { + parsed.url = matchedUrl[1] + } + return parsed +} + +function normalizeData (data, changes) { + // fixDescriptionField + if (data.description && typeof data.description !== 'string') { + changes?.push(`'description' field should be a string`) + delete data.description + } + if (data.readme && !data.description && data.readme !== 'ERROR: No README data found!') { + data.description = extractDescription(data.readme) + } + if (data.description === undefined) { + delete data.description + } + if (!data.description) { + changes?.push('No description') + } + + // fixModulesField + if (data.modules) { + changes?.push(`modules field is deprecated`) + delete data.modules + } + + // fixFilesField + const files = data.files + if (files && !Array.isArray(files)) { + changes?.push(`Invalid 'files' member`) + delete data.files + } else if (data.files) { + data.files = data.files.filter(function (file) { + if (!file || typeof file !== 'string') { + changes?.push(`Invalid filename in 'files' list: ${file}`) + return false + } else { + return true + } + }) + } + + // fixManField + if (data.man && typeof data.man === 'string') { + data.man = [data.man] + } + + // fixBugsField + if (!data.bugs && data.repository?.url) { + const hosted = hostedGitInfo.fromUrl(data.repository.url) + if (hosted && hosted.bugs()) { + data.bugs = { url: hosted.bugs() } + } + } else if (data.bugs) { + if (typeof data.bugs === 'string') { + if (isEmail(data.bugs)) { + data.bugs = { email: data.bugs } + /* eslint-disable-next-line node/no-deprecated-api */ + } else if (url.parse(data.bugs).protocol) { + data.bugs = { url: data.bugs } + } else { + changes?.push(`Bug string field must be url, email, or {email,url}`) + } + } else { + for (const k in data.bugs) { + if (['web', 'name'].includes(k)) { + changes?.push(`bugs['${k}'] should probably be bugs['url'].`) + data.bugs.url = data.bugs[k] + delete data.bugs[k] + } + } + const oldBugs = data.bugs + data.bugs = {} + if (oldBugs.url) { + /* eslint-disable-next-line node/no-deprecated-api */ + if (typeof (oldBugs.url) === 'string' && url.parse(oldBugs.url).protocol) { + data.bugs.url = oldBugs.url + } else { + changes?.push('bugs.url field must be a string url. Deleted.') + } + } + if (oldBugs.email) { + if (typeof (oldBugs.email) === 'string' && isEmail(oldBugs.email)) { + data.bugs.email = oldBugs.email + } else { + changes?.push('bugs.email field must be a string email. Deleted.') + } + } + } + if (!data.bugs.email && !data.bugs.url) { + delete data.bugs + changes?.push('Normalized value of bugs field is an empty object. Deleted.') + } + } + // fixKeywordsField + if (typeof data.keywords === 'string') { + data.keywords = data.keywords.split(/,\s+/) + } + if (data.keywords && !Array.isArray(data.keywords)) { + delete data.keywords + changes?.push(`keywords should be an array of strings`) + } else if (data.keywords) { + data.keywords = data.keywords.filter(function (kw) { + if (typeof kw !== 'string' || !kw) { + changes?.push(`keywords should be an array of strings`) + return false + } else { + return true + } + }) + } + // fixBundleDependenciesField + const bdd = 'bundledDependencies' + const bd = 'bundleDependencies' + if (data[bdd] && !data[bd]) { + data[bd] = data[bdd] + delete data[bdd] + } + if (data[bd] && !Array.isArray(data[bd])) { + changes?.push(`Invalid 'bundleDependencies' list. Must be array of package names`) + delete data[bd] + } else if (data[bd]) { + data[bd] = data[bd].filter(function (filtered) { + if (!filtered || typeof filtered !== 'string') { + changes?.push(`Invalid bundleDependencies member: ${filtered}`) + return false + } else { + if (!data.dependencies) { + data.dependencies = {} + } + if (!Object.prototype.hasOwnProperty.call(data.dependencies, filtered)) { + changes?.push(`Non-dependency in bundleDependencies: ${filtered}`) + data.dependencies[filtered] = '*' + } + return true + } + }) + } + // fixHomepageField + if (!data.homepage && data.repository && data.repository.url) { + const hosted = hostedGitInfo.fromUrl(data.repository.url) + if (hosted) { + data.homepage = hosted.docs() + } + } + if (data.homepage) { + if (typeof data.homepage !== 'string') { + changes?.push('homepage field must be a string url. Deleted.') + delete data.homepage + } else { + /* eslint-disable-next-line node/no-deprecated-api */ + if (!url.parse(data.homepage).protocol) { + data.homepage = 'http://' + data.homepage + } + } + } + // fixReadmeField + if (!data.readme) { + changes?.push('No README data') + data.readme = 'ERROR: No README data found!' + } + // fixLicenseField + const license = data.license || data.licence + if (!license) { + changes?.push('No license field.') + } else if (typeof (license) !== 'string' || license.length < 1 || license.trim() === '') { + changes?.push('license should be a valid SPDX license expression') + } else if (!validateLicense(license).validForNewPackages) { + changes?.push('license should be a valid SPDX license expression') + } + // fixPeople + if (data.author) { + data.author = stringifyPerson(data.author) + } + ['maintainers', 'contributors'].forEach(function (set) { + if (!Array.isArray(data[set])) { + return + } + data[set] = data[set].map(stringifyPerson) + }) + // fixTypos + for (const d in typos) { + if (Object.prototype.hasOwnProperty.call(data, d)) { + changes?.push(`${d} should probably be ${typos[d]}.`) + } + } +} + +module.exports = { normalizeData } diff --git a/lib/normalize.js b/lib/normalize.js index 3adec01..c6cccf4 100644 --- a/lib/normalize.js +++ b/lib/normalize.js @@ -348,7 +348,6 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase }) changes?.push(`"readmeFilename" was set to ${readmeFile}`) } if (!data.readme) { - // this.warn('missingReadme') data.readme = 'ERROR: No README data found!' } } @@ -572,30 +571,10 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase }) } } + // 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 legacyFixer = require('normalize-package-data/lib/fixer.js') - const legacyMakeWarning = require('normalize-package-data/lib/make_warning.js') - legacyFixer.warn = function () { - changes?.push(legacyMakeWarning.apply(null, arguments)) - } - - const legacySteps = [ - 'fixDescriptionField', - 'fixModulesField', - 'fixFilesField', - 'fixManField', - 'fixBugsField', - 'fixKeywordsField', - 'fixBundleDependenciesField', - 'fixHomepageField', - 'fixReadmeField', - 'fixLicenseField', - 'fixPeople', - 'fixTypos', - ] - for (const legacyStep of legacySteps) { - legacyFixer[legacyStep](data) - } + const { normalizeData } = require('./normalize-data.js') + normalizeData(data, changes) } // Warn if the bin references don't point to anything. This might be better diff --git a/tap-snapshots/test/normalize-data.js.test.cjs b/tap-snapshots/test/normalize-data.js.test.cjs new file mode 100644 index 0000000..541348e --- /dev/null +++ b/tap-snapshots/test/normalize-data.js.test.cjs @@ -0,0 +1,235 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/normalize-data.js TAP fixBugsField bugsTypos > must match snapshot 1`] = ` +Array [ + "bugs['web'] should probably be bugs['url'].", +] +` + +exports[`test/normalize-data.js TAP fixBugsField no bugs with repository with url > must match snapshot 1`] = ` +Array [] +` + +exports[`test/normalize-data.js TAP fixBugsField non string > must match snapshot 1`] = ` +Array [ + "Normalized value of bugs field is an empty object. Deleted.", +] +` + +exports[`test/normalize-data.js TAP fixBugsField object email > must match snapshot 1`] = ` +Array [] +` + +exports[`test/normalize-data.js TAP fixBugsField object invalid url other > must match snapshot 1`] = ` +Array [ + "bugs.url field must be a string url. Deleted.", + "Normalized value of bugs field is an empty object. Deleted.", +] +` + +exports[`test/normalize-data.js TAP fixBugsField object invalid url string > must match snapshot 1`] = ` +Array [ + "bugs.url field must be a string url. Deleted.", + "Normalized value of bugs field is an empty object. Deleted.", +] +` + +exports[`test/normalize-data.js TAP fixBugsField object non email > must match snapshot 1`] = ` +Array [ + "bugs.email field must be a string email. Deleted.", + "Normalized value of bugs field is an empty object. Deleted.", +] +` + +exports[`test/normalize-data.js TAP fixBugsField object non-string email > must match snapshot 1`] = ` +Array [ + "bugs.email field must be a string email. Deleted.", + "Normalized value of bugs field is an empty object. Deleted.", +] +` + +exports[`test/normalize-data.js TAP fixBugsField object valid url > must match snapshot 1`] = ` +Array [] +` + +exports[`test/normalize-data.js TAP fixBugsField repository w/ no bugs template > must match snapshot 1`] = ` +Array [] +` + +exports[`test/normalize-data.js TAP fixBugsField string email > must match snapshot 1`] = ` +Array [] +` + +exports[`test/normalize-data.js TAP fixBugsField string other > must match snapshot 1`] = ` +Array [ + "Bug string field must be url, email, or {email,url}", + "Normalized value of bugs field is an empty object. Deleted.", +] +` + +exports[`test/normalize-data.js TAP fixBugsField string url > must match snapshot 1`] = ` +Array [] +` + +exports[`test/normalize-data.js TAP fixBundleDependenciesField bundledDependencies > must match snapshot 1`] = ` +Array [] +` + +exports[`test/normalize-data.js TAP fixBundleDependenciesField filters non strings > must match snapshot 1`] = ` +Array [ + "Invalid bundleDependencies member: 100", +] +` + +exports[`test/normalize-data.js TAP fixBundleDependenciesField non array > must match snapshot 1`] = ` +Array [ + "Invalid 'bundleDependencies' list. Must be array of package names", +] +` + +exports[`test/normalize-data.js TAP fixBundleDependenciesField non-dependency > must match snapshot 1`] = ` +Array [ + "Non-dependency in bundleDependencies: @npm/test", +] +` + +exports[`test/normalize-data.js TAP fixDescriptionField no description and no readme > must match snapshot 1`] = ` +Array [ + "No description", + "No README data", +] +` + +exports[`test/normalize-data.js TAP fixDescriptionField non string > must match snapshot 1`] = ` +Array [ + "'description' field should be a string", +] +` + +exports[`test/normalize-data.js TAP fixDescriptionField summarizes readme > must match snapshot 1`] = ` +Array [] +` + +exports[`test/normalize-data.js TAP fixFilesField invalid entry > must match snapshot 1`] = ` +Array [ + "Invalid filename in 'files' list: null", + "Invalid filename in 'files' list: true", +] +` + +exports[`test/normalize-data.js TAP fixFilesField non array > must match snapshot 1`] = ` +Array [ + "Invalid 'files' member", +] +` + +exports[`test/normalize-data.js TAP fixHomepageField no homepage with repository with url > must match snapshot 1`] = ` +Array [] +` + +exports[`test/normalize-data.js TAP fixHomepageField no protocol > must match snapshot 1`] = ` +Array [] +` + +exports[`test/normalize-data.js TAP fixHomepageField non hosted repository > must match snapshot 1`] = ` +Array [] +` + +exports[`test/normalize-data.js TAP fixHomepageField non string > must match snapshot 1`] = ` +Array [ + "homepage field must be a string url. Deleted.", +] +` + +exports[`test/normalize-data.js TAP fixHomepageField repository w/ no docs template > must match snapshot 1`] = ` +Array [] +` + +exports[`test/normalize-data.js TAP fixKeywordsField filters non strings > must match snapshot 1`] = ` +Array [ + "keywords should be an array of strings", +] +` + +exports[`test/normalize-data.js TAP fixKeywordsField non array > must match snapshot 1`] = ` +Array [ + "keywords should be an array of strings", +] +` + +exports[`test/normalize-data.js TAP fixKeywordsField splits string > must match snapshot 1`] = ` +Array [] +` + +exports[`test/normalize-data.js TAP fixLicenseField invalid > must match snapshot 1`] = ` +Array [ + "license should be a valid SPDX license expression", +] +` + +exports[`test/normalize-data.js TAP fixLicenseField missing > must match snapshot 1`] = ` +Array [ + "No license field.", +] +` + +exports[`test/normalize-data.js TAP fixLicenseField non string > must match snapshot 1`] = ` +Array [ + "license should be a valid SPDX license expression", +] +` + +exports[`test/normalize-data.js TAP fixManfield string > must match snapshot 1`] = ` +Array [] +` + +exports[`test/normalize-data.js TAP fixModulesField > must match snapshot 1`] = ` +Array [ + "modules field is deprecated", +] +` + +exports[`test/normalize-data.js TAP fixPeople author name url and email > must match snapshot 1`] = ` +Array [] +` + +exports[`test/normalize-data.js TAP fixPeople author no name > must match snapshot 1`] = ` +Array [] +` + +exports[`test/normalize-data.js TAP fixPeople author only name > must match snapshot 1`] = ` +Array [] +` + +exports[`test/normalize-data.js TAP fixPeople author string > must match snapshot 1`] = ` +Array [] +` + +exports[`test/normalize-data.js TAP fixPeople author web and mail > must match snapshot 1`] = ` +Array [] +` + +exports[`test/normalize-data.js TAP fixPeople contributors > must match snapshot 1`] = ` +Array [] +` + +exports[`test/normalize-data.js TAP fixPeople maintainers > must match snapshot 1`] = ` +Array [] +` + +exports[`test/normalize-data.js TAP fixReadmeField no readme > must match snapshot 1`] = ` +Array [ + "No README data", +] +` + +exports[`test/normalize-data.js TAP fixTypos top level > must match snapshot 1`] = ` +Array [ + "script should probably be scripts.", +] +` diff --git a/test/fixtures/legacy/package.json b/test/fixtures/normalize-data/package.json similarity index 100% rename from test/fixtures/legacy/package.json rename to test/fixtures/normalize-data/package.json diff --git a/test/index.js b/test/index.js index d724286..20b7fcb 100644 --- a/test/index.js +++ b/test/index.js @@ -1,7 +1,7 @@ const fs = require('node:fs') const { join, resolve } = require('node:path') const t = require('tap') -const PackageJson = require('../lib/index.js') +const PackageJson = require('../') const getPackageFile = (file) => JSON.parse( @@ -80,7 +80,7 @@ t.test('load', t => { ) }) t.test('update long package.json', async t => { - const fixture = resolve(__dirname, 'fixtures', 'legacy', 'package.json') + const fixture = resolve(__dirname, 'fixtures', 'normalize-data', 'package.json') const path = t.testdir({}) fs.copyFileSync(fixture, resolve(path, 'package.json')) const pkgJson = await PackageJson.load(path) diff --git a/test/normalize-data.js b/test/normalize-data.js new file mode 100644 index 0000000..a9e9505 --- /dev/null +++ b/test/normalize-data.js @@ -0,0 +1,427 @@ +const t = require('tap') +const PackageJson = require('../') + +const base = { + name: '@npmcli/test', + description: 'test fixture', + version: '0.0.0', + readme: 'test fixture package', + license: 'UNLICENSED', +} + +function normalizeData (t, data) { + const changes = [] + const p = new PackageJson().fromContent(data) + p.normalize({ steps: 'normalizeData', changes }) + t.matchSnapshot(changes) + return p +} + +t.test('fixDescriptionField', async t => { + t.test('non string', async t => { + const { content } = normalizeData(t, { + ...base, + description: true, + }) + t.equal(content.description, base.readme) + }) + + t.test('no description and no readme', async t => { + const { content } = normalizeData(t, { + ...base, + description: undefined, + readme: undefined, + }) + t.equal(content.description, undefined) + }) + + t.test('summarizes readme', async t => { + const { content } = normalizeData(t, { + ...base, + description: undefined, + readme: '# test package\ntest fixture readme\nsecond line\n\nthird line', + }) + t.equal(content.description, 'test fixture readme second line') + }) +}) + +t.test('fixModulesField', async t => { + const { content } = normalizeData(t, { + ...base, + modules: true, + }) + t.equal(content.modules, undefined) +}) + +t.test('fixFilesField', async t => { + t.test('non array', async t => { + const { content } = normalizeData(t, { + ...base, + files: './index.js', + }) + t.equal(content.files, undefined) + }) + + t.test('invalid entry', async t => { + const { content } = normalizeData(t, { + ...base, + files: [null, true, './index.js'], + }) + t.same(content.files, ['./index.js']) + }) +}) + +t.test('fixManfield', async t => { + t.test('string', async t => { + const { content } = normalizeData(t, { + ...base, + man: './man', + }) + t.same(content.man, ['./man']) + }) +}) + +t.test('fixBugsField', async t => { + t.test('no bugs with repository with url', async t => { + const { content } = normalizeData(t, { + ...base, + repository: { + url: 'git+https://github.com/npm/package-json.git', + }, + }) + t.same(content.bugs, { url: 'https://github.com/npm/package-json/issues' }) + }) + + t.test('non string', async t => { + const { content } = normalizeData(t, { + ...base, + bugs: {}, + }) + t.same(content.bugs, undefined) + }) + + t.test('string email', async t => { + const { content } = normalizeData(t, { + ...base, + bugs: 'support@npmjs.org', + }) + t.same(content.bugs, { email: 'support@npmjs.org' }) + }) + + t.test('string url', async t => { + const { content } = normalizeData(t, { + ...base, + bugs: 'https://npmjs.org', + }) + t.same(content.bugs, { url: 'https://npmjs.org' }) + }) + + t.test('string other', async t => { + const { content } = normalizeData(t, { + ...base, + bugs: 'something else', + }) + t.equal(content.bugs, undefined) + }) + + t.test('bugsTypos', async t => { + const { content } = normalizeData(t, { + ...base, + bugs: { web: 'https://npmjs.org' }, + }) + t.same(content.bugs, { url: 'https://npmjs.org' }) + }) + + t.test('object valid url', async t => { + const { content } = normalizeData(t, { + ...base, + bugs: { url: 'https://npmjs.org' }, + }) + t.same(content.bugs, { url: 'https://npmjs.org' }) + }) + + t.test('object invalid url string', async t => { + const { content } = normalizeData(t, { + ...base, + bugs: { url: 'homepage' }, + }) + t.equal(content.bugs, undefined) + }) + + t.test('object invalid url other', async t => { + const { content } = normalizeData(t, { + ...base, + bugs: { url: {} }, + }) + t.equal(content.bugs, undefined) + }) + + t.test('object email', async t => { + const { content } = normalizeData(t, { + ...base, + bugs: { email: 'support@npmjs.org' }, + }) + t.same(content.bugs, { email: 'support@npmjs.org' }) + }) + + t.test('object non email', async t => { + const { content } = normalizeData(t, { + ...base, + bugs: { email: 'support' }, + }) + t.equal(content.bugs, undefined) + }) + + t.test('object non-string email', async t => { + const { content } = normalizeData(t, { + ...base, + bugs: { email: {} }, + }) + t.equal(content.bugs, undefined) + }) + + t.test('repository w/ no bugs template', async t => { + const { content } = normalizeData(t, { + ...base, + repository: { url: 'https://git.sr.ht/example/repo.git' }, + }) + t.equal(content.bugs, undefined) + }) +}) + +t.test('fixKeywordsField', async t => { + t.test('splits string', async t => { + const { content } = normalizeData(t, { + ...base, + keywords: 'a, b, c', + }) + t.same(content.keywords, ['a', 'b', 'c']) + }) + + t.test('non array', async t => { + const { content } = normalizeData(t, { + ...base, + keywords: {}, + }) + t.equal(content.keywords, undefined) + }) + + t.test('filters non strings', async t => { + const { content } = normalizeData(t, { + ...base, + keywords: ['a', 100, 'c'], + }) + t.same(content.keywords, ['a', 'c']) + }) +}) + +t.test('fixBundleDependenciesField', async t => { + t.test('bundledDependencies', async t => { + const { content } = normalizeData(t, { + ...base, + dependencies: { '@npm/test': '*' }, + bundledDependencies: ['@npm/test'], + }) + t.equal(content.bundledDependencies, undefined) + t.same(content.bundleDependencies, ['@npm/test']) + }) + + t.test('non array', async t => { + const { content } = normalizeData(t, { + ...base, + bundleDependencies: '@npm/test', + }) + t.equal(content.bundleDependencies, undefined) + }) + + t.test('filters non strings', async t => { + const { content } = normalizeData(t, { + ...base, + dependencies: { '@npm/test': '*' }, + bundleDependencies: ['@npm/test', 100], + }) + t.same(content.bundleDependencies, ['@npm/test']) + }) + + t.test('non-dependency', async t => { + const { content } = normalizeData(t, { + ...base, + bundleDependencies: ['@npm/test'], + }) + t.same(content.bundleDependencies, ['@npm/test']) + }) +}) + +t.test('fixHomepageField', async t => { + t.test('no homepage with repository with url', async t => { + const { content } = normalizeData(t, { + ...base, + repository: { + url: 'git+https://github.com/npm/package-json.git', + }, + }) + t.equal(content.homepage, 'https://github.com/npm/package-json#readme') + }) + + t.test('non string', async t => { + const { content } = normalizeData(t, { + ...base, + homepage: true, + }) + t.equal(content.homepage, undefined) + }) + + t.test('no protocol', async t => { + const { content } = normalizeData(t, { + ...base, + homepage: 'npmjs.org', + }) + t.equal(content.homepage, 'http://npmjs.org') + }) + + t.test('repository w/ no docs template', async t => { + const { content } = normalizeData(t, { + ...base, + repository: { url: 'https://git.sr.ht/example/repo.git' }, + }) + t.equal(content.docs, undefined) + }) + + t.test('non hosted repository', async t => { + const { content } = normalizeData(t, { + ...base, + repository: { url: 'https://npmjs.org' }, + }) + t.equal(content.docs, undefined) + }) +}) + +t.test('fixReadmeField', async t => { + t.test('no readme', async t => { + const { content } = normalizeData(t, { + ...base, + readme: undefined, + }) + t.equal(content.readme, 'ERROR: No README data found!') + }) +}) + +t.test('fixLicenseField', async t => { + t.test('missing', async t => { + const { content } = normalizeData(t, { + ...base, + license: undefined, + }) + t.equal(content.license, undefined) + }) + + t.test('non string', async t => { + const { content } = normalizeData(t, { + ...base, + license: 100, + }) + t.equal(content.license, 100) + }) + + t.test('invalid', async t => { + const { content } = normalizeData(t, { + ...base, + license: 'BESPOKE LICENSE', + }) + t.equal(content.license, 'BESPOKE LICENSE') + }) +}) + +t.test('fixPeople', async t => { + t.test('author', async t => { + t.test('string', async t => { + const { content } = normalizeData(t, { + ...base, + author: 'npm', + }) + t.same(content.author, { name: 'npm' }) + }) + + t.test('no name', async t => { + const { content } = normalizeData(t, { + ...base, + author: { + url: 'https://npmjs.org', + }, + }) + t.same(content.author, { + url: 'https://npmjs.org', + }) + }) + + t.test('name url and email', async t => { + const { content } = normalizeData(t, { + ...base, + author: { + name: 'npm', + url: 'https://npmjs.org', + email: 'support@npmjs.org', + }, + }) + t.same(content.author, { + name: 'npm', + url: 'https://npmjs.org', + email: 'support@npmjs.org', + }) + }) + + t.test('web and mail', async t => { + const { content } = normalizeData(t, { + ...base, + author: { + name: 'npm', + web: 'https://npmjs.org', + mail: 'support@npmjs.org', + }, + }) + t.same(content.author, { + name: 'npm', + url: 'https://npmjs.org', + email: 'support@npmjs.org', + }) + }) + + t.test('only name', async t => { + const { content } = normalizeData(t, { + ...base, + author: { + name: 'npm', + }, + }) + t.same(content.author, { name: 'npm' }) + }) + }) + + t.test('maintainers', async t => { + const { content } = normalizeData(t, { + ...base, + maintainers: ['npm'], + }) + t.same(content.maintainers, [{ name: 'npm' }]) + }) + + t.test('contributors', async t => { + const { content } = normalizeData(t, { + ...base, + contributors: ['npm'], + }) + t.same(content.contributors, [{ name: 'npm' }]) + }) +}) + +t.test('fixTypos', async t => { + t.test('top level', async t => { + const { content } = normalizeData(t, { + ...base, + script: { + lint: 'npm run lint', + }, + }) + t.same(content.script, { lint: 'npm run lint' }) + }) +}) From 0930f4e5c8b02e6d8935f37ca7f46b492c3d1ef7 Mon Sep 17 00:00:00 2001 From: Gar Date: Wed, 15 Jan 2025 07:25:11 -0800 Subject: [PATCH 7/9] chore: @npmcli/eslint-config@5.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cfc8d2d..fe047f8 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "validate-npm-package-license": "^3.0.4" }, "devDependencies": { - "@npmcli/eslint-config": "^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", From 526473bf1f2fcb8b1b3c3af68f890df203ebe33d Mon Sep 17 00:00:00 2001 From: Gar Date: Wed, 15 Jan 2025 07:25:45 -0800 Subject: [PATCH 8/9] fix: remove max-len linting bypasses The newest @npmcli/eslint-config allows these to be long --- lib/normalize.js | 1 - test/prepare.js | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/normalize.js b/lib/normalize.js index c6cccf4..7115390 100644 --- a/lib/normalize.js +++ b/lib/normalize.js @@ -487,7 +487,6 @@ 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')) { 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] } diff --git a/test/prepare.js b/test/prepare.js index ec65320..794891b 100644 --- a/test/prepare.js +++ b/test/prepare.js @@ -617,7 +617,6 @@ for (const [name, testPrepare] of Object.entries(testMethods)) { t.has(content, { type: undefined }) }) - // eslint-disable-next-line max-len // https://nodejs.org/api/esm.html#esm_writing_dual_packages_while_avoiding_or_minimizing_hazards t.skip('handles esm modules', async t => { From b6465f44c727d6513db6898c7cbe41dd355cebe8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 21 Jan 2025 09:22:45 -0800 Subject: [PATCH 9/9] chore: release 6.1.1 (#141) :robot: I have created a release *beep* *boop* --- ## [6.1.1](https://github.com/npm/package-json/compare/v6.1.0...v6.1.1) (2025-01-21) ### Bug Fixes * [`526473b`](https://github.com/npm/package-json/commit/526473bf1f2fcb8b1b3c3af68f890df203ebe33d) [#139](https://github.com/npm/package-json/pull/139) remove max-len linting bypasses (@wraithgar) * [`2a7bbe5`](https://github.com/npm/package-json/commit/2a7bbe5bc797f1bdba7c183b417e1a2bbfc33973) [#139](https://github.com/npm/package-json/pull/139) inline normalize-package-data logic (@wraithgar) * [`2d320bc`](https://github.com/npm/package-json/commit/2d320bc4e4b8609037b19d31420464dd3c67b9d5) [#140](https://github.com/npm/package-json/pull/140) save when reverting content (@wraithgar) ### Dependencies * [`6ea3a9d`](https://github.com/npm/package-json/commit/6ea3a9d336b42b613474b04dfe4eb44b65d95612) [#139](https://github.com/npm/package-json/pull/139) add `validate-npm-package-license@3.0.4` * [`35152b6`](https://github.com/npm/package-json/commit/35152b6a647f0c6ff1caa4342ad5cec43b7881cd) [#139](https://github.com/npm/package-json/pull/139) remove normalize-package-data ### Chores * [`0930f4e`](https://github.com/npm/package-json/commit/0930f4e5c8b02e6d8935f37ca7f46b492c3d1ef7) [#139](https://github.com/npm/package-json/pull/139) `@npmcli/eslint-config@5.1.0` (@wraithgar) * [`1464adc`](https://github.com/npm/package-json/commit/1464adc74d7a8702b11b179f815d48eab10ec1cf) [#140](https://github.com/npm/package-json/pull/140) scope test fixture package names (@wraithgar) * [`d722a1f`](https://github.com/npm/package-json/commit/d722a1f3ea30fffbc0ddc1fe01e02599f240f381) [#137](https://github.com/npm/package-json/pull/137) bump @npmcli/template-oss from 4.23.5 to 4.23.6 (#137) (@dependabot[bot], @npm-cli-bot) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 13 +++++++++++++ package.json | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8ace915..0f6aa44 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "6.1.0" + ".": "6.1.1" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 679fa90..d054241 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [6.1.1](https://github.com/npm/package-json/compare/v6.1.0...v6.1.1) (2025-01-21) +### Bug Fixes +* [`526473b`](https://github.com/npm/package-json/commit/526473bf1f2fcb8b1b3c3af68f890df203ebe33d) [#139](https://github.com/npm/package-json/pull/139) remove max-len linting bypasses (@wraithgar) +* [`2a7bbe5`](https://github.com/npm/package-json/commit/2a7bbe5bc797f1bdba7c183b417e1a2bbfc33973) [#139](https://github.com/npm/package-json/pull/139) inline normalize-package-data logic (@wraithgar) +* [`2d320bc`](https://github.com/npm/package-json/commit/2d320bc4e4b8609037b19d31420464dd3c67b9d5) [#140](https://github.com/npm/package-json/pull/140) save when reverting content (@wraithgar) +### Dependencies +* [`6ea3a9d`](https://github.com/npm/package-json/commit/6ea3a9d336b42b613474b04dfe4eb44b65d95612) [#139](https://github.com/npm/package-json/pull/139) add `validate-npm-package-license@3.0.4` +* [`35152b6`](https://github.com/npm/package-json/commit/35152b6a647f0c6ff1caa4342ad5cec43b7881cd) [#139](https://github.com/npm/package-json/pull/139) remove normalize-package-data +### Chores +* [`0930f4e`](https://github.com/npm/package-json/commit/0930f4e5c8b02e6d8935f37ca7f46b492c3d1ef7) [#139](https://github.com/npm/package-json/pull/139) `@npmcli/eslint-config@5.1.0` (@wraithgar) +* [`1464adc`](https://github.com/npm/package-json/commit/1464adc74d7a8702b11b179f815d48eab10ec1cf) [#140](https://github.com/npm/package-json/pull/140) scope test fixture package names (@wraithgar) +* [`d722a1f`](https://github.com/npm/package-json/commit/d722a1f3ea30fffbc0ddc1fe01e02599f240f381) [#137](https://github.com/npm/package-json/pull/137) bump @npmcli/template-oss from 4.23.5 to 4.23.6 (#137) (@dependabot[bot], @npm-cli-bot) + ## [6.1.0](https://github.com/npm/package-json/compare/v6.0.1...v6.1.0) (2024-11-27) ### Features * [`4c22738`](https://github.com/npm/package-json/commit/4c22738d919e29a32ae20472f48837b65181c309) [#133](https://github.com/npm/package-json/pull/133) adds ability to sort package.json on save (#133) (@reggi, @wraithgar) diff --git a/package.json b/package.json index fe047f8..5421878 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/package-json", - "version": "6.1.0", + "version": "6.1.1", "description": "Programmatic API to update package.json", "keywords": [ "npm",