Skip to content

Commit 09d8573

Browse files
committed
fix: pull in fix logic from normalize-package-data
This brings in the logic from `normalize-package-data` to be in this repo instead. It doesn't bring all of the logic, just the steps involved with "fix". There was some re-duplication of `bundleDependencies` that `normalize-package-data` was doing that has been removed. It was also completely re-implementing the script fixing, which we already call as part of "fix" so that was dropped.
1 parent ee84e3a commit 09d8573

7 files changed

Lines changed: 520 additions & 62 deletions

File tree

lib/index.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,7 @@ class PackageJson {
4242
'fixNameField',
4343
'fixVersionField',
4444
'fixRepositoryField',
45-
'fixBinField',
4645
'fixDependencies',
47-
'fixScriptsField',
4846
'devDependencies',
4947
'scriptpath',
5048
])

lib/normalize.js

Lines changed: 144 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const semver = require('semver')
12
const fs = require('fs/promises')
23
const { glob } = require('glob')
34
const normalizePackageBin = require('npm-normalize-package-bin')
@@ -6,6 +7,27 @@ const legacyMakeWarning = require('normalize-package-data/lib/make_warning.js')
67
const path = require('path')
78
const log = require('proc-log')
89
const git = require('@npmcli/git')
10+
const hostedGitInfo = require('hosted-git-info')
11+
12+
function isCorrectlyEncodedName (spec) {
13+
return !spec.match(/[/@\s+%:]/) &&
14+
spec === encodeURIComponent(spec)
15+
}
16+
17+
function isValidScopedPackageName (spec) {
18+
if (spec.charAt(0) !== '@') {
19+
return false
20+
}
21+
22+
const rest = spec.slice(1).split('/')
23+
if (rest.length !== 2) {
24+
return false
25+
}
26+
27+
return rest[0] && rest[1] &&
28+
rest[0] === encodeURIComponent(rest[0]) &&
29+
rest[1] === encodeURIComponent(rest[1])
30+
}
931

1032
// We don't want the `changes` array in here by default because this is a hot
1133
// path for parsing packuments during install. So the calling method passes it
@@ -24,11 +46,47 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase })
2446

2547
// name and version are load bearing so we have to clean them up first
2648
if (steps.includes('fixNameField') || steps.includes('normalizeData')) {
27-
legacyFixer.fixNameField(data, { strict, allowLegacyCase })
49+
if (!data.name && !strict) {
50+
changes?.push('Missing "name" field was set to an empty string')
51+
data.name = ''
52+
} else {
53+
if (typeof data.name !== 'string') {
54+
throw new Error('name field must be a string.')
55+
}
56+
if (!strict) {
57+
const name = data.name.trim()
58+
if (data.name !== name) {
59+
changes?.push(`Whitespace was trimmed from "name"`)
60+
data.name = name
61+
}
62+
}
63+
64+
if (data.name.startsWith('.') ||
65+
!(isValidScopedPackageName(data.name) || isCorrectlyEncodedName(data.name)) ||
66+
(strict && (!allowLegacyCase) && data.name !== data.name.toLowerCase()) ||
67+
data.name.toLowerCase() === 'node_modules' ||
68+
data.name.toLowerCase() === 'favicon.ico') {
69+
throw new Error('Invalid name: ' + JSON.stringify(data.name))
70+
}
71+
}
2872
}
2973

3074
if (steps.includes('fixVersionField') || steps.includes('normalizeData')) {
31-
legacyFixer.fixVersionField(data, strict)
75+
// allow "loose" semver 1.0 versions in non-strict mode
76+
// enforce strict semver 2.0 compliance in strict mode
77+
const loose = !strict
78+
if (!data.version) {
79+
data.version = ''
80+
} else {
81+
if (!semver.valid(data.version, !loose)) {
82+
throw new Error(`Invalid version: "${data.version}"`)
83+
}
84+
const version = semver.clean(data.version, !loose)
85+
if (version !== data.version) {
86+
changes?.push(`"version" was cleaned and set to "${version}"`)
87+
data.version = version
88+
}
89+
}
3290
}
3391
// remove attributes that start with "_"
3492
if (steps.includes('_attributes')) {
@@ -84,11 +142,11 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase })
84142
if (data.dependencies &&
85143
data.optionalDependencies && typeof data.optionalDependencies === 'object') {
86144
for (const name in data.optionalDependencies) {
87-
changes?.push(`optionalDependencies entry "${name}" was removed`)
145+
changes?.push(`optionalDependencies."${name}" was removed`)
88146
delete data.dependencies[name]
89147
}
90148
if (!Object.keys(data.dependencies).length) {
91-
changes?.push(`empty "optionalDependencies" was removed`)
149+
changes?.push(`Empty "optionalDependencies" was removed`)
92150
delete data.dependencies
93151
}
94152
}
@@ -121,20 +179,21 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase })
121179
}
122180

