@npmcli/arborist 2.2.1 → 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ideal.js +4 -52
- package/bin/lib/logging.js +1 -1
- package/lib/arborist/build-ideal-tree.js +51 -16
- package/lib/arborist/load-virtual.js +31 -11
- package/lib/arborist/reify.js +9 -3
- package/lib/calc-dep-flags.js +1 -1
- package/package.json +1 -1
package/bin/ideal.js
CHANGED
|
@@ -1,59 +1,11 @@
|
|
|
1
1
|
const Arborist = require('../')
|
|
2
2
|
|
|
3
|
+
const { inspect } = require('util')
|
|
3
4
|
const options = require('./lib/options.js')
|
|
4
5
|
const print = require('./lib/print-tree.js')
|
|
5
6
|
require('./lib/logging.js')
|
|
6
7
|
require('./lib/timers.js')
|
|
7
8
|
|
|
8
|
-
const c = require('chalk')
|
|
9
|
-
|
|
10
|
-
const whichIsA = (name, dependents, indent = ' ') => {
|
|
11
|
-
if (!dependents || dependents.length === 0)
|
|
12
|
-
return ''
|
|
13
|
-
const str = `\nfor: ` +
|
|
14
|
-
dependents.map(dep => {
|
|
15
|
-
return dep.more ? `${dep.more} more (${dep.names.join(', ')})`
|
|
16
|
-
: `${dep.type} dependency ` +
|
|
17
|
-
`${c.bold(name)}@"${c.bold(dep.spec)}"` + `\nfrom:` +
|
|
18
|
-
(dep.from.location ? (dep.from.name
|
|
19
|
-
? ` ${c.bold(dep.from.name)}@${c.bold(dep.from.version)} ` +
|
|
20
|
-
c.dim(`at ${dep.from.location}`)
|
|
21
|
-
: ' the root project')
|
|
22
|
-
: ` ${c.bold(dep.from.name)}@${c.bold(dep.from.version)}`) +
|
|
23
|
-
whichIsA(dep.from.name, dep.from.dependents, ' ')
|
|
24
|
-
}).join('\nand: ')
|
|
25
|
-
|
|
26
|
-
return str.split(/\n/).join(`\n${indent}`)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const explainEresolve = ({ dep, current, peerConflict, fixWithForce }) => {
|
|
30
|
-
return (!dep.whileInstalling ? '' : `While resolving: ` +
|
|
31
|
-
`${c.bold(dep.whileInstalling.name)}@${c.bold(dep.whileInstalling.version)}\n`) +
|
|
32
|
-
|
|
33
|
-
`Found: ` +
|
|
34
|
-
`${c.bold(current.name)}@${c.bold(current.version)} ` +
|
|
35
|
-
c.dim(`at ${current.location}`) +
|
|
36
|
-
`${whichIsA(current.name, current.dependents)}` +
|
|
37
|
-
|
|
38
|
-
`\n\nCould not add conflicting dependency: ` +
|
|
39
|
-
`${c.bold(dep.name)}@${c.bold(dep.version)} ` +
|
|
40
|
-
c.dim(`at ${dep.location}`) +
|
|
41
|
-
`${whichIsA(dep.name, dep.dependents)}\n` +
|
|
42
|
-
|
|
43
|
-
(!peerConflict ? '' :
|
|
44
|
-
`\nConflicting peer dependency: ` +
|
|
45
|
-
`${c.bold(peerConflict.name)}@${c.bold(peerConflict.version)} ` +
|
|
46
|
-
c.dim(`at ${peerConflict.location}`) +
|
|
47
|
-
`${whichIsA(peerConflict.name, peerConflict.dependents)}\n`
|
|
48
|
-
) +
|
|
49
|
-
|
|
50
|
-
`\nFix the upstream dependency conflict, or
|
|
51
|
-
run this command with --legacy-peer-deps${
|
|
52
|
-
fixWithForce ? ' or --force' : ''}
|
|
53
|
-
to accept an incorrect (and potentially broken) dependency resolution.
|
|
54
|
-
`
|
|
55
|
-
}
|
|
56
|
-
|
|
57
9
|
const start = process.hrtime()
|
|
58
10
|
new Arborist(options).buildIdealTree(options).then(tree => {
|
|
59
11
|
const end = process.hrtime(start)
|
|
@@ -62,7 +14,7 @@ new Arborist(options).buildIdealTree(options).then(tree => {
|
|
|
62
14
|
if (tree.meta && options.save)
|
|
63
15
|
tree.meta.save()
|
|
64
16
|
}).catch(er => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
17
|
+
const opt = { depth: Infinity, color: true }
|
|
18
|
+
console.error(er.code === 'ERESOLVE' ? inspect(er, opt) : er)
|
|
19
|
+
process.exitCode = 1
|
|
68
20
|
})
|
package/bin/lib/logging.js
CHANGED
|
@@ -26,7 +26,7 @@ if (loglevel !== 'silent') {
|
|
|
26
26
|
return
|
|
27
27
|
const pref = `${process.pid} ${level} `
|
|
28
28
|
if (level === 'warn' && args[0] === 'ERESOLVE')
|
|
29
|
-
args[2] = inspect(args[2], { depth:
|
|
29
|
+
args[2] = inspect(args[2], { depth: 10 })
|
|
30
30
|
const msg = pref + format(...args).trim().split('\n').join(`\n${pref}`)
|
|
31
31
|
console.error(msg)
|
|
32
32
|
})
|
|
@@ -397,7 +397,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
|
|
|
397
397
|
// that they're there, and not reinstall the world unnecessarily.
|
|
398
398
|
if (this[_global] && (this[_updateAll] || this[_updateNames].length)) {
|
|
399
399
|
const nm = resolve(this.path, 'node_modules')
|
|
400
|
-
for (const name of await readdir(nm)) {
|
|
400
|
+
for (const name of await readdir(nm).catch(() => [])) {
|
|
401
401
|
tree.package.dependencies = tree.package.dependencies || {}
|
|
402
402
|
if (this[_updateAll] || this[_updateNames].includes(name))
|
|
403
403
|
tree.package.dependencies[name] = '*'
|
|
@@ -491,7 +491,8 @@ module.exports = cls => class IdealTreeBuilder extends cls {
|
|
|
491
491
|
/* istanbul ignore else - should also be covered by realpath failure */
|
|
492
492
|
if (filepath) {
|
|
493
493
|
const { name } = spec
|
|
494
|
-
|
|
494
|
+
const tree = this.idealTree.target || this.idealTree
|
|
495
|
+
spec = npa(`file:${relpath(tree.path, filepath)}`, tree.path)
|
|
495
496
|
spec.name = name
|
|
496
497
|
}
|
|
497
498
|
return spec
|
|
@@ -663,6 +664,11 @@ This is a one-time fix-up, please be patient...
|
|
|
663
664
|
})
|
|
664
665
|
}
|
|
665
666
|
await promiseCallLimit(queue)
|
|
667
|
+
|
|
668
|
+
// have to re-calc dep flags, because the nodes don't have edges
|
|
669
|
+
// until their packages get assigned, so everything looks extraneous
|
|
670
|
+
calcDepFlags(this.idealTree)
|
|
671
|
+
|
|
666
672
|
// yes, yes, this isn't the "original" version, but now that it's been
|
|
667
673
|
// upgraded, we need to make sure we don't do the work to upgrade it
|
|
668
674
|
// again, since it's now as new as can be.
|
|
@@ -800,6 +806,7 @@ This is a one-time fix-up, please be patient...
|
|
|
800
806
|
// a virtual root of whatever brought in THIS node.
|
|
801
807
|
// so we VR the node itself if the edge is not a peer
|
|
802
808
|
const source = edge.peer ? peerSource : node
|
|
809
|
+
|
|
803
810
|
const virtualRoot = this[_virtualRoot](source, true)
|
|
804
811
|
// reuse virtual root if we already have one, but don't
|
|
805
812
|
// try to do the override ahead of time, since we MAY be able
|
|
@@ -821,13 +828,17 @@ This is a one-time fix-up, please be patient...
|
|
|
821
828
|
// +-- z@1
|
|
822
829
|
// But if x and y are loaded in the same virtual root, then they will
|
|
823
830
|
// be forced to agree on a version of z.
|
|
831
|
+
const required = new Set([edge.from])
|
|
832
|
+
const parent = edge.peer ? virtualRoot : null
|
|
824
833
|
const dep = vrDep && vrDep.satisfies(edge) ? vrDep
|
|
825
|
-
: await this[_nodeFromEdge](edge,
|
|
834
|
+
: await this[_nodeFromEdge](edge, parent, null, required)
|
|
835
|
+
|
|
826
836
|
/* istanbul ignore next */
|
|
827
837
|
debug(() => {
|
|
828
838
|
if (!dep)
|
|
829
839
|
throw new Error('no dep??')
|
|
830
840
|
})
|
|
841
|
+
|
|
831
842
|
tasks.push({edge, dep})
|
|
832
843
|
}
|
|
833
844
|
|
|
@@ -864,7 +875,7 @@ This is a one-time fix-up, please be patient...
|
|
|
864
875
|
|
|
865
876
|
// loads a node from an edge, and then loads its peer deps (and their
|
|
866
877
|
// peer deps, on down the line) into a virtual root parent.
|
|
867
|
-
async [_nodeFromEdge] (edge, parent_, secondEdge
|
|
878
|
+
async [_nodeFromEdge] (edge, parent_, secondEdge, required) {
|
|
868
879
|
// create a virtual root node with the same deps as the node that
|
|
869
880
|
// is requesting this one, so that we can get all the peer deps in
|
|
870
881
|
// a context where they're likely to be resolvable.
|
|
@@ -895,6 +906,11 @@ This is a one-time fix-up, please be patient...
|
|
|
895
906
|
// ensure the one we want is the one that's placed
|
|
896
907
|
node.parent = parent
|
|
897
908
|
|
|
909
|
+
if (required.has(edge.from) && edge.type !== 'peerOptional' ||
|
|
910
|
+
secondEdge && (
|
|
911
|
+
required.has(secondEdge.from) && secondEdge.type !== 'peerOptional'))
|
|
912
|
+
required.add(node)
|
|
913
|
+
|
|
898
914
|
// handle otherwise unresolvable dependency nesting loops by
|
|
899
915
|
// creating a symbolic link
|
|
900
916
|
// a1 -> b1 -> a2 -> b2 -> a1 -> ...
|
|
@@ -908,7 +924,7 @@ This is a one-time fix-up, please be patient...
|
|
|
908
924
|
// keep track of the thing that caused this node to be included.
|
|
909
925
|
const src = parent.sourceReference
|
|
910
926
|
this[_peerSetSource].set(node, src)
|
|
911
|
-
return this[_loadPeerSet](node)
|
|
927
|
+
return this[_loadPeerSet](node, required)
|
|
912
928
|
}
|
|
913
929
|
|
|
914
930
|
[_virtualRoot] (node, reuse = false) {
|
|
@@ -1053,7 +1069,7 @@ This is a one-time fix-up, please be patient...
|
|
|
1053
1069
|
// gets placed first. In non-strict mode, we behave strictly if the
|
|
1054
1070
|
// virtual root is based on the root project, and allow non-peer parent
|
|
1055
1071
|
// deps to override, but throw if no preference can be determined.
|
|
1056
|
-
async [_loadPeerSet] (node) {
|
|
1072
|
+
async [_loadPeerSet] (node, required) {
|
|
1057
1073
|
const peerEdges = [...node.edgesOut.values()]
|
|
1058
1074
|
// we typically only install non-optional peers, but we have to
|
|
1059
1075
|
// factor them into the peerSet so that we can avoid conflicts
|
|
@@ -1068,10 +1084,12 @@ This is a one-time fix-up, please be patient...
|
|
|
1068
1084
|
const parentEdge = node.parent.edgesOut.get(edge.name)
|
|
1069
1085
|
const {isProjectRoot, isWorkspace} = node.parent.sourceReference
|
|
1070
1086
|
const isMine = isProjectRoot || isWorkspace
|
|
1087
|
+
const conflictOK = this[_force] || !isMine && !this[_strictPeerDeps]
|
|
1088
|
+
|
|
1071
1089
|
if (!edge.to) {
|
|
1072
1090
|
if (!parentEdge) {
|
|
1073
1091
|
// easy, just put the thing there
|
|
1074
|
-
await this[_nodeFromEdge](edge, node.parent)
|
|
1092
|
+
await this[_nodeFromEdge](edge, node.parent, null, required)
|
|
1075
1093
|
continue
|
|
1076
1094
|
} else {
|
|
1077
1095
|
// if the parent's edge is very broad like >=1, and the edge in
|
|
@@ -1082,14 +1100,16 @@ This is a one-time fix-up, please be patient...
|
|
|
1082
1100
|
// a conflict. this is always a problem in strict mode, never
|
|
1083
1101
|
// in force mode, and a problem in non-strict mode if this isn't
|
|
1084
1102
|
// on behalf of our project. in all such cases, we warn at least.
|
|
1085
|
-
await this[_nodeFromEdge](parentEdge, node.parent, edge)
|
|
1103
|
+
const dep = await this[_nodeFromEdge](parentEdge, node.parent, edge, required)
|
|
1086
1104
|
|
|
1087
1105
|
// hooray! that worked!
|
|
1088
1106
|
if (edge.valid)
|
|
1089
1107
|
continue
|
|
1090
1108
|
|
|
1091
|
-
// allow it
|
|
1092
|
-
|
|
1109
|
+
// allow it. either we're overriding, or it's not something
|
|
1110
|
+
// that will be installed by default anyway, and we'll fail when
|
|
1111
|
+
// we get to the point where we need to, if we need to.
|
|
1112
|
+
if (conflictOK || !required.has(dep))
|
|
1093
1113
|
continue
|
|
1094
1114
|
|
|
1095
1115
|
// problem
|
|
@@ -1102,7 +1122,7 @@ This is a one-time fix-up, please be patient...
|
|
|
1102
1122
|
// in non-strict mode if it's not our fault. don't warn here, because
|
|
1103
1123
|
// we are going to warn again when we place the deps, if we end up
|
|
1104
1124
|
// overriding for something else.
|
|
1105
|
-
if (
|
|
1125
|
+
if (conflictOK)
|
|
1106
1126
|
continue
|
|
1107
1127
|
|
|
1108
1128
|
// ok, it's the root, or we're in unforced strict mode, so this is bad
|
|
@@ -1198,8 +1218,25 @@ This is a one-time fix-up, please be patient...
|
|
|
1198
1218
|
break
|
|
1199
1219
|
}
|
|
1200
1220
|
|
|
1201
|
-
if
|
|
1202
|
-
|
|
1221
|
+
// if we can't find a target, that means that the last placed checked
|
|
1222
|
+
// (and all the places before it) had a copy already. if we're in
|
|
1223
|
+
// --force mode, then the user has explicitly said that they're ok
|
|
1224
|
+
// with conflicts. This can only occur in --force mode in the case
|
|
1225
|
+
// when a node was added to the tree with a peerOptional dep that we
|
|
1226
|
+
// ignored, and then later, that edge became invalid, and we fail to
|
|
1227
|
+
// resolve it. We will warn about it in a moment.
|
|
1228
|
+
if (!target) {
|
|
1229
|
+
if (this[_force]) {
|
|
1230
|
+
// we know that there is a dep (not the root) which is the target
|
|
1231
|
+
// of this edge, or else it wouldn't have been a conflict.
|
|
1232
|
+
target = edge.to.resolveParent
|
|
1233
|
+
canPlace = KEEP
|
|
1234
|
+
} else
|
|
1235
|
+
this[_failPeerConflict](edge)
|
|
1236
|
+
} else {
|
|
1237
|
+
// it worked, so we clearly have no peer conflicts at this point.
|
|
1238
|
+
this[_peerConflict] = null
|
|
1239
|
+
}
|
|
1203
1240
|
|
|
1204
1241
|
this.log.silly(
|
|
1205
1242
|
'placeDep',
|
|
@@ -1210,9 +1247,6 @@ This is a one-time fix-up, please be patient...
|
|
|
1210
1247
|
`want: ${edge.spec || '*'}`
|
|
1211
1248
|
)
|
|
1212
1249
|
|
|
1213
|
-
// it worked, so we clearly have no peer conflicts at this point.
|
|
1214
|
-
this[_peerConflict] = null
|
|
1215
|
-
|
|
1216
1250
|
// Can only get KEEP here if the original edge was valid,
|
|
1217
1251
|
// and we're checking for an update but it's already up to date.
|
|
1218
1252
|
if (canPlace === KEEP) {
|
|
@@ -1398,6 +1432,7 @@ This is a one-time fix-up, please be patient...
|
|
|
1398
1432
|
})
|
|
1399
1433
|
const entryEdge = peerEntryEdge || edge
|
|
1400
1434
|
const source = this[_peerSetSource].get(dep)
|
|
1435
|
+
|
|
1401
1436
|
isSource = isSource || target === source
|
|
1402
1437
|
// if we're overriding the source, then we care if the *target* is
|
|
1403
1438
|
// ours, even if it wasn't actually the original source, since we
|
|
@@ -24,6 +24,7 @@ const loadWorkspacesVirtual = Symbol.for('loadWorkspacesVirtual')
|
|
|
24
24
|
const flagsSuspect = Symbol.for('flagsSuspect')
|
|
25
25
|
const reCalcDepFlags = Symbol('reCalcDepFlags')
|
|
26
26
|
const checkRootEdges = Symbol('checkRootEdges')
|
|
27
|
+
const rootOptionProvided = Symbol('rootOptionProvided')
|
|
27
28
|
|
|
28
29
|
const depsToEdges = (type, deps) =>
|
|
29
30
|
Object.entries(deps).map(d => [type, ...d])
|
|
@@ -63,6 +64,8 @@ module.exports = cls => class VirtualLoader extends cls {
|
|
|
63
64
|
root = await this[loadRoot](s),
|
|
64
65
|
} = options
|
|
65
66
|
|
|
67
|
+
this[rootOptionProvided] = options.root
|
|
68
|
+
|
|
66
69
|
await this[loadFromShrinkwrap](s, root)
|
|
67
70
|
return treeCheck(this.virtualTree)
|
|
68
71
|
}
|
|
@@ -74,13 +77,17 @@ module.exports = cls => class VirtualLoader extends cls {
|
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
async [loadFromShrinkwrap] (s, root) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
if (!this[rootOptionProvided]) {
|
|
81
|
+
// root is never any of these things, but might be a brand new
|
|
82
|
+
// baby Node object that never had its dep flags calculated.
|
|
83
|
+
root.extraneous = false
|
|
84
|
+
root.dev = false
|
|
85
|
+
root.optional = false
|
|
86
|
+
root.devOptional = false
|
|
87
|
+
root.peer = false
|
|
88
|
+
} else
|
|
89
|
+
this[flagsSuspect] = true
|
|
90
|
+
|
|
84
91
|
this[checkRootEdges](s, root)
|
|
85
92
|
root.meta = s
|
|
86
93
|
this.virtualTree = root
|
|
@@ -88,20 +95,23 @@ module.exports = cls => class VirtualLoader extends cls {
|
|
|
88
95
|
await this[resolveLinks](links, nodes)
|
|
89
96
|
this[assignBundles](nodes)
|
|
90
97
|
if (this[flagsSuspect])
|
|
91
|
-
this[reCalcDepFlags]()
|
|
98
|
+
this[reCalcDepFlags](nodes.values())
|
|
92
99
|
return root
|
|
93
100
|
}
|
|
94
101
|
|
|
95
|
-
[reCalcDepFlags] () {
|
|
102
|
+
[reCalcDepFlags] (nodes) {
|
|
96
103
|
// reset all dep flags
|
|
97
|
-
|
|
104
|
+
// can't use inventory here, because virtualTree might not be root
|
|
105
|
+
for (const node of nodes) {
|
|
106
|
+
if (node.isRoot || node === this[rootOptionProvided])
|
|
107
|
+
continue
|
|
98
108
|
node.extraneous = true
|
|
99
109
|
node.dev = true
|
|
100
110
|
node.optional = true
|
|
101
111
|
node.devOptional = true
|
|
102
112
|
node.peer = true
|
|
103
113
|
}
|
|
104
|
-
calcDepFlags(this.virtualTree,
|
|
114
|
+
calcDepFlags(this.virtualTree, !this[rootOptionProvided])
|
|
105
115
|
}
|
|
106
116
|
|
|
107
117
|
// check the lockfile deps, and see if they match. if they do not
|
|
@@ -237,6 +247,12 @@ module.exports = cls => class VirtualLoader extends cls {
|
|
|
237
247
|
// shrinkwrap doesn't include package name unless necessary
|
|
238
248
|
if (!sw.name)
|
|
239
249
|
sw.name = nameFromFolder(path)
|
|
250
|
+
|
|
251
|
+
const dev = sw.dev
|
|
252
|
+
const optional = sw.optional
|
|
253
|
+
const devOptional = dev || optional || sw.devOptional
|
|
254
|
+
const peer = sw.peer
|
|
255
|
+
|
|
240
256
|
const node = new Node({
|
|
241
257
|
legacyPeerDeps: this.legacyPeerDeps,
|
|
242
258
|
root: this.virtualTree,
|
|
@@ -246,6 +262,10 @@ module.exports = cls => class VirtualLoader extends cls {
|
|
|
246
262
|
resolved: consistentResolve(sw.resolved, this.path, path),
|
|
247
263
|
pkg: sw,
|
|
248
264
|
hasShrinkwrap: sw.hasShrinkwrap,
|
|
265
|
+
dev,
|
|
266
|
+
optional,
|
|
267
|
+
devOptional,
|
|
268
|
+
peer,
|
|
249
269
|
})
|
|
250
270
|
// cast to boolean because they're undefined in the lock file when false
|
|
251
271
|
node.extraneous = !!sw.extraneous
|
package/lib/arborist/reify.js
CHANGED
|
@@ -442,7 +442,8 @@ module.exports = cls => class Reifier extends cls {
|
|
|
442
442
|
if (this[_trashList].has(node.path))
|
|
443
443
|
return node
|
|
444
444
|
|
|
445
|
-
|
|
445
|
+
const timer = `reifyNode:${node.location}`
|
|
446
|
+
process.emit('time', timer)
|
|
446
447
|
this.addTracker('reify', node.name, node.location)
|
|
447
448
|
|
|
448
449
|
const p = Promise.resolve()
|
|
@@ -454,7 +455,7 @@ module.exports = cls => class Reifier extends cls {
|
|
|
454
455
|
return this[_handleOptionalFailure](node, p)
|
|
455
456
|
.then(() => {
|
|
456
457
|
this.finishTracker('reify', node.name, node.location)
|
|
457
|
-
process.emit('timeEnd',
|
|
458
|
+
process.emit('timeEnd', timer)
|
|
458
459
|
return node
|
|
459
460
|
})
|
|
460
461
|
}
|
|
@@ -474,9 +475,14 @@ module.exports = cls => class Reifier extends cls {
|
|
|
474
475
|
|
|
475
476
|
// no idea what this thing is. remove it from the tree.
|
|
476
477
|
if (!res) {
|
|
477
|
-
|
|
478
|
+
const warning = 'invalid or damaged lockfile detected\n' +
|
|
479
|
+
'please re-try this operation once it completes\n' +
|
|
480
|
+
'so that the damage can be corrected, or perform\n' +
|
|
481
|
+
'a fresh install with no lockfile if the problem persists.'
|
|
482
|
+
this.log.warn('reify', warning)
|
|
478
483
|
this.log.verbose('reify', 'unrecognized node in tree', node.path)
|
|
479
484
|
node.parent = null
|
|
485
|
+
node.fsParent = null
|
|
480
486
|
this[_addNodeToTrashList](node)
|
|
481
487
|
return
|
|
482
488
|
}
|
package/lib/calc-dep-flags.js
CHANGED
|
@@ -11,7 +11,7 @@ const calcDepFlags = (tree, resetRoot = true) => {
|
|
|
11
11
|
tree,
|
|
12
12
|
visit: node => calcDepFlagsStep(node),
|
|
13
13
|
filter: node => node,
|
|
14
|
-
getChildren: node => [...
|
|
14
|
+
getChildren: (node, tree) => [...tree.edgesOut.values()].map(edge => edge.to),
|
|
15
15
|
})
|
|
16
16
|
return ret
|
|
17
17
|
}
|