1+ const semver = require ( 'semver' )
12const fs = require ( 'fs/promises' )
23const { glob } = require ( 'glob' )
3- const normalizePackageBin = require ( 'npm-normalize-package-bin' )
44const legacyFixer = require ( 'normalize-package-data/lib/fixer.js' )
55const legacyMakeWarning = require ( 'normalize-package-data/lib/make_warning.js' )
66const path = require ( 'path' )
77const log = require ( 'proc-log' )
88const git = require ( '@npmcli/git' )
9+ const hostedGitInfo = require ( 'hosted-git-info' )
10+
11+ // used to be npm-normalize-package-bin
12+ function normalizePackageBin ( pkg , changes ) {
13+ if ( pkg . bin ) {
14+ if ( typeof pkg . bin === 'string' && pkg . name ) {
15+ changes ?. push ( '"bin" was converted to an object' )
16+ pkg . bin = { [ pkg . name ] : pkg . bin }
17+ } else if ( Array . isArray ( pkg . bin ) ) {
18+ changes ?. push ( '"bin" was converted to an object' )
19+ pkg . bin = pkg . bin . reduce ( ( acc , k ) => {
20+ acc [ path . basename ( k ) ] = k
21+ return acc
22+ } , { } )
23+ }
24+ if ( typeof pkg . bin === 'object' ) {
25+ for ( const binKey in pkg . bin ) {
26+ if ( typeof pkg . bin [ binKey ] !== 'string' ) {
27+ delete pkg . bin [ binKey ]
28+ changes ?. push ( `removed invalid "bin[${ binKey } ]"` )
29+ continue
30+ }
31+ const base = path . join ( '/' , path . basename ( binKey . replace ( / \\ | : / g, '/' ) ) ) . slice ( 1 )
32+ if ( ! base ) {
33+ delete pkg . bin [ binKey ]
34+ changes ?. push ( `removed invalid "bin[${ binKey } ]"` )
35+ continue
36+ }
37+
38+ const binTarget = path . join ( '/' , pkg . bin [ binKey ] . replace ( / \\ / g, '/' ) )
39+ . replace ( / \\ / g, '/' ) . slice ( 1 )
40+
41+ if ( ! binTarget ) {
42+ delete pkg . bin [ binKey ]
43+ changes ?. push ( `removed invalid "bin[${ binKey } ]"` )
44+ continue
45+ }
46+
47+ if ( base !== binKey ) {
48+ delete pkg . bin [ binKey ]
49+ changes ?. push ( `"bin[${ binKey } ]" was renamed to "bin[${ base } ]"` )
50+ }
51+ if ( binTarget !== pkg . bin [ binKey ] ) {
52+ changes ?. push ( `"bin[${ base } ]" script name was cleaned` )
53+ }
54+ pkg . bin [ base ] = binTarget
55+ }
56+
57+ if ( Object . keys ( pkg . bin ) . length === 0 ) {
58+ changes ?. push ( 'empty "bin" was removed' )
59+ delete pkg . bin
60+ }
61+
62+ return pkg
63+ }
64+ }
65+ delete pkg . bin
66+ }
67+
68+ function isCorrectlyEncodedName ( spec ) {
69+ return ! spec . match ( / [ / @ \s + % : ] / ) &&
70+ spec === encodeURIComponent ( spec )
71+ }
72+
73+ function isValidScopedPackageName ( spec ) {
74+ if ( spec . charAt ( 0 ) !== '@' ) {
75+ return false
76+ }
77+
78+ const rest = spec . slice ( 1 ) . split ( '/' )
79+ if ( rest . length !== 2 ) {
80+ return false
81+ }
82+
83+ return rest [ 0 ] && rest [ 1 ] &&
84+ rest [ 0 ] === encodeURIComponent ( rest [ 0 ] ) &&
85+ rest [ 1 ] === encodeURIComponent ( rest [ 1 ] )
86+ }
987
1088// We don't want the `changes` array in here by default because this is a hot
1189// 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 })
1896 const scripts = data . scripts || { }
1997 const pkgId = `${ data . name ?? '' } @${ data . version ?? '' } `
2098
21- legacyFixer . warn = function ( ) {
22- changes ?. push ( legacyMakeWarning . apply ( null , arguments ) )
23- }
24-
2599 // name and version are load bearing so we have to clean them up first
26100 if ( steps . includes ( 'fixNameField' ) || steps . includes ( 'normalizeData' ) ) {
27- legacyFixer . fixNameField ( data , { strict, allowLegacyCase } )
101+ if ( ! data . name && ! strict ) {
102+ changes ?. push ( 'Missing "name" field was set to an empty string' )
103+ data . name = ''
104+ } else {
105+ if ( typeof data . name !== 'string' ) {
106+ throw new Error ( 'name field must be a string.' )
107+ }
108+ if ( ! strict ) {
109+ const name = data . name . trim ( )
110+ if ( data . name !== name ) {
111+ changes ?. push ( `Whitespace was trimmed from "name"` )
112+ data . name = name
113+ }
114+ }
115+
116+ if ( data . name . startsWith ( '.' ) ||
117+ ! ( isValidScopedPackageName ( data . name ) || isCorrectlyEncodedName ( data . name ) ) ||
118+ ( strict && ( ! allowLegacyCase ) && data . name !== data . name . toLowerCase ( ) ) ||
119+ data . name . toLowerCase ( ) === 'node_modules' ||
120+ data . name . toLowerCase ( ) === 'favicon.ico' ) {
121+ throw new Error ( 'Invalid name: ' + JSON . stringify ( data . name ) )
122+ }
123+ }
28124 }
29125
30126 if ( steps . includes ( 'fixVersionField' ) || steps . includes ( 'normalizeData' ) ) {
31- legacyFixer . fixVersionField ( data , strict )
127+ // allow "loose" semver 1.0 versions in non-strict mode
128+ // enforce strict semver 2.0 compliance in strict mode
129+ const loose = ! strict
130+ if ( ! data . version ) {
131+ data . version = ''
132+ } else {
133+ if ( ! semver . valid ( data . version , loose ) ) {
134+ throw new Error ( `Invalid version: "${ data . version } "` )
135+ }
136+ const version = semver . clean ( data . version , loose )
137+ if ( version !== data . version ) {
138+ changes ?. push ( `"version" was cleaned and set to "${ version } "` )
139+ data . version = version
140+ }
141+ }
32142 }
33143 // remove attributes that start with "_"
34144 if ( steps . includes ( '_attributes' ) ) {
@@ -49,6 +159,7 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase })
49159 }
50160
51161 // fix bundledDependencies typo
162+ // normalize bundleDependencies
52163 if ( steps . includes ( 'bundledDependencies' ) ) {
53164 if ( data . bundleDependencies === undefined && data . bundledDependencies !== undefined ) {
54165 data . bundleDependencies = data . bundledDependencies
@@ -70,7 +181,7 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase })
70181 changes ?. push ( `"bundleDependencies" was changed from an object to an array` )
71182 data . bundleDependencies = Object . keys ( bd )
72183 }
73- } else {
184+ } else if ( 'bundleDependencies' in data ) {
74185 changes ?. push ( `"bundleDependencies" was removed` )
75186 delete data . bundleDependencies
76187 }
@@ -84,11 +195,11 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase })
84195 if ( data . dependencies &&
85196 data . optionalDependencies && typeof data . optionalDependencies === 'object' ) {
86197 for ( const name in data . optionalDependencies ) {
87- changes ?. push ( `optionalDependencies entry "${ name } " was removed` )
198+ changes ?. push ( `optionalDependencies. "${ name } " was removed` )
88199 delete data . dependencies [ name ]
89200 }
90201 if ( ! Object . keys ( data . dependencies ) . length ) {
91- changes ?. push ( `empty "optionalDependencies" was removed` )
202+ changes ?. push ( `Empty "optionalDependencies" was removed` )
92203 delete data . dependencies
93204 }
94205 }
@@ -121,20 +232,21 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase })
121232 }
122233
123234 // strip "node_modules/.bin" from scripts entries
235+ // remove invalid scripts entries (non-strings)
124236 if ( steps . includes ( 'scripts' ) || steps . includes ( 'scriptpath' ) ) {
125237 const spre = / ^ ( \. [ / \\ ] ) ? n o d e _ m o d u l e s [ / \\ ] .b i n [ \\ / ] /
126238 if ( typeof data . scripts === 'object' ) {
127239 for ( const name in data . scripts ) {
128240 if ( typeof data . scripts [ name ] !== 'string' ) {
129241 delete data . scripts [ name ]
130- changes ?. push ( `invalid scripts entry "${ name } " was removed` )
131- } else if ( steps . includes ( 'scriptpath' ) ) {
242+ changes ?. push ( `Invalid scripts. "${ name } " was removed` )
243+ } else if ( steps . includes ( 'scriptpath' ) && spre . test ( data . scripts [ name ] ) ) {
132244 data . scripts [ name ] = data . scripts [ name ] . replace ( spre , '' )
133245 changes ?. push ( `scripts entry "${ name } " was fixed to remove node_modules/.bin reference` )
134246 }
135247 }
136248 } else {
137- changes ?. push ( `removed invalid "scripts"` )
249+ changes ?. push ( `Removed invalid "scripts"` )
138250 delete data . scripts
139251 }
140252 }
@@ -154,7 +266,7 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase })
154266 . map ( line => line . replace ( / ^ \s * # .* $ / , '' ) . trim ( ) )
155267 . filter ( line => line )
156268 data . contributors = authors
157- changes . push ( '"contributors" was auto-populated with the contents of the "AUTHORS" file' )
269+ changes ? .push ( '"contributors" was auto-populated with the contents of the "AUTHORS" file' )
158270 } catch {
159271 // do nothing
160272 }
@@ -201,7 +313,7 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase })
201313 }
202314
203315 if ( steps . includes ( 'bin' ) || steps . includes ( 'binDir' ) || steps . includes ( 'binRefs' ) ) {
204- normalizePackageBin ( data )
316+ normalizePackageBin ( data , changes )
205317 }
206318
207319 // expand "directories.bin"
@@ -216,7 +328,7 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase })
216328 return acc
217329 } , { } )
218330 // *sigh*
219- normalizePackageBin ( data )
331+ normalizePackageBin ( data , changes )
220332 }
221333
222334 // populate "gitHead" attribute
@@ -320,22 +432,96 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase })
320432
321433 // Some steps are isolated so we can do a limited subset of these in `fix`
322434 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 )
435+ if ( data . repositories ) {
436+ /* eslint-disable-next-line max-len */
437+ changes ?. push ( `"repository" was set to the first entry in "repositories" (${ data . repository } )` )
438+ data . repository = data . repositories [ 0 ]
439+ }
440+ if ( data . repository ) {
441+ if ( typeof data . repository === 'string' ) {
442+ changes ?. push ( '"repository" was changed from a string to an object' )
443+ data . repository = {
444+ type : 'git' ,
445+ url : data . repository ,
446+ }
447+ }
448+ if ( data . repository . url ) {
449+ const hosted = hostedGitInfo . fromUrl ( data . repository . url )
450+ let r
451+ if ( hosted ) {
452+ if ( hosted . getDefaultRepresentation ( ) === 'shortcut' ) {
453+ r = hosted . https ( )
454+ } else {
455+ r = hosted . toString ( )
456+ }
457+ if ( r !== data . repository . url ) {
458+ changes ?. push ( `"repository.url" was normalized to "${ r } "` )
459+ data . repository . url = r
460+ }
461+ }
462+ }
463+ }
328464 }
329465
330466 if ( steps . includes ( 'fixDependencies' ) || steps . includes ( 'normalizeData' ) ) {
331- legacyFixer . fixDependencies ( data , strict )
332- }
467+ // peerDependencies?
468+ // devDependencies is meaningless here, it's ignored on an installed package
469+ for ( const type of [ 'dependencies' , 'devDependencies' , 'optionalDependencies' ] ) {
470+ if ( data [ type ] ) {
471+ let secondWarning = true
472+ if ( typeof data [ type ] === 'string' ) {
473+ changes ?. push ( `"${ type } " was converted from a string into an object` )
474+ data [ type ] = data [ type ] . trim ( ) . split ( / [ \n \r \s \t , ] + / )
475+ secondWarning = false
476+ }
477+ if ( Array . isArray ( data [ type ] ) ) {
478+ if ( secondWarning ) {
479+ changes ?. push ( `"${ type } " was converted from an array into an object` )
480+ }
481+ const o = { }
482+ for ( const d of data [ type ] ) {
483+ if ( typeof d === 'string' ) {
484+ const dep = d . trim ( ) . split ( / ( : ? [ @ \s > < = ] ) / )
485+ const dn = dep . shift ( )
486+ const dv = dep . join ( '' ) . replace ( / ^ @ / , '' ) . trim ( )
487+ o [ dn ] = dv
488+ }
489+ }
490+ data [ type ] = o
491+ }
492+ }
493+ }
494+ // normalize-package-data used to put optional dependencies BACK into
495+ // dependencies here, we no longer do this
333496
334- if ( steps . includes ( 'fixScriptsField' ) || steps . includes ( 'normalizeData' ) ) {
335- legacyFixer . fixScriptsField ( data )
497+ for ( const deps of [ 'dependencies' , 'devDependencies' ] ) {
498+ if ( deps in data ) {
499+ if ( ! data [ deps ] || typeof data [ deps ] !== 'object' ) {
500+ changes ?. push ( `Removed invalid "${ deps } "` )
501+ delete data [ deps ]
502+ } else {
503+ for ( const d in data [ deps ] ) {
504+ const r = data [ deps ] [ d ]
505+ if ( typeof r !== 'string' ) {
506+ changes ?. push ( `Removed invalid "${ deps } .${ d } "` )
507+ delete data [ deps ] [ d ]
508+ }
509+ const hosted = hostedGitInfo . fromUrl ( data [ deps ] [ d ] ) ?. toString ( )
510+ if ( hosted && hosted !== data [ deps ] [ d ] ) {
511+ changes ?. push ( `Normalized git reference to "${ deps } .${ d } "` )
512+ data [ deps ] [ d ] = hosted . toString ( )
513+ }
514+ }
515+ }
516+ }
517+ }
336518 }
337519
338520 if ( steps . includes ( 'normalizeData' ) ) {
521+ legacyFixer . warn = function ( ) {
522+ changes ?. push ( legacyMakeWarning . apply ( null , arguments ) )
523+ }
524+
339525 const legacySteps = [
340526 'fixDescriptionField' ,
341527 'fixModulesField' ,
0 commit comments