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..7115390 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!' } } @@ -488,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] } @@ -572,30 +570,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/package.json b/package.json index 8404e03..fe047f8 100644 --- a/package.json +++ b/package.json @@ -33,12 +33,12 @@ "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" + "semver": "^7.5.3", + "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", 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' }) + }) +}) 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 => {