1+ const semver = require ( 'semver' )
12const fs = require ( 'fs/promises' )
23const { glob } = require ( 'glob' )
34const normalizePackageBin = require ( 'npm-normalize-package-bin' )
@@ -6,6 +7,27 @@ const legacyMakeWarning = require('normalize-package-data/lib/make_warning.js')
67const path = require ( 'path' )
78const log = require ( 'proc-log' )
89const 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 = / ^ ( \. [ / \\ ] ) ? n o d e _ m o d u l e s [ / \\ ] .b i n [ \\ / ] /
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' ) ) {
0 commit comments