Skip to content

Commit

Permalink
fix: attribute selectors, workspace root, cleanup
Browse files Browse the repository at this point in the history
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
  • Loading branch information
wraithgar committed Jun 29, 2022
1 parent 3d043f3 commit bde355b
Show file tree
Hide file tree
Showing 12 changed files with 664 additions and 539 deletions.
2 changes: 1 addition & 1 deletion docs/content/commands/npm-query.md
Expand Up @@ -11,7 +11,7 @@ description: Dependency selector query
<!-- see lib/commands/query.js -->

```bash
npm query <value>
npm query <selector>
```

<!-- automatically generated, do not edit manually -->
Expand Down
25 changes: 19 additions & 6 deletions docs/content/using-npm/dependency-selectors.md
Expand Up @@ -20,16 +20,29 @@ The `npm query` commmand exposes a new dependency selector syntax (informed by &
- 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
- the term "dependencies" is in reference to any `Node` found in a `tree` returned by `Arborist`

#### Combinators

- `>` direct descendant/child
- ` ` any descendant/child
- `~` sibling

#### Selectors

- `*` universal selector
- `#<name>` dependency selector (equivalent to `[name="..."]`)
- `#<name>@<version>` (equivalent to `[name=<name>]:semver(<version>)`)
- `,` selector list delimiter
- `.` class selector
- `:` pseudo class selector
- `>` direct decendent/child selector
- `~` sibling selector
- `.` dependency type selector
- `:` pseudo selector

#### Dependency Type Selectors

- `.prod` dependency found in the `dependencies` section of `package.json`, or is a child of said dependency
- `.dev` dependency found in the `devDependencies` section of `package.json`, or is a child of said dependency
- `.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
- `.peer` dependency found in the `peerDependencies` section of `package.json`
- `.workspace` dependency found in the `workspaces` section of `package.json`
- `.bundled` dependency found in the `bundleDependencies` section of `package.json`, or is a child of said dependency

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

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

#### Nested `Objects`
#### Nested `Objects`

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

Expand Down
79 changes: 47 additions & 32 deletions lib/commands/query.js
Expand Up @@ -3,28 +3,30 @@
const { resolve } = require('path')
const Arborist = require('@npmcli/arborist')
const BaseCommand = require('../base-command.js')
const QuerySelectorAllResponse = require('../utils/query-selector-all-response.js')

// retrieves a normalized inventory
const convertInventoryItemsToResponses = inventory => {
const responses = []
const responsesSeen = new Set()
for (const node of inventory) {
if (!responsesSeen.has(node.target.realpath)) {
const item = new QuerySelectorAllResponse(node)
responses.push(item)
responsesSeen.add(item.path)
}
class QuerySelectorItem {
constructor (node) {
// all enumerable properties from the target
Object.assign(this, node.target.package)

// append extra info
this.pkgid = node.target.pkgid
this.location = node.target.location
this.path = node.target.path
this.realpath = node.target.realpath
this.resolved = node.target.resolved
this.isLink = node.target.isLink
this.isWorkspace = node.target.isWorkspace
}
return responses
}

class Query extends BaseCommand {
#response = [] // response is the query response
#seen = new Set() // paths we've seen so we can keep response deduped

static description = 'Retrieve a filtered list of packages'
static name = 'query'
static usage = [
'<value>',
]
static usage = ['<selector>']

static ignoreImplicitWorkspace = false

Expand All @@ -35,43 +37,56 @@ class Query extends BaseCommand {
'include-workspace-root',
]

async exec (args, workspaces) {
const globalTop = resolve(this.npm.globalDir, '..')
const where = this.npm.config.get('global') ? globalTop : this.npm.prefix
get parsedResponse () {
return JSON.stringify(this.#response, null, 2)
}

async exec (args) {
// one dir up from wherever node_modules lives
const where = resolve(this.npm.dir, '..')
const opts = {
...this.npm.flatOptions,
path: where,
}
const arb = new Arborist(opts)
const tree = await arb.loadActual(opts)
const items = await tree.querySelectorAll(args[0])
const res =
JSON.stringify(convertInventoryItemsToResponses(items), null, 2)
this.buildResponse(items)

return this.npm.output(res)
this.npm.output(this.parsedResponse)
}

async execWorkspaces (args, filters) {
await this.setWorkspaces(filters)
const result = new Set()
const opts = {
...this.npm.flatOptions,
path: this.npm.prefix,
}
const arb = new Arborist(opts)
const tree = await arb.loadActual(opts)
for (const [, workspacePath] of this.workspaces.entries()) {
this.prefix = workspacePath
const [workspace] = await tree.querySelectorAll(`.workspace:path(${workspacePath})`)
const res = await workspace.querySelectorAll(args[0])
const converted = convertInventoryItemsToResponses(res)
for (const item of converted) {
result.add(item)
for (const workspacePath of this.workspacePaths) {
let items
if (workspacePath === tree.root.path) {
// include-workspace-root
items = await tree.querySelectorAll(args[0])
} else {
const [workspace] = await tree.querySelectorAll(`.workspace:path(${workspacePath})`)
items = await workspace.target.querySelectorAll(args[0])
}
this.buildResponse(items)
}
this.npm.output(this.parsedResponse)
}

// builds a normalized inventory
buildResponse (items) {
for (const node of items) {
if (!this.#seen.has(node.target.realpath)) {
const item = new QuerySelectorItem(node)
this.#response.push(item)
this.#seen.add(item.realpath)
}
}
// when running in workspaces names, make sure to key by workspace
// name the results of each value retrieved in each ws
this.npm.output(JSON.stringify([...result], null, 2))
}
}

Expand Down
30 changes: 0 additions & 30 deletions lib/utils/query-selector-all-response.js

This file was deleted.

3 changes: 2 additions & 1 deletion package-lock.json
Expand Up @@ -5551,8 +5551,9 @@
},
"node_modules/p-map": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
"integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
"inBundle": true,
"license": "MIT",
"dependencies": {
"aggregate-error": "^3.0.0"
},
Expand Down
40 changes: 37 additions & 3 deletions tap-snapshots/test/lib/commands/query.js.test.cjs
Expand Up @@ -13,15 +13,49 @@ exports[`test/lib/commands/query.js TAP global > should return global package 1`
"_id": "lorem@2.0.0",
"pkgid": "lorem@2.0.0",
"location": "node_modules/lorem",
"path": "{CWD}/test/lib/commands/tap-testdir-query-global/global/lib/node_modules/lorem",
"realpath": "{CWD}/test/lib/commands/tap-testdir-query-global/global/lib/node_modules/lorem",
"path": "{CWD}/test/lib/commands/tap-testdir-query-global/global/node_modules/lorem",
"realpath": "{CWD}/test/lib/commands/tap-testdir-query-global/global/node_modules/lorem",
"resolved": null,
"isLink": false,
"isWorkspace": false
}
]
`

exports[`test/lib/commands/query.js TAP include-workspace-root > should return workspace object and root object 1`] = `
[
{
"name": "project",
"workspaces": [
"c"
],
"dependencies": {
"a": "^1.0.0",
"b": "^1.0.0"
},
"pkgid": "project@",
"location": "",
"path": "{CWD}/test/lib/commands/tap-testdir-query-include-workspace-root/prefix",
"realpath": "{CWD}/test/lib/commands/tap-testdir-query-include-workspace-root/prefix",
"resolved": null,
"isLink": false,
"isWorkspace": false
},
{
"name": "c",
"version": "1.0.0",
"_id": "c@1.0.0",
"pkgid": "c@1.0.0",
"location": "c",
"path": "{CWD}/test/lib/commands/tap-testdir-query-include-workspace-root/prefix/c",
"realpath": "{CWD}/test/lib/commands/tap-testdir-query-include-workspace-root/prefix/c",
"resolved": null,
"isLink": false,
"isWorkspace": true
}
]
`

exports[`test/lib/commands/query.js TAP linked node > should return linked node res 1`] = `
[
{
Expand All @@ -39,7 +73,7 @@ exports[`test/lib/commands/query.js TAP linked node > should return linked node
]
`

exports[`test/lib/commands/query.js TAP simple query > should return root object 1`] = `
exports[`test/lib/commands/query.js TAP simple query > should return root object and direct children 1`] = `
[
{
"name": "project",
Expand Down
2 changes: 1 addition & 1 deletion tap-snapshots/test/lib/load-all-commands.js.test.cjs
Expand Up @@ -687,7 +687,7 @@ exports[`test/lib/load-all-commands.js TAP load each command query > must match
Retrieve a filtered list of packages
Usage:
npm query <value>
npm query <selector>
Options:
[-g|--global]
Expand Down
2 changes: 1 addition & 1 deletion tap-snapshots/test/lib/npm.js.test.cjs
Expand Up @@ -739,7 +739,7 @@ All commands:
query Retrieve a filtered list of packages
Usage:
npm query <value>
npm query <selector>
Options:
[-g|--global]
Expand Down
51 changes: 50 additions & 1 deletion test/lib/commands/query.js
Expand Up @@ -7,6 +7,8 @@ t.cleanSnapshot = (str) => {
.replace(/\r\n/g, '\n')
return normalizePath(str)
.replace(new RegExp(normalizePath(process.cwd()), 'g'), '{CWD}')
// normalize between windows and posix
.replace(new RegExp('lib/node_modules', 'g'), 'node_modules')
}

