1
+ const semver = require ( 'semver' )
1
2
const fs = require ( 'fs/promises' )
2
3
const { glob } = require ( 'glob' )
3
- const normalizePackageBin = require ( 'npm-normalize-package-bin' )
4
4
const legacyFixer = require ( 'normalize-package-data/lib/fixer.js' )
5
5
const legacyMakeWarning = require ( 'normalize-package-data/lib/make_warning.js' )
6
6
const path = require ( 'path' )
7
7
const log = require ( 'proc-log' )
8
8
const 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
+ }
9
87
10
88
// We don't want the `changes` array in here by default because this is a hot
11
89
// 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 })
18
96
const scripts = data . scripts || { }
19
97
const pkgId = `${ data . name ?? '' } @${ data . version ?? '' } `
20
98
21
- legacyFixer . warn = function ( ) {
22
- changes ?. push ( legacyMakeWarning . apply ( null , arguments ) )
23
- }
24
-
25
99
// name and version are load bearing so we have to clean them up first
26
100
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
+ }
28
124
}
29
125
30
126
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
+ }
32
142
}
33
143
// remove attributes that start with "_"
34
144
if ( steps . includes ( '_attributes' ) ) {
@@ -49,6 +159,7 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase })
49
159
}
50
160
51
161
// fix bundledDependencies typo
162
+ // normalize bundleDependencies
52
163
if ( steps . includes ( 'bundledDependencies' ) ) {
53
164
if ( data . bundleDependencies === undefined && data . bundledDependencies !== undefined ) {
54
165
data . bundleDependencies = data . bundledDependencies
@@ -70,7 +181,7 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase })
70
181
changes ?. push ( `"bundleDependencies" was changed from an object to an array` )
71
182
data . bundleDependencies = Object . keys ( bd )
72
183
}
73
- } else {
184
+ } else if ( 'bundleDependencies' in data ) {
74
185
changes ?. push ( `"bundleDependencies" was removed` )
75
186
delete data . bundleDependencies
76
187
}
@@ -84,11 +195,11 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase })
84
195
if ( data . dependencies &&
85
196
data . optionalDependencies && typeof data . optionalDependencies === 'object' ) {
86
197
for ( const name in data . optionalDependencies ) {
87
- changes ?. push ( `optionalDependencies entry "${ name } " was removed` )
198
+ changes ?. push ( `optionalDependencies. "${ name } " was removed` )
88
199
delete data . dependencies [ name ]
89
200
}
90
201
if ( ! Object . keys ( data . dependencies ) . length ) {
91
- changes ?. push ( `empty "optionalDependencies" was removed` )
202
+ changes ?. push ( `Empty "optionalDependencies" was removed` )
92
203
delete data . dependencies
93
204
}
94
205
}
@@ -121,20 +232,21 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase })
121
232
}
122
233
123
234
// strip "node_modules/.bin" from scripts entries
235
+ // remove invalid scripts entries (non-strings)
124
236
if ( steps . includes ( 'scripts' ) || steps . includes ( 'scriptpath' ) ) {
125
237
const spre = / ^ ( \. [ / \\ ] ) ? n o d e _ m o d u l e s [ / \\ ] .b i n [ \\ / ] /
126
238
if ( typeof data . scripts === 'object' ) {
127
239
for ( const name in data . scripts ) {
128
240
if ( typeof data . scripts [ name ] !== 'string' ) {
129
241
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 ] ) ) {
132
244
data . scripts [ name ] = data . scripts [ name ] . replace ( spre , '' )
133
245
changes ?. push ( `scripts entry "${ name } " was fixed to remove node_modules/.bin reference` )
134
246
}
135
247
}
136
248
} else {
137
- changes ?. push ( `removed invalid "scripts"` )
249
+ changes ?. push ( `Removed invalid "scripts"` )
138
250
delete data . scripts
139
251
}
140
252
}
@@ -154,7 +266,7 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase })
154
266
. map ( line => line . replace ( / ^ \s * # .* $ / , '' ) . trim ( ) )
155
267
. filter ( line => line )
156
268
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' )
158
270
} catch {
159
271
// do nothing
160
272
}
@@ -201,7 +313,7 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase })
201
313
}
202
314
203
315
if ( steps . includes ( 'bin' ) || steps . includes ( 'binDir' ) || steps . includes ( 'binRefs' ) ) {
204
- normalizePackageBin ( data )
316
+ normalizePackageBin ( data , changes )
205
317
}
206
318
207
319
// expand "directories.bin"
@@ -216,7 +328,7 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase })
216
328
return acc
217
329
} , { } )
218
330
// *sigh*
219
- normalizePackageBin ( data )
331
+ normalizePackageBin ( data , changes )
220
332
}
221
333
222
334
// populate "gitHead" attribute
@@ -320,22 +432,96 @@ const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase })
320
432
321
433
// Some steps are isolated so we can do a limited subset of these in `fix`
322
434
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
+ }
328
464
}
329
465
330
466
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
333
496
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
+ }
336
518
}
337
519
338
520
if ( steps . includes ( 'normalizeData' ) ) {
521
+ legacyFixer . warn = function ( ) {
522
+ changes ?. push ( legacyMakeWarning . apply ( null , arguments ) )
523
+ }
524
+
339
525
const legacySteps = [
340
526
'fixDescriptionField' ,
341
527
'fixModulesField' ,
0 commit comments