Skip to content

Commit 0345c40

Browse files
committed
fix: attribute selectors, workspace root, cleanup
Giant refactor: - adds case insensitivity to attribute selectors - fixes --include-workspace-root - fixes -w results - docs updates - consolidating state into the `results` object and passing that to the functions that the ast walker functions use. - optimizing and refactoring other loops - code consolidation and consistency between two different attribute selectors - Un-asyncify functions that don't do async operators. We leave the exported fn async so we can add some in the future. - lots of other minor tweaks/cleanups
1 parent 3d043f3 commit 0345c40

File tree

11 files changed

+655
-539
lines changed

11 files changed

+655
-539
lines changed

docs/content/commands/npm-query.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ description: Dependency selector query
1111
<!-- see lib/commands/query.js -->
1212

1313
```bash
14-
npm query <value>
14+
npm query <selector>
1515
```
1616

1717
<!-- automatically generated, do not edit manually -->

docs/content/using-npm/dependency-selectors.md

+19-6
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,29 @@ The `npm query` commmand exposes a new dependency selector syntax (informed by &
2020
- there is no "type" or "tag" selectors (ex. `div, h1, a`) as a dependency/target is the only type of `Node` that can be queried
2121
- the term "dependencies" is in reference to any `Node` found in a `tree` returned by `Arborist`
2222

23+
#### Combinators
24+
25+
- `>` direct descendant/child
26+
- ` ` any descendant/child
27+
- `~` sibling
28+
2329
#### Selectors
2430

2531
- `*` universal selector
2632
- `#<name>` dependency selector (equivalent to `[name="..."]`)
2733
- `#<name>@<version>` (equivalent to `[name=<name>]:semver(<version>)`)
2834
- `,` selector list delimiter
29-
- `.` class selector
30-
- `:` pseudo class selector
31-
- `>` direct decendent/child selector
32-
- `~` sibling selector
35+
- `.` dependency type selector
36+
- `:` pseudo selector
37+
38+
#### Dependency Type Selectors
39+
40+
- `.prod` dependency found in the `dependencies` section of `package.json`, or is a child of said dependency
41+
- `.dev` dependency found in the `devDependencies` section of `package.json`, or is a child of said dependency
42+
- `.optional` dependency found in the `optionalDependencies` section of `package.json`, or has `"optional": true` set in its entry in the `peerDependenciesMeta` section of `package.json`, or a child of said dependency
43+
- `.peer` dependency found in the `peerDependencies` section of `package.json`
44+
- `.workspace` dependency found in the `workspaces` section of `package.json`
45+
- `.bundled` dependency found in the `bundleDependencies` section of `package.json`, or is a child of said dependency
3346

3447
#### Pseudo Selectors
3548
- [`:not(<selector>)`](https://developer.mozilla.org/en-US/docs/Web/CSS/:not)
@@ -58,7 +71,7 @@ The attribute selector evaluates the key/value pairs in `package.json` if they a
5871
- `[attribute~=value]` attribute value contains word...
5972
- `[attribute*=value]` attribute value contains string...
6073
- `[attribute|=value]` attribute value is equal to or starts with...
61-
- `[attribute^=value]` attribute value begins with...
74+
- `[attribute^=value]` attribute value starts with...
6275
- `[attribute$=value]` attribute value ends with...
6376

6477
#### `Array` & `Object` Attribute Selectors
@@ -72,7 +85,7 @@ The generic `:attr()` pseudo selector standardizes a pattern which can be used f
7285
*:attr(scripts, [test~=tap])
7386
```
7487

75-
#### Nested `Objects`
88+
#### Nested `Objects`
7689

7790
Nested objects are expressed as sequential arguments to `:attr()`.
7891

lib/commands/query.js

+47-32
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,30 @@
33
const { resolve } = require('path')
44
const Arborist = require('@npmcli/arborist')
55
const BaseCommand = require('../base-command.js')
6-
const QuerySelectorAllResponse = require('../utils/query-selector-all-response.js')
76

8-
// retrieves a normalized inventory
9-
const convertInventoryItemsToResponses = inventory => {
10-
const responses = []
11-
const responsesSeen = new Set()
12-
for (const node of inventory) {
13-
if (!responsesSeen.has(node.target.realpath)) {
14-
const item = new QuerySelectorAllResponse(node)
15-
responses.push(item)
16-
responsesSeen.add(item.path)
17-
}
7+
class QuerySelectorItem {
8+
constructor (node) {
9+
// all enumerable properties from the target
10+
Object.assign(this, node.target.package)
11+
12+
// append extra info
13+
this.pkgid = node.target.pkgid
14+
this.location = node.target.location
15+
this.path = node.target.path
16+
this.realpath = node.target.realpath
17+
this.resolved = node.target.resolved
18+
this.isLink = node.target.isLink
19+
this.isWorkspace = node.target.isWorkspace
1820
}
19-
return responses
2021
}
2122

2223
class Query extends BaseCommand {
24+
#response = [] // response is the query response
25+
#seen = new Set() // paths we've seen so we can keep response deduped
26+
2327
static description = 'Retrieve a filtered list of packages'
2428
static name = 'query'
25-
static usage = [
26-
'<value>',
27-
]
29+
static usage = ['<selector>']
2830

2931
static ignoreImplicitWorkspace = false
3032

@@ -35,43 +37,56 @@ class Query extends BaseCommand {
3537
'include-workspace-root',
3638
]
3739

38-
async exec (args, workspaces) {
39-
const globalTop = resolve(this.npm.globalDir, '..')
40-
const where = this.npm.config.get('global') ? globalTop : this.npm.prefix
40+
get parsedResponse () {
41+
return JSON.stringify(this.#response, null, 2)
42+
}
43+
44+
async exec (args) {
45+
// one dir up from wherever node_modules lives
46+
const where = resolve(this.npm.dir, '..')
4147
const opts = {
4248
...this.npm.flatOptions,
4349
path: where,
4450
}
4551
const arb = new Arborist(opts)
4652
const tree = await arb.loadActual(opts)
4753
const items = await tree.querySelectorAll(args[0])
48-
const res =
49-
JSON.stringify(convertInventoryItemsToResponses(items), null, 2)
54+
this.buildResponse(items)
5055

51-
return this.npm.output(res)
56+
this.npm.output(this.parsedResponse)
5257
}
5358

5459
async execWorkspaces (args, filters) {
5560
await this.setWorkspaces(filters)
56-
const result = new Set()
5761
const opts = {
5862
...this.npm.flatOptions,
5963
path: this.npm.prefix,
6064
}
6165
const arb = new Arborist(opts)
6266
const tree = await arb.loadActual(opts)
63-
for (const [, workspacePath] of this.workspaces.entries()) {
64-
this.prefix = workspacePath
65-
const [workspace] = await tree.querySelectorAll(`.workspace:path(${workspacePath})`)
66-
const res = await workspace.querySelectorAll(args[0])
67-
const converted = convertInventoryItemsToResponses(res)
68-
for (const item of converted) {
69-
result.add(item)
67+
for (const workspacePath of this.workspacePaths) {
68+
let items
69+
if (workspacePath === tree.root.path) {
70+
// include-workspace-root
71+
items = await tree.querySelectorAll(args[0])
72+
} else {
73+
const [workspace] = await tree.querySelectorAll(`.workspace:path(${workspacePath})`)
74+
items = await workspace.target.querySelectorAll(args[0])
75+
}
76+
this.buildResponse(items)
77+
}
78+
this.npm.output(this.parsedResponse)
79+
}
80+
81+
// builds a normalized inventory
82+
buildResponse (items) {
83+
for (const node of items) {
84+
if (!this.#seen.has(node.target.realpath)) {
85+
const item = new QuerySelectorItem(node)
86+
this.#response.push(item)
87+
this.#seen.add(item.realpath)
7088
}
7189
}
72-
// when running in workspaces names, make sure to key by workspace
73-
// name the results of each value retrieved in each ws
74-
this.npm.output(JSON.stringify([...result], null, 2))
7590
}
7691
}
7792

lib/utils/query-selector-all-response.js

-30
This file was deleted.

package-lock.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -5551,8 +5551,9 @@
55515551
},
55525552
"node_modules/p-map": {
55535553
"version": "4.0.0",
5554+
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
5555+
"integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
55545556
"inBundle": true,
5555-
"license": "MIT",
55565557
"dependencies": {
55575558
"aggregate-error": "^3.0.0"
55585559
},

tap-snapshots/test/lib/commands/query.js.test.cjs

+37-3
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,49 @@ exports[`test/lib/commands/query.js TAP global > should return global package 1`
1313
1414
"pkgid": "[email protected]",
1515
"location": "node_modules/lorem",
16-
"path": "{CWD}/test/lib/commands/tap-testdir-query-global/global/lib/node_modules/lorem",
17-
"realpath": "{CWD}/test/lib/commands/tap-testdir-query-global/global/lib/node_modules/lorem",
16+
"path": "{CWD}/test/lib/commands/tap-testdir-query-global/global/node_modules/lorem",
17+
"realpath": "{CWD}/test/lib/commands/tap-testdir-query-global/global/node_modules/lorem",
1818
"resolved": null,
1919
"isLink": false,
2020
"isWorkspace": false
2121
}
2222
]
2323
`
2424

25+
exports[`test/lib/commands/query.js TAP include-workspace-root > should return workspace object and root object 1`] = `
26+
[
27+
{
28+
"name": "project",
29+
"workspaces": [
30+
"c"
31+
],
32+
"dependencies": {
33+
"a": "^1.0.0",
34+
"b": "^1.0.0"
35+
},
36+
"pkgid": "project@",
37+
"location": "",
38+
"path": "{CWD}/test/lib/commands/tap-testdir-query-include-workspace-root/prefix",
39+
"realpath": "{CWD}/test/lib/commands/tap-testdir-query-include-workspace-root/prefix",
40+
"resolved": null,
41+
"isLink": false,
42+
"isWorkspace": false
43+
},
44+
{
45+
"name": "c",
46+
"version": "1.0.0",
47+
48+
"pkgid": "[email protected]",
49+
"location": "c",
50+
"path": "{CWD}/test/lib/commands/tap-testdir-query-include-workspace-root/prefix/c",
51+
"realpath": "{CWD}/test/lib/commands/tap-testdir-query-include-workspace-root/prefix/c",
52+
"resolved": null,
53+
"isLink": false,
54+
"isWorkspace": true
55+
}
56+
]
57+
`
58+
2559
exports[`test/lib/commands/query.js TAP linked node > should return linked node res 1`] = `
2660
[
2761
{
@@ -39,7 +73,7 @@ exports[`test/lib/commands/query.js TAP linked node > should return linked node
3973
]
4074
`
4175

42-
exports[`test/lib/commands/query.js TAP simple query > should return root object 1`] = `
76+
exports[`test/lib/commands/query.js TAP simple query > should return root object and direct children 1`] = `
4377
[
4478
{
4579
"name": "project",

tap-snapshots/test/lib/load-all-commands.js.test.cjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -687,7 +687,7 @@ exports[`test/lib/load-all-commands.js TAP load each command query > must match
687687
Retrieve a filtered list of packages
688688
689689
Usage:
690-
npm query <value>
690+
npm query <selector>
691691
692692
Options:
693693
[-g|--global]

tap-snapshots/test/lib/npm.js.test.cjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -739,7 +739,7 @@ All commands:
739739
query Retrieve a filtered list of packages
740740
741741
Usage:
742-
npm query <value>
742+
npm query <selector>
743743
744744
Options:
745745
[-g|--global]

test/lib/commands/query.js

+50-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ t.cleanSnapshot = (str) => {
77
.replace(/\r\n/g, '\n')
88
return normalizePath(str)
99
.replace(new RegExp(normalizePath(process.cwd()), 'g'), '{CWD}')
10+
// normalize between windows and posix
11+
.replace(new RegExp('lib/node_modules', 'g'), 'node_modules')
1012
}
1113

1214
t.test('simple query', async t => {
@@ -32,7 +34,7 @@ t.test('simple query', async t => {
3234
},
3335
})
3436
await npm.exec('query', [':root, :root > *'])
35-
t.matchSnapshot(joinedOutput(), 'should return root object')
37+
t.matchSnapshot(joinedOutput(), 'should return root object and direct children')
3638
})
3739