t.test('simple query', async t => {
Expand All @@ -32,7 +34,7 @@ t.test('simple query', async t => {
},
})
await npm.exec('query', [':root, :root > *'])
t.matchSnapshot(joinedOutput(), 'should return root object')
t.matchSnapshot(joinedOutput(), 'should return root object and direct children')
})

t.test('workspace query', async t => {
Expand Down Expand Up @@ -72,6 +74,43 @@ t.test('workspace query', async t => {
t.matchSnapshot(joinedOutput(), 'should return workspace object')
})

t.test('include-workspace-root', async t => {
const { npm, joinedOutput } = await loadMockNpm(t, {
config: {
'include-workspace-root': true,
workspaces: ['c'],
},
prefixDir: {
node_modules: {
a: {
name: 'a',
version: '1.0.0',
},
b: {
name: 'b',
version: '^2.0.0',
},
c: t.fixture('symlink', '../c'),
},
c: {
'package.json': JSON.stringify({
name: 'c',
version: '1.0.0',
}),
},
'package.json': JSON.stringify({
name: 'project',
workspaces: ['c'],
dependencies: {
a: '^1.0.0',
b: '^1.0.0',
},
}),
},
})
await npm.exec('query', [':scope'], ['c'])
t.matchSnapshot(joinedOutput(), 'should return workspace object and root object')
})
t.test('linked node', async t => {
const { npm, joinedOutput } = await loadMockNpm(t, {
prefixDir: {
Expand Down Expand Up @@ -101,7 +140,17 @@ t.test('global', async t => {
config: {
global: true,
},
// This is a global dir that works in both windows and non-windows, that's
// why it has two node_modules folders
globalPrefixDir: {
node_modules: {
lorem: {
'package.json': JSON.stringify({
name: 'lorem',
version: '2.0.0',
}),
},
},
lib: {
node_modules: {
lorem: {
Expand Down

0 comments on commit bde355b

Please sign in to comment.