123181
// strip "node_modules/.bin" from scripts entries
182+
// remove invalid scripts entries (non-strings)
124183
if (steps.includes('scripts') || steps.includes('scriptpath')) {
125184
const spre = /^(\.[/\\])?node_modules[/\\].bin[\\/]/
126185
if (typeof data.scripts === 'object') {
127186
for (const name in data.scripts) {
128187
if (typeof data.scripts[name] !== 'string') {
129188
delete data.scripts[name]
130-
changes?.push(`invalid scripts entry "${name}" was removed`)
189+
changes?.push(`Invalid scripts."${name}" was removed`)
131190
} else if (steps.includes('scriptpath')) {
132191
data.scripts[name] = data.scripts[name].replace(spre, '')
133192
changes?.push(`scripts entry "${name}" was fixed to remove node_modules/.bin reference`)
134193
}
135194
}
136195
} else {
137-
changes?.push(`removed invalid "scripts"`)
196+
changes?.push(`Removed invalid "scripts"`)
138197
delete data.scripts
139198
}
140199
}
@@ -320,19 +379,89 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase })
320379

321380
// Some steps are isolated so we can do a limited subset of these in `fix`
322381
if (steps.includes('fixRepositoryField') || steps.includes('normalizeData')) {
323-
legacyFixer.fixRepositoryField(data)
324-
}
325-
326-
if (steps.includes('fixBinField') || steps.includes('normalizeData')) {
327-
legacyFixer.fixBinField(data)
382+
if (data.repositories) {
383+
/* eslint-disable-next-line max-len */
384+
changes?.push(`"repository" was set to the first entry in "repositories" (${data.repository})`)
385+
data.repository = data.repositories[0]
386+
}
387+
if (data.repository) {
388+
if (typeof data.repository === 'string') {
389+
changes?.push('"repository" was changed from a string to an object')
390+
data.repository = {
391+
type: 'git',
392+
url: data.repository,
393+
}
394+
}
395+
if (data.repository.url) {
396+
const hosted = hostedGitInfo.fromUrl(data.repository.url)
397+
let r
398+
if (hosted) {
399+
if (hosted.getDefaultRepresentation() === 'shortcut') {
400+
r = hosted.https()
401+
} else {
402+
r = hosted.toString()
403+
}
404+
if (r !== data.repository.url) {
405+
changes?.push(`"repository.url" was normalized to "${r}"`)
406+
data.repository.url = r
407+
}
408+
}
409+
}
410+
}
328411
}
329412

330413
if (steps.includes('fixDependencies') || steps.includes('normalizeData')) {
331-
legacyFixer.fixDependencies(data, strict)
332-
}
414+
// peerDependencies?
415+
// devDependencies is meaningless here, it's ignored on an installed package
416+
for (const type of ['dependencies', 'devDependencies', 'optionalDependencies']) {
417+
if (data[type]) {
418+
let secondWarning = true
419+
if (typeof data[type] === 'string') {
420+
changes?.push(`"${type}" was converted from a string into an object`)
421+
data[type] = data[type].trim().split(/[\n\r\s\t ,]+/)
422+
secondWarning = false
423+
}
424+
if (Array.isArray(data[type])) {
425+
if (secondWarning) {
426+
changes?.push(`"${type}" was converted from an array into an object`)
427+
}
428+
const o = {}
429+
for (const d of data[type]) {
430+
if (typeof d === 'string') {
431+
const dep = d.trim().split(/(:?[@\s><=])/)
432+
const dn = dep.shift()
433+
const dv = dep.join('').replace(/^@/, '').trim()
434+
o[dn] = dv
435+
}
436+
}
437+
data[type] = o
438+
}
439+
}
440+
}
441+
// normalize-package-data used to put optional dependencies BACK into
442+
// dependencies here, we no longer do this
333443

334-
if (steps.includes('fixScriptsField') || steps.includes('normalizeData')) {
335-
legacyFixer.fixScriptsField(data)
444+
for (const deps of ['dependencies', 'devDependencies']) {
445+
if (deps in data) {
446+
if (!data[deps] || typeof data[deps] !== 'object') {
447+
changes?.push(`Removed invalid "${deps}"`)
448+
delete data[deps]
449+
} else {
450+
for (const d in data[deps]) {
451+
const r = data[deps][d]
452+
if (typeof r !== 'string') {
453+
changes?.push(`Removed invalid "${deps}.${d}"`)
454+
delete data[deps][d]
455+
}
456+
const hosted = hostedGitInfo.fromUrl(data[deps][d])?.toString()
457+
if (hosted && hosted !== data[deps][d]) {
458+
changes?.push(`Normalized git reference to "${deps}.${d}"`)
459+
data[deps][d] = hosted.toString()
460+
}
461+
}
462+
}
463+
}
464+
}
336465
}
337466

338467
if (steps.includes('normalizeData')) {

tap-snapshots/test/fix.js.test.cjs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/* IMPORTANT
2+
* This snapshot file is auto-generated, but designed for humans.
3+
* It should be checked into source control and tracked carefully.
4+
* Re-generate by setting TAP_SNAPSHOT=1 and running tests.
5+
* Make sure to inspect the output below. Do not ignore changes!
6+
*/
7+
'use strict'
8+
exports[`test/fix.js TAP with changes binRefs scoped name > must match snapshot 1`] = `
9+
Array [
10+
"\\"bundleDependencies\\" was removed",
11+
]
12+
`
13+
14+
exports[`test/fix.js TAP with changes fixDependencies array dependencies > must match snapshot 1`] = `
15+
Array [
16+
"\\"bundleDependencies\\" was removed",
17+
"\\"dependencies\\" was converted from an array into an object",
18+
]
19+
`
20+
21+
exports[`test/fix.js TAP with changes fixDependencies false dependencies > must match snapshot 1`] = `
22+
Array [
23+
"\\"bundleDependencies\\" was removed",
24+
"Removed invalid \\"dependencies\\"",
25+
]
26+
`
27+
28+
exports[`test/fix.js TAP with changes fixDependencies git dependency > must match snapshot 1`] = `
29+
Array [
30+
"\\"bundleDependencies\\" was removed",
31+
"Normalized git reference to \\"dependencies.npm\\"",
32+
]
33+
`
34+
35+
exports[`test/fix.js TAP with changes fixDependencies non-string dependency > must match snapshot 1`] = `
36+
Array [
37+
"\\"bundleDependencies\\" was removed",
38+
"Removed invalid \\"dependencies.npm\\"",
39+
]
40+
`
41+
42+
exports[`test/fix.js TAP with changes fixDependencies string dependencies > must match snapshot 1`] = `
43+
Array [
44+
"\\"bundleDependencies\\" was removed",
45+
"\\"dependencies\\" was converted from a string into an object",
46+
]
47+
`
48+
49+
exports[`test/fix.js TAP with changes fixNameField scoped whitespace > must match snapshot 1`] = `
50+
Array [
51+
"Whitespace was trimmed from \\"name\\"",
52+
"\\"bundleDependencies\\" was removed",
53+
]
54+
`
55+
56+
exports[`test/fix.js TAP with changes fixNameField unscoped whitespace > must match snapshot 1`] = `
57+
Array [
58+
"Whitespace was trimmed from \\"name\\"",
59+
"\\"bundleDependencies\\" was removed",
60+
]
61+
`
62+
63+
exports[`test/fix.js TAP with changes fixRepositoryField full > must match snapshot 1`] = `
64+
Array [
65+
"\\"bundleDependencies\\" was removed",
66+
"\\"repository\\" was changed from a string to an object",
67+
"\\"repository.url\\" was normalized to \\"git+https://github.com/npm/cli.git\\"",
68+
]
69+
`
70+
71+
exports[`test/fix.js TAP with changes fixRepositoryField object no url > must match snapshot 1`] = `
72+
Array [
73+
"\\"bundleDependencies\\" was removed",
74+
]
75+
`
76+
77+
exports[`test/fix.js TAP with changes fixRepositoryField repositories array > must match snapshot 1`] = `
78+
Array [
79+
"\\"bundleDependencies\\" was removed",
80+
"\\"repository\\" was set to the first entry in \\"repositories\\" ([object Object])",
81+
"\\"repository\\" was changed from a string to an object",
82+
]
83+
`
84+
85+
exports[`test/fix.js TAP with changes fixRepositoryField shortcut > must match snapshot 1`] = `
86+
Array [
87+
"\\"bundleDependencies\\" was removed",
88+
"\\"repository\\" was changed from a string to an object",
89+
"\\"repository.url\\" was normalized to \\"git+https://github.com/npm/cli.git\\"",
90+
]
91+
`
92+
93+
exports[`test/fix.js TAP with changes fixVersionField none > must match snapshot 1`] = `
94+
Array [
95+
"\\"bundleDependencies\\" was removed",
96+
]
97+
`
98+
99+
exports[`test/fix.js TAP with changes fixVersionField unclean > must match snapshot 1`] = `
100+
Array [
101+
"\\"version\\" was cleaned and set to \\"1.0.0\\"",
102+
"\\"bundleDependencies\\" was removed",
103+
]
104+
`
105+
106+
exports[`test/fix.js TAP with changes scriptpath non-object script entry > must match snapshot 1`] = `
107+
Array [
108+
"\\"bundleDependencies\\" was removed",
109+
"Invalid scripts.\\"test\\" was removed",
110+
]
111+
`
112+
113+
exports[`test/fix.js TAP with changes scriptpath non-object scripts > must match snapshot 1`] = `
114+
Array [
115+
"\\"bundleDependencies\\" was removed",
116+
"Removed invalid \\"scripts\\"",
117+
]
118+
`

tap-snapshots/test/index.js.test.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ exports[`test/index.js TAP load custom formatting > should save back custom form
3030
{"name":"foo","version":"1.0.1","description":"Lorem ipsum dolor"}
3131
`
3232

33-
exports[`test/index.js TAP load read, update content and write > should properly save contennt to a package.json 1`] = `
33+
exports[`test/index.js TAP load read, update content and write > should properly save content to a package.json 1`] = `
3434
{
3535
"name": "foo",
3636
"version": "1.0.1",

0 commit comments

Comments
 (0)