3840
t.test('workspace query', async t => {
@@ -72,6 +74,43 @@ t.test('workspace query', async t => {
7274
t.matchSnapshot(joinedOutput(), 'should return workspace object')
7375
})
7476

77+
t.test('include-workspace-root', async t => {
78+
const { npm, joinedOutput } = await loadMockNpm(t, {
79+
config: {
80+
'include-workspace-root': true,
81+
workspaces: ['c'],
82+
},
83+
prefixDir: {
84+
node_modules: {
85+
a: {
86+
name: 'a',
87+
version: '1.0.0',
88+
},
89+
b: {
90+
name: 'b',
91+
version: '^2.0.0',
92+
},
93+
c: t.fixture('symlink', '../c'),
94+
},
95+
c: {
96+
'package.json': JSON.stringify({
97+
name: 'c',
98+
version: '1.0.0',
99+
}),
100+
},
101+
'package.json': JSON.stringify({
102+
name: 'project',
103+
workspaces: ['c'],
104+
dependencies: {
105+
a: '^1.0.0',
106+
b: '^1.0.0',
107+
},
108+
}),
109+
},
110+
})
111+
await npm.exec('query', [':scope'], ['c'])
112+
t.matchSnapshot(joinedOutput(), 'should return workspace object and root object')
113+
})
75114
t.test('linked node', async t => {
76115
const { npm, joinedOutput } = await loadMockNpm(t, {
77116
prefixDir: {
@@ -101,7 +140,17 @@ t.test('global', async t => {
101140
config: {
102141
global: true,
103142
},
143+
// This is a global dir that works in both windows and non-windows, that's
144+
// why it has two node_modules folders
104145
globalPrefixDir: {
146+
node_modules: {
147+
lorem: {
148+
'package.json': JSON.stringify({
149+
name: 'lorem',
150+
version: '2.0.0',
151+
}),
152+
},
153+
},
105154
lib: {
106155
node_modules: {
107156
lorem: {

0 commit comments

Comments
 (0)