@npmcli/arborist 5.3.0 → 5.5.0

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.
@@ -484,7 +484,7 @@ Try using the package name instead, e.g:
484
484
  .catch(/* istanbul ignore next */ er => null)
485
485
  if (st && st.isSymbolicLink()) {
486
486
  const target = await readlink(dir)
487
- const real = resolve(dirname(dir), target)
487
+ const real = resolve(dirname(dir), target).replace(/#/g, '%23')
488
488
  tree.package.dependencies[name] = `file:${real}`
489
489
  } else {
490
490
  tree.package.dependencies[name] = '*'
@@ -603,7 +603,7 @@ Try using the package name instead, e.g:
603
603
  if (filepath) {
604
604
  const { name } = spec
605
605
  const tree = this.idealTree.target
606
- spec = npa(`file:${relpath(tree.path, filepath)}`, tree.path)
606
+ spec = npa(`file:${relpath(tree.path, filepath).replace(/#/g, '%23')}`, tree.path)
607
607
  spec.name = name
608
608
  }
609
609
  return spec
@@ -1076,9 +1076,8 @@ This is a one-time fix-up, please be patient...
1076
1076
  // if it fails at this point, though, dont' worry because it
1077
1077
  // may well be an optional dep that has gone missing. it'll
1078
1078
  // fail later anyway.
1079
- const from = fromPath(placed)
1080
1079
  promises.push(...this[_problemEdges](placed).map(e =>
1081
- this[_fetchManifest](npa.resolve(e.name, e.spec, from))
1080
+ this[_fetchManifest](npa.resolve(e.name, e.spec, fromPath(placed, e)))
1082
1081
  .catch(er => null)))
1083
1082
  },
1084
1083
  })
@@ -74,8 +74,12 @@ class Arborist extends Base {
74
74
  cache: options.cache || `${homedir()}/.npm/_cacache`,
75
75
  packumentCache: options.packumentCache || new Map(),
76
76
  workspacesEnabled: options.workspacesEnabled !== false,
77
+ replaceRegistryHost: options.replaceRegistryHost,
77
78
  lockfileVersion: lockfileVersion(options.lockfileVersion),
78
79
  }
80
+ this.replaceRegistryHost = this.options.replaceRegistryHost =
81
+ (!this.options.replaceRegistryHost || this.options.replaceRegistryHost === 'npmjs') ?
82
+ 'registry.npmjs.org' : this.options.replaceRegistryHost
79
83
 
80
84
  this[_workspacesEnabled] = this.options.workspacesEnabled
81
85
 
@@ -115,6 +115,7 @@ module.exports = cls => class ActualLoader extends cls {
115
115
  root = null,
116
116
  transplantFilter = () => true,
117
117
  ignoreMissing = false,
118
+ forceActual = false,
118
119
  } = options
119
120
  this[_filter] = filter
120
121
  this[_transplantFilter] = transplantFilter
@@ -141,26 +142,30 @@ module.exports = cls => class ActualLoader extends cls {
141
142
 
142
143
  this[_actualTree].assertRootOverrides()
143
144
 
144
- // Note: hidden lockfile will be rejected if it's not the latest thing
145
- // in the folder, or if any of the entries in the hidden lockfile are
146
- // missing.
147
- const meta = await Shrinkwrap.load({
148
- path: this[_actualTree].path,
149
- hiddenLockfile: true,
150
- resolveOptions: this.options,
151
- })
152
- if (meta.loadedFromDisk) {
153
- this[_actualTree].meta = meta
154
- return this[_loadActualVirtually]({ root })
155
- } else {
145
+ // if forceActual is set, don't even try the hidden lockfile
146
+ if (!forceActual) {
147
+ // Note: hidden lockfile will be rejected if it's not the latest thing
148
+ // in the folder, or if any of the entries in the hidden lockfile are
149
+ // missing.
156
150
  const meta = await Shrinkwrap.load({
157
151
  path: this[_actualTree].path,
158
- lockfileVersion: this.options.lockfileVersion,
152
+ hiddenLockfile: true,
159
153
  resolveOptions: this.options,
160
154
  })
161
- this[_actualTree].meta = meta
162
- return this[_loadActualActually]({ root, ignoreMissing })
155
+
156
+ if (meta.loadedFromDisk) {
157
+ this[_actualTree].meta = meta
158
+ return this[_loadActualVirtually]({ root })
159
+ }
163
160
  }
161
+
162
+ const meta = await Shrinkwrap.load({
163
+ path: this[_actualTree].path,
164
+ lockfileVersion: this.options.lockfileVersion,
165
+ resolveOptions: this.options,
166
+ })
167
+ this[_actualTree].meta = meta
168
+ return this[_loadActualActually]({ root, ignoreMissing })
164
169
  }
165
170
 
166
171
  async [_loadActualVirtually] ({ root }) {
@@ -196,7 +201,7 @@ module.exports = cls => class ActualLoader extends cls {
196
201
  const actualRoot = tree.isLink ? tree.target : tree
197
202
  const { dependencies = {} } = actualRoot.package
198
203
  for (const [name, kid] of actualRoot.children.entries()) {
199
- const def = kid.isLink ? `file:${kid.realpath}` : '*'
204
+ const def = kid.isLink ? `file:${kid.realpath.replace(/#/g, '%23')}` : '*'
200
205
  dependencies[name] = dependencies[name] || def
201
206
  }
202
207
  actualRoot.package = { ...actualRoot.package, dependencies }
@@ -162,7 +162,7 @@ module.exports = cls => class VirtualLoader extends cls {
162
162
  lockfile: s.data,
163
163
  })
164
164
  for (const [name, path] of workspaces.entries()) {
165
- lockWS.push(['workspace', name, `file:${path}`])
165
+ lockWS.push(['workspace', name, `file:${path.replace(/#/g, '%23')}`])
166
166
  }
167
167
 
168
168
  const lockEdges = [
@@ -712,13 +712,19 @@ module.exports = cls => class Reifier extends cls {
712
712
  [_registryResolved] (resolved) {
713
713
  // the default registry url is a magic value meaning "the currently
714
714
  // configured registry".
715
+ // `resolved` must never be falsey.
715
716
  //
716
717
  // XXX: use a magic string that isn't also a valid value, like
717
718
  // ${REGISTRY} or something. This has to be threaded through the
718
719
  // Shrinkwrap and Node classes carefully, so for now, just treat
719
720
  // the default reg as the magical animal that it has been.
720
- return resolved && resolved
721
- .replace(/^https?:\/\/registry\.npmjs\.org\//, this.registry)
721
+ const resolvedURL = new URL(resolved)
722
+ if ((this.options.replaceRegistryHost === resolvedURL.hostname)
723
+ || this.options.replaceRegistryHost === 'always') {
724
+ // this.registry always has a trailing slash
725
+ resolved = `${this.registry.slice(0, -1)}${resolvedURL.pathname}${resolvedURL.searchParams}`
726
+ }
727
+ return resolved
722
728
  }
723
729
 
724
730
  // bundles are *sort of* like shrinkwraps, in that the branch is defined
@@ -1241,7 +1247,7 @@ module.exports = cls => class Reifier extends cls {
1241
1247
  // path initially, in which case we can end up with the wrong
1242
1248
  // thing, so just get the ultimate fetchSpec and relativize it.
1243
1249
  const p = req.fetchSpec.replace(/^file:/, '')
1244
- const rel = relpath(addTree.realpath, p)
1250
+ const rel = relpath(addTree.realpath, p).replace(/#/g, '%23')
1245
1251
  newSpec = `file:${rel}`
1246
1252
  }
1247
1253
  } else {
@@ -20,8 +20,8 @@ const consistentResolve = (resolved, fromPath, toPath, relPaths = false) => {
20
20
  raw,
21
21
  } = npa(resolved, fromPath)
22
22
  const isPath = type === 'file' || type === 'directory'
23
- return isPath && !relPaths ? `file:${fetchSpec}`
24
- : isPath ? 'file:' + (toPath ? relpath(toPath, fetchSpec) : fetchSpec)
23
+ return isPath && !relPaths ? `file:${fetchSpec.replace(/#/g, '%23')}`
24
+ : isPath ? 'file:' + (toPath ? relpath(toPath, fetchSpec.replace(/#/g, '%23')) : fetchSpec.replace(/#/g, '%23'))
25
25
  : hosted ? `git+${
26
26
  hosted.auth ? hosted.https(hostedOpt) : hosted.sshurl(hostedOpt)
27
27
  }`
package/lib/dep-valid.js CHANGED
@@ -20,7 +20,7 @@ const depValid = (child, requested, requestor) => {
20
20
  // file: deps that depend on other files/dirs, we must resolve the
21
21
  // location based on the *requestor* file/dir, not where it ends up.
22
22
  // '' is equivalent to '*'
23
- requested = npa.resolve(child.name, requested || '*', fromPath(requestor))
23
+ requested = npa.resolve(child.name, requested || '*', fromPath(requestor, requestor.edgesOut.get(child.name)))
24
24
  } catch (er) {
25
25
  // Not invalid because the child doesn't match, but because
26
26
  // the spec itself is not supported. Nothing would match,
package/lib/edge.js CHANGED
@@ -169,7 +169,11 @@ class Edge {
169
169
  if (this.overrides && this.overrides.value && this.overrides.name === this.name) {
170
170
  if (this.overrides.value.startsWith('$')) {
171
171
  const ref = this.overrides.value.slice(1)
172
- const pkg = this.from.root.package
172
+ // we may be a virtual root, if we are we want to resolve reference overrides
173
+ // from the real root, not the virtual one
174
+ const pkg = this.from.sourceReference
175
+ ? this.from.sourceReference.root.package
176
+ : this.from.root.package
173
177
  const overrideSpec = (pkg.devDependencies && pkg.devDependencies[ref]) ||
174
178
  (pkg.optionalDependencies && pkg.optionalDependencies[ref]) ||
175
179
  (pkg.dependencies && pkg.dependencies[ref]) ||
package/lib/from-path.js CHANGED
@@ -6,8 +6,19 @@
6
6
  const { dirname } = require('path')
7
7
  const npa = require('npm-package-arg')
8
8
 
9
- const fromPath = (node, spec) =>
10
- spec && spec.type === 'file' ? dirname(spec.fetchSpec)
11
- : node.realpath
9
+ const fromPath = (node, spec, edge) => {
10
+ if (edge && edge.overrides && edge.overrides.name === edge.name && edge.overrides.value) {
11
+ // fromPath could be called with a node that has a virtual root, if that happens
12
+ // we want to make sure we get the real root node when overrides are in use. this
13
+ // is to allow things like overriding a dependency with a tarball file that's a
14
+ // relative path from the project root
15
+ return node.sourceReference
16
+ ? node.sourceReference.root.realpath
17
+ : node.root.realpath
18
+ }
12
19
 
13
- module.exports = node => fromPath(node, node.resolved && npa(node.resolved))
20
+ return spec && spec.type === 'file' ? dirname(spec.fetchSpec)
21
+ : node.realpath
22
+ }
23
+
24
+ module.exports = (node, edge) => fromPath(node, node.resolved && npa(node.resolved), edge)
package/lib/link.js CHANGED
@@ -118,7 +118,7 @@ class Link extends Node {
118
118
  // the path/realpath guard is there for the benefit of setting
119
119
  // these things in the "wrong" order
120
120
  return this.path && this.realpath
121
- ? `file:${relpath(dirname(this.path), this.realpath)}`
121
+ ? `file:${relpath(dirname(this.path), this.realpath).replace(/#/g, '%23')}`
122
122
  : null
123
123
  }
124
124
 
package/lib/node.js CHANGED
@@ -69,6 +69,8 @@ const consistentResolve = require('./consistent-resolve.js')
69
69
  const printableTree = require('./printable.js')
70
70
  const CaseInsensitiveMap = require('./case-insensitive-map.js')
71
71
 
72
+ const querySelectorAll = require('./query-selector-all.js')
73
+
72
74
  class Node {
73
75
  constructor (options) {
74
76
  // NB: path can be null if it's a link target
@@ -824,7 +826,7 @@ class Node {
824
826
  }
825
827
 
826
828
  for (const [name, path] of this[_workspaces].entries()) {
827
- new Edge({ from: this, name, spec: `file:${path}`, type: 'workspace' })
829
+ new Edge({ from: this, name, spec: `file:${path.replace(/#/g, '%23')}`, type: 'workspace' })
828
830
  }
829
831
  }
830
832
 
@@ -1446,6 +1448,12 @@ class Node {
1446
1448
  return base === name && basename(nm) === 'node_modules' ? dir : false
1447
1449
  }
1448
1450
 
1451
+ // maybe accept both string value or array of strings
1452
+ // seems to be what dom API does
1453
+ querySelectorAll (query) {
1454
+ return querySelectorAll(this, query)
1455
+ }
1456
+
1449
1457
  toJSON () {
1450
1458
  return printableTree(this)
1451
1459
  }
@@ -0,0 +1,561 @@
1
+ 'use strict'
2
+
3
+ const { resolve } = require('path')
4
+ const { parser, arrayDelimiter } = require('@npmcli/query')
5
+ const localeCompare = require('@isaacs/string-locale-compare')('en')
6
+ const npa = require('npm-package-arg')
7
+ const minimatch = require('minimatch')
8
+ const semver = require('semver')
9
+
10
+ // handle results for parsed query asts, results are stored in a map that has a
11
+ // key that points to each ast selector node and stores the resulting array of
12
+ // arborist nodes as its value, that is essential to how we handle multiple
13
+ // query selectors, e.g: `#a, #b, #c` <- 3 diff ast selector nodes
14
+ class Results {
15
+ #currentAstSelector
16
+ #initialItems
17
+ #inventory
18
+ #pendingCombinator
19
+ #results = new Map()
20
+ #targetNode
21
+
22
+ constructor (opts) {
23
+ this.#currentAstSelector = opts.rootAstNode.nodes[0]
24
+ this.#inventory = opts.inventory
25
+ this.#initialItems = opts.initialItems
26
+ this.#targetNode = opts.targetNode
27
+
28
+ this.currentResults = this.#initialItems
29
+
30
+ // reset by rootAstNode walker
31
+ this.currentAstNode = opts.rootAstNode
32
+ }
33
+
34
+ get currentResults () {
35
+ return this.#results.get(this.#currentAstSelector)
36
+ }
37
+
38
+ set currentResults (value) {
39
+ this.#results.set(this.#currentAstSelector, value)
40
+ }
41
+
42
+ // retrieves the initial items to which start the filtering / matching
43
+ // for most of the different types of recognized ast nodes, e.g: class (aka
44
+ // depType), id, *, etc in different contexts we need to start with the
45
+ // current list of filtered results, for example a query for `.workspace`
46
+ // actually means the same as `*.workspace` so we want to start with the full
47
+ // inventory if that's the first ast node we're reading but if it appears in
48
+ // the middle of a query it should respect the previous filtered results,
49
+ // combinators are a special case in which we always want to have the
50
+ // complete inventory list in order to use the left-hand side ast node as a
51
+ // filter combined with the element on its right-hand side
52
+ get initialItems () {
53
+ const firstParsed =
54
+ (this.currentAstNode.parent.nodes[0] === this.currentAstNode) &&
55
+ (this.currentAstNode.parent.parent.type === 'root')
56
+
57
+ if (firstParsed) {
58
+ return this.#initialItems
59
+ }
60
+ if (this.currentAstNode.prev().type === 'combinator') {
61
+ return this.#inventory
62
+ }
63
+ return this.currentResults
64
+ }
65
+
66
+ // combinators need information about previously filtered items along
67
+ // with info of the items parsed / retrieved from the selector right
68
+ // past the combinator, for this reason combinators are stored and
69
+ // only ran as the last part of each selector logic
70
+ processPendingCombinator (nextResults) {
71
+ if (this.#pendingCombinator) {
72
+ const res = this.#pendingCombinator(this.currentResults, nextResults)
73
+ this.#pendingCombinator = null
74
+ this.currentResults = res
75
+ } else {
76
+ this.currentResults = nextResults
77
+ }
78
+ }
79
+
80
+ // when collecting results to a root astNode, we traverse the list of child
81
+ // selector nodes and collect all of their resulting arborist nodes into a
82
+ // single/flat Set of items, this ensures we also deduplicate items
83
+ collect (rootAstNode) {
84
+ return new Set(rootAstNode.nodes.flatMap(n => this.#results.get(n)))
85
+ }
86
+
87
+ // selector types map to the '.type' property of the ast nodes via `${astNode.type}Type`
88
+ //
89
+ // attribute selector [name=value], etc
90
+ attributeType () {
91
+ const nextResults = this.initialItems.filter(node =>
92
+ attributeMatch(this.currentAstNode, node.package)
93
+ )
94
+ this.processPendingCombinator(nextResults)
95
+ }
96
+
97
+ // dependency type selector (i.e. .prod, .dev, etc)
98
+ // css calls this class, we interpret is as dependency type
99
+ classType () {
100
+ const depTypeFn = depTypes[String(this.currentAstNode)]
101
+ if (!depTypeFn) {
102
+ throw Object.assign(
103
+ new Error(`\`${String(this.currentAstNode)}\` is not a supported dependency type.`),
104
+ { code: 'EQUERYNODEPTYPE' }
105
+ )
106
+ }
107
+ const nextResults = depTypeFn(this.initialItems)
108
+ this.processPendingCombinator(nextResults)
109
+ }
110
+
111
+ // combinators (i.e. '>', ' ', '~')
112
+ combinatorType () {
113
+ this.#pendingCombinator = combinators[String(this.currentAstNode)]
114
+ }
115
+
116
+ // name selectors (i.e. #foo, #foo@1.0.0)
117
+ // css calls this id, we interpret it as name
118
+ idType () {
119
+ const spec = npa(this.currentAstNode.value)
120
+ const nextResults = this.initialItems.filter(node =>
121
+ (node.name === spec.name || node.package.name === spec.name) &&
122
+ (semver.satisfies(node.version, spec.fetchSpec) || !spec.rawSpec))
123
+ this.processPendingCombinator(nextResults)
124
+ }
125
+
126
+ // pseudo selectors (prefixed with :)
127
+ pseudoType () {
128
+ const pseudoFn = `${this.currentAstNode.value.slice(1)}Pseudo`
129
+ if (!this[pseudoFn]) {
130
+ throw Object.assign(
131
+ new Error(`\`${this.currentAstNode.value
132
+ }\` is not a supported pseudo selector.`),
133
+ { code: 'EQUERYNOPSEUDO' }
134
+ )
135
+ }
136
+ const nextResults = this[pseudoFn]()
137
+ this.processPendingCombinator(nextResults)
138
+ }
139
+
140
+ selectorType () {
141
+ this.#currentAstSelector = this.currentAstNode
142
+ // starts a new array in which resulting items
143
+ // can be stored for each given ast selector
144
+ if (!this.currentResults) {
145
+ this.currentResults = []
146
+ }
147
+ }
148
+
149
+ universalType () {
150
+ this.processPendingCombinator(this.initialItems)
151
+ }
152
+
153
+ // pseudo selectors map to the 'value' property of the pseudo selectors in the ast nodes
154
+ // via selectors via `${value.slice(1)}Pseudo`
155
+ attrPseudo () {
156
+ const { lookupProperties, attributeMatcher } = this.currentAstNode
157
+
158
+ return this.initialItems.filter(node => {
159
+ let objs = [node.package]
160
+ for (const prop of lookupProperties) {
161
+ // if an isArray symbol is found that means we'll need to iterate
162
+ // over the previous found array to basically make sure we traverse
163
+ // all its indexes testing for possible objects that may eventually
164
+ // hold more keys specified in a selector
165
+ if (prop === arrayDelimiter) {
166
+ objs = objs.flat()
167
+ continue
168
+ }
169
+
170
+ // otherwise just maps all currently found objs
171
+ // to the next prop from the lookup properties list,
172
+ // filters out any empty key lookup
173
+ objs = objs.flatMap(obj => obj[prop] || [])
174
+
175
+ // in case there's no property found in the lookup
176
+ // just filters that item out
177
+ const noAttr = objs.every(obj => !obj)
178
+ if (noAttr) {
179
+ return false
180
+ }
181
+ }
182
+
183
+ // if any of the potential object matches
184
+ // that item should be in the final result
185
+ return objs.some(obj => attributeMatch(attributeMatcher, obj))
186
+ })
187
+ }
188
+
189
+ emptyPseudo () {
190
+ return this.initialItems.filter(node => node.edgesOut.size === 0)
191
+ }
192
+
193
+ extraneousPseudo () {
194
+ return this.initialItems.filter(node => node.extraneous)
195
+ }
196
+
197
+ hasPseudo () {
198
+ const found = []
199
+ for (const item of this.initialItems) {
200
+ const res = retrieveNodesFromParsedAst({
201
+ // This is the one time initialItems differs from inventory
202
+ initialItems: [item],
203
+ inventory: this.#inventory,
204
+ rootAstNode: this.currentAstNode.nestedNode,
205
+ targetNode: item,
206
+ })
207
+ if (res.size > 0) {
208
+ found.push(item)
209
+ }
210
+ }
211
+ return found
212
+ }
213
+
214
+ invalidPseudo () {
215
+ const found = []
216
+ for (const node of this.initialItems) {
217
+ for (const edge of node.edgesIn) {
218
+ if (edge.invalid) {
219
+ found.push(node)
220
+ break
221
+ }
222
+ }
223
+ }
224
+ return found
225
+ }
226
+
227
+ isPseudo () {
228
+ const res = retrieveNodesFromParsedAst({
229
+ initialItems: this.initialItems,
230
+ inventory: this.#inventory,
231
+ rootAstNode: this.currentAstNode.nestedNode,
232
+ targetNode: this.currentAstNode,
233
+ })
234
+ return [...res]
235
+ }
236
+
237
+ linkPseudo () {
238
+ return this.initialItems.filter(node => node.isLink || (node.isTop && !node.isRoot))
239
+ }
240
+
241
+ missingPseudo () {
242
+ return this.#inventory.reduce((res, node) => {
243
+ for (const edge of node.edgesOut.values()) {
244
+ if (edge.missing) {
245
+ const pkg = { name: edge.name, version: edge.spec }
246
+ res.push(new this.#targetNode.constructor({ pkg }))
247
+ }
248
+ }
249
+ return res
250
+ }, [])
251
+ }
252
+
253
+ notPseudo () {
254
+ const res = retrieveNodesFromParsedAst({
255
+ initialItems: this.initialItems,
256
+ inventory: this.#inventory,
257
+ rootAstNode: this.currentAstNode.nestedNode,
258
+ targetNode: this.currentAstNode,
259
+ })
260
+ const internalSelector = new Set(res)
261
+ return this.initialItems.filter(node =>
262
+ !internalSelector.has(node))
263
+ }
264
+
265
+ pathPseudo () {
266
+ return this.initialItems.filter(node => {
267
+ if (!this.currentAstNode.pathValue) {
268
+ return true
269
+ }
270
+ return minimatch(
271
+ node.realpath.replace(/\\+/g, '/'),
272
+ resolve(node.root.realpath, this.currentAstNode.pathValue).replace(/\\+/g, '/')
273
+ )
274
+ })
275
+ }
276
+
277
+ privatePseudo () {
278
+ return this.initialItems.filter(node => node.package.private)
279
+ }
280
+
281
+ rootPseudo () {
282
+ return this.initialItems.filter(node => node === this.#targetNode.root)
283
+ }
284
+
285
+ scopePseudo () {
286
+ return this.initialItems.filter(node => node === this.#targetNode)
287
+ }
288
+
289
+ semverPseudo () {
290
+ if (!this.currentAstNode.semverValue) {
291
+ return this.initialItems
292
+ }
293
+ return this.initialItems.filter(node =>
294
+ semver.satisfies(node.version, this.currentAstNode.semverValue))
295
+ }
296
+
297
+ typePseudo () {
298
+ if (!this.currentAstNode.typeValue) {
299
+ return this.initialItems
300
+ }
301
+ return this.initialItems
302
+ .flatMap(node => {
303
+ const found = []
304
+ for (const edge of node.edgesIn) {
305
+ if (npa(`${edge.name}@${edge.spec}`).type === this.currentAstNode.typeValue) {
306
+ found.push(edge.to)
307
+ }
308
+ }
309
+ return found
310
+ })
311
+ }
312
+
313
+ dedupedPseudo () {
314
+ return this.initialItems.filter(node => node.target.edgesIn.size > 1)
315
+ }
316
+ }
317
+
318
+ // operators for attribute selectors
319
+ const attributeOperators = {
320
+ // attribute value is equivalent
321
+ '=' ({ attr, value, insensitive }) {
322
+ return attr === value
323
+ },
324
+ // attribute value contains word
325
+ '~=' ({ attr, value, insensitive }) {
326
+ return (attr.match(/\w+/g) || []).includes(value)
327
+ },
328
+ // attribute value contains string
329
+ '*=' ({ attr, value, insensitive }) {
330
+ return attr.includes(value)
331
+ },
332
+ // attribute value is equal or starts with
333
+ '|=' ({ attr, value, insensitive }) {
334
+ return attr.startsWith(`${value}-`)
335
+ },
336
+ // attribute value starts with
337
+ '^=' ({ attr, value, insensitive }) {
338
+ return attr.startsWith(value)
339
+ },
340
+ // attribute value ends with
341
+ '$=' ({ attr, value, insensitive }) {
342
+ return attr.endsWith(value)
343
+ },
344
+ }
345
+
346
+ const attributeOperator = ({ attr, value, insensitive, operator }) => {
347
+ if (typeof attr === 'number') {
348
+ attr = String(attr)
349
+ }
350
+ if (typeof attr !== 'string') {
351
+ // It's an object or an array, bail
352
+ return false
353
+ }
354
+ if (insensitive) {
355
+ attr = attr.toLowerCase()
356
+ }
357
+ return attributeOperators[operator]({
358
+ attr,
359
+ insensitive,
360
+ value,
361
+ })
362
+ }
363
+
364
+ const attributeMatch = (matcher, obj) => {
365
+ const insensitive = !!matcher.insensitive
366
+ const operator = matcher.operator || ''
367
+ const attribute = matcher.qualifiedAttribute
368
+ let value = matcher.value || ''
369
+ // return early if checking existence
370
+ if (operator === '') {
371
+ return Boolean(obj[attribute])
372
+ }
373
+ if (insensitive) {
374
+ value = value.toLowerCase()
375
+ }
376
+ // in case the current object is an array
377
+ // then we try to match every item in the array
378
+ if (Array.isArray(obj[attribute])) {
379
+ return obj[attribute].find((i, index) => {
380
+ const attr = obj[attribute][index] || ''
381
+ return attributeOperator({ attr, value, insensitive, operator })
382
+ })
383
+ } else {
384
+ const attr = obj[attribute] || ''
385
+ return attributeOperator({ attr, value, insensitive, operator })
386
+ }
387
+ }
388
+
389
+ const edgeIsType = (node, type, seen = new Set()) => {
390
+ for (const edgeIn of node.edgesIn) {
391
+ // TODO Need a test with an infinite loop
392
+ if (seen.has(edgeIn)) {
393
+ continue
394
+ }
395
+ seen.add(edgeIn)
396
+ if (edgeIn.type === type || edgeIn.from[type] || edgeIsType(edgeIn.from, type, seen)) {
397
+ return true
398
+ }
399
+ }
400
+ return false
401
+ }
402
+
403
+ const filterByType = (nodes, type) => {
404
+ const found = []
405
+ for (const node of nodes) {
406
+ if (node[type] || edgeIsType(node, type)) {
407
+ found.push(node)
408
+ }
409
+ }
410
+ return found
411
+ }
412
+
413
+ const depTypes = {
414
+ // dependency
415
+ '.prod' (prevResults) {
416
+ const found = []
417
+ for (const node of prevResults) {
418
+ if (!node.dev) {
419
+ found.push(node)
420
+ }
421
+ }
422
+ return found
423
+ },
424
+ // devDependency
425
+ '.dev' (prevResults) {
426
+ return filterByType(prevResults, 'dev')
427
+ },
428
+ // optionalDependency
429
+ '.optional' (prevResults) {
430
+ return filterByType(prevResults, 'optional')
431
+ },
432
+ // peerDependency
433
+ '.peer' (prevResults) {
434
+ return filterByType(prevResults, 'peer')
435
+ },
436
+ // workspace
437
+ '.workspace' (prevResults) {
438
+ return prevResults.filter(node => node.isWorkspace)
439
+ },
440
+ // bundledDependency
441
+ '.bundled' (prevResults) {
442
+ return prevResults.filter(node => node.inBundle)
443
+ },
444
+ }
445
+
446
+ // checks if a given node has a direct parent in any of the nodes provided in
447
+ // the compare nodes array
448
+ const hasParent = (node, compareNodes) => {
449
+ // All it takes is one so we loop and return on the first hit
450
+ for (const compareNode of compareNodes) {
451
+ // follows logical parent for link anscestors
452
+ if (node.isTop && (node.resolveParent === compareNode)) {
453
+ return true
454
+ }
455
+ // follows edges-in to check if they match a possible parent
456
+ for (const edge of node.edgesIn) {
457
+ if (edge && edge.from === compareNode) {
458
+ return true
459
+ }
460
+ }
461
+ }
462
+ return false
463
+ }
464
+
465
+ // checks if a given node is a descendant of any of the nodes provided in the
466
+ // compareNodes array
467
+ const hasAscendant = (node, compareNodes, seen = new Set()) => {
468
+ // TODO (future) loop over ancestry property
469
+ if (hasParent(node, compareNodes)) {
470
+ return true
471
+ }
472
+
473
+ if (node.isTop && node.resolveParent) {
474
+ return hasAscendant(node.resolveParent, compareNodes)
475
+ }
476
+ for (const edge of node.edgesIn) {
477
+ // TODO Need a test with an infinite loop
478
+ if (seen.has(edge)) {
479
+ continue
480
+ }
481
+ seen.add(edge)
482
+ if (edge && edge.from && hasAscendant(edge.from, compareNodes, seen)) {
483
+ return true
484
+ }
485
+ }
486
+ return false
487
+ }
488
+
489
+ const combinators = {
490
+ // direct descendant
491
+ '>' (prevResults, nextResults) {
492
+ return nextResults.filter(node => hasParent(node, prevResults))
493
+ },
494
+ // any descendant
495
+ ' ' (prevResults, nextResults) {
496
+ return nextResults.filter(node => hasAscendant(node, prevResults))
497
+ },
498
+ // sibling
499
+ '~' (prevResults, nextResults) {
500
+ // Return any node in nextResults that is a sibling of (aka shares a
501
+ // parent with) a node in prevResults
502
+ const parentNodes = new Set() // Parents of everything in prevResults
503
+ for (const node of prevResults) {
504
+ for (const edge of node.edgesIn) {
505
+ // edge.from always exists cause it's from another node's edgesIn
506
+ parentNodes.add(edge.from)
507
+ }
508
+ }
509
+ return nextResults.filter(node =>
510
+ !prevResults.includes(node) && hasParent(node, [...parentNodes])
511
+ )
512
+ },
513
+ }
514
+
515
+ const retrieveNodesFromParsedAst = (opts) => {
516
+ // when we first call this it's the parsed query. all other times it's
517
+ // results.currentNode.nestedNode
518
+ const rootAstNode = opts.rootAstNode
519
+
520
+ if (!rootAstNode.nodes) {
521
+ return new Set()
522
+ }
523
+
524
+ const results = new Results(opts)
525
+
526
+ rootAstNode.walk((nextAstNode) => {
527
+ // This is the only place we reset currentAstNode
528
+ results.currentAstNode = nextAstNode
529
+ const updateFn = `${results.currentAstNode.type}Type`
530
+ if (typeof results[updateFn] !== 'function') {
531
+ throw Object.assign(
532
+ new Error(`\`${results.currentAstNode.type}\` is not a supported selector.`),
533
+ { code: 'EQUERYNOSELECTOR' }
534
+ )
535
+ }
536
+ results[updateFn]()
537
+ })
538
+
539
+ return results.collect(rootAstNode)
540
+ }
541
+
542
+ // We are keeping this async in the event that we do add async operators, we
543
+ // won't have to have a breaking change on this function signature.
544
+ const querySelectorAll = async (targetNode, query) => {
545
+ // This never changes ever we just pass it around. But we can't scope it to
546
+ // this whole file if we ever want to support concurrent calls to this
547
+ // function.
548
+ const inventory = [...targetNode.root.inventory.values()]
549
+ // res is a Set of items returned for each parsed css ast selector
550
+ const res = retrieveNodesFromParsedAst({
551
+ initialItems: inventory,
552
+ inventory,
553
+ rootAstNode: parser(query),
554
+ targetNode,
555
+ })
556
+
557
+ // returns nodes ordered by realpath
558
+ return [...res].sort((a, b) => localeCompare(a.location, b.location))
559
+ }
560
+
561
+ module.exports = querySelectorAll
package/lib/shrinkwrap.js CHANGED
@@ -815,7 +815,7 @@ class Shrinkwrap {
815
815
  const pathFixed = !resolved ? null
816
816
  : !/^file:/.test(resolved) ? resolved
817
817
  // resolve onto the metadata path
818
- : `file:${resolve(this.path, resolved.slice(5))}`
818
+ : `file:${resolve(this.path, resolved.slice(5)).replace(/#/g, '%23')}`
819
819
 
820
820
  // if we have one, only set the other if it matches
821
821
  // otherwise it could be for a completely different thing.
@@ -996,7 +996,7 @@ class Shrinkwrap {
996
996
  : npa.resolve(node.name, edge.spec, edge.from.realpath)
997
997
 
998
998
  if (node.isLink) {
999
- lock.version = `file:${relpath(this.path, node.realpath)}`
999
+ lock.version = `file:${relpath(this.path, node.realpath).replace(/#/g, '%23')}`
1000
1000
  } else if (spec && (spec.type === 'file' || spec.type === 'remote')) {
1001
1001
  lock.version = spec.saveSpec
1002
1002
  } else if (spec && spec.type === 'git' || rSpec.type === 'git') {
@@ -1074,7 +1074,7 @@ class Shrinkwrap {
1074
1074
  // this especially shows up with workspace edges when the root
1075
1075
  // node is also a workspace in the set.
1076
1076
  const p = resolve(node.realpath, spec.slice('file:'.length))
1077
- set[k] = `file:${relpath(node.realpath, p)}`
1077
+ set[k] = `file:${relpath(node.realpath, p).replace(/#/g, '%23')}`
1078
1078
  } else {
1079
1079
  set[k] = spec
1080
1080
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/arborist",
3
- "version": "5.3.0",
3
+ "version": "5.5.0",
4
4
  "description": "Manage node_modules trees",
5
5
  "dependencies": {
6
6
  "@isaacs/string-locale-compare": "^1.1.0",
@@ -11,15 +11,17 @@
11
11
  "@npmcli/name-from-folder": "^1.0.1",
12
12
  "@npmcli/node-gyp": "^2.0.0",
13
13
  "@npmcli/package-json": "^2.0.0",
14
+ "@npmcli/query": "^1.1.1",
14
15
  "@npmcli/run-script": "^4.1.3",
15
16
  "bin-links": "^3.0.0",
16
17
  "cacache": "^16.0.6",
17
18
  "common-ancestor-path": "^1.0.1",
18
19
  "json-parse-even-better-errors": "^2.3.1",
19
20
  "json-stringify-nice": "^1.1.4",
21
+ "minimatch": "^5.1.0",
20
22
  "mkdirp": "^1.0.4",
21
23
  "mkdirp-infer-owner": "^2.0.0",
22
- "nopt": "^5.0.0",
24
+ "nopt": "^6.0.0",
23
25
  "npm-install-checks": "^5.0.0",
24
26
  "npm-package-arg": "^9.0.0",
25
27
  "npm-pick-manifest": "^7.0.0",