# `npm query` Command & Dependency Selector Syntax ## Summary Introduce a new `npm query` commmand which exposes a new dependency selector syntax (informed by & respecting many aspects of the [CSS Selectors 4 Spec](https://dev.w3.org/csswg/selectors4/#relational)). ## Motivation - Standardize the shape of, & querying of, dependency graphs with a robust object model, metadata & selector syntax - Leverage existing, known language syntax & operators from CSS to make disperate package information broadly accessible - Unlock the ability to anwser complex, multi-faceted questions about dependencies, their relationships & associative metadata - Consolidate redundant logic of similar query commands in `npm` (ex. `npm fund`, `npm ls`, `npm outdated`, `npm audit` ...) ## Detailed Explanation This RFC's spec & implementation should closely mimic the capabilities of existing CSS Selector specifications. Notably, we'll introduce limited net-new classes, states & syntax to ensure the widest adoption & understanding of paradigms. When deviating, we'll explicitely state why & how. ## Implementation - `Arborist`'s `Node` Class will have a new `.querySelectorAll()` method - this method will return a filtered, flattened dependency Arborist `Node` list based on a valid query selector - Introduce a new command, `npm query`, which will take a dependency selector & output a flattened dependency Node list (output is in `json` by default, but configurable) ### Dependency Selector Syntax #### Overview: - there is no "type" or "tag" selectors (ex. `div, h1, a`) as a dependency is the only type of `Node` to query - `dependency` & `link` types - the term "dependencies" is in reference to any `Node` found in the `idealTree` returned by `Arborist` #### 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 #### [Attribute Selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors) - `[]` attribute selector (ie. existence of attribute) - `[attribute=value]` attribute value is equivalant... - `[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 ends with... #### Pseudo Selectors - [`:not(<selector>)`](https://developer.mozilla.org/en-US/docs/Web/CSS/:not) - [`:has(<selector>)`](https://developer.mozilla.org/en-US/docs/Web/CSS/:has) - [`:where(<selector list>)`](https://developer.mozilla.org/en-US/docs/Web/CSS/:where) - [`:is(<selector list>)`](https://developer.mozilla.org/en-US/docs/Web/CSS/:is) - [`:root`](https://developer.mozilla.org/en-US/docs/Web/CSS/:root) matches the root node/dependency - [`:scope`](https://developer.mozilla.org/en-US/docs/Web/CSS/:scope) matches node/dependency it was queried against - [`:empty`](https://developer.mozilla.org/en-US/docs/Web/CSS/:empty) when a dependency has no dependencies - [`:private`](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#private) when a dependency is private - `:link` when a dependency is linked - `:deduped` when a dependency has been deduped - `:override` when a dependency is an override - `:extraneous` when a dependency could be but is not deduped - `:outdated` when a dependency is not `latest` - `:vulnerable` when a dependency has a `CVE` - `:semver(<spec>)` matching a valid [`node-semver`](https://github.com/npm/node-semver) spec - `:path(<path>)` [glob](https://www.npmjs.com/package/glob) matching based on dependencies path relative to the project - `:realpath(<path>)` [glob](https://www.npmjs.com/package/glob) matching based on dependencies realpath - `:type(<type>)` [based on currently recognized types](https://github.com/npm/npm-package-arg#result-object) #### Generic Pseudo Selectors: A standardized pseudo selector pattern will be used for `Object`s, `Array`s or `Arrays` of `Object`s accessible via `Arborist`'s `Node.package` metadata. Pseudo classes generated in this way will allow for iterative attribute selection. `Array`s specifically use a special `value` keyword in place of a typical attribute name. `Arrays` also support exact `valu`e matching when a `String` is passed to the selector. See examples below: #### Example of an `Object`: ```css /* return dependencies that have a `scripts.test` containing `"tap"` */ *:scripts([test~=tap]) ``` #### Example of an `Array` Attribute Selection: ```css /* return dependencies that have a keyword that begins with "react" */ *:keywords([value^="react"]) ``` #### Example of an `Array` matching directly to a value: ```css /* return dependencies that have the exact keyword "react" */ /* this is equivalent to `*:keywords([value="react"])` */ *:keywords("react") ``` #### Example of an `Array` of `Object`s: ```css /* returns */ *:contributors([email="ruyadorno@github.com"])` ``` ### Classes Given that `Arborist` will control our understanding of the DOM (Dependency Object Model), claseses are predefined by their relationships to their ancestors & are specific to their dependency type. Dependencies may have, & are allowed to have, multiple classifications (ex. a `workspace` may also be a `dev` dependency). - `.prod` - `.dev` - `.optional` - `.peer` - `.bundled` - `.workspace` ### Attributes There are several known attributes that are normalized & queryable living in `Node.package` (aka. `package.json`). - `[name]` - `[version]` - `[description]` - `[homepage]` - `[bugs]` - `[author]` - `[license]` - `[funding]` - `[files]` - `[main]` - `[browser]` - `[bin]` - `[man]` - `[directories]` - `[repository]` - `[scripts]` - `[config]` - `[workspaces]` - `[dependencies]` - `[devDependencies]` - `[optionalDependencies]` - `[bundledDependencies]` - `[peerDependencies]` - `[peerDependenciesMeta]` - `[engines]` - `[os]` - `[cpu]` - `[keywords]` ### Command #### `npm query "<selector>"` (alias `q`) #### Options: - `query-output` - Default: `json` - Type: `json`, `list`, `explain`, `outdated`, `funding`, `audit`, `duplicates`, `file` #### Response Output - an array of dependency objects is returned which can contain multiple copies of the same package which may or may not have been linked or deduped ```json [ { "name": "", "version": "", "description": "", "homepage": "", "bugs": {}, "author": {}, "license": {}, "funding": {}, "files": [], "main": "", "browser": "", "bin": {}, "man": [], "directories": {}, "repository": {}, "scripts": {}, "config": {}, "dependencies": {}, "devDependencies": {}, "optionalDependencies": {}, "bundledDependencies": {}, "peerDependencies": {}, "peerDependenciesMeta": {}, "engines": {}, "os": [], "cpu": [], "workspaces": {}, "keywords": [], "ancenstry": "", "path": "", "realpath": "", "parent": "", "vulnerabilities": [], "cwe": [] }, ... ``` #### Usage ```bash npm query ":root > .workspace > *" # get all workspace direct deps ``` ### Extended Example Queries & Use Cases ```stylus // all deps * // all direct deps :root > * // direct production deps :root > .prod // direct development deps :root > .dev // any peer dep of a direct deps :root > * > .peer // any workspace dep .workspace // all workspaces that depend on another workspace .workspace > .workspace // all workspaces that have peer deps .workspace:has(*.peer) // any dep named "lodash" // equivalent to [name="lodash"] #lodash // any dep named "lodash" & version "1.2.3" // equivalent to [name="lodash"][version="1.2.3"] #lodash@1.2.3 [name="lodash"]:semver(1.2.3) // querying by semver #lodash@^2 [name="lodash"][version="^2"] // all deps living alongside vulnerable deps *:vulnerable ~ *:not(:vulnerable) // has any deps *:has(*) // has any vulnerable deps *:has(*:vulnerable) // deps with no other deps (ie. "leaf" nodes) *:empty // all vulnerable deps that aren't dev deps & that aren't vulnerable to CWE-1333 *:vulnerable:not(.dev:cwe(1333)) // manually querying git dependencies *[repository^="github:"], *[repository^="git:"], *[repository^="https://github.com"], *[repository^="http://github.com"], *[repository^="https://github.com"], *[repository^="+git:..."] // querying git dependencies *:type(git) // find all references to "install" scripts *[scripts=install], *[scripts=postinstall], *[scripts=preinstall] .prod:not(.dev) .prod:not([name="lodash"]) // license querying (RFC) *[license="MIT"], *[license="ISC"] // find all packages that have @ruyadorno as a contributor *:contributors([email=ruyadorno@github.com]) ``` ## Future ### Command Mapping to `query-output` Previous commands with similar behaivours will now be able to utilize `Aborist` `Node.querySelectorAll()` under-the-hood & will fast-follow it's implementation. #### `npm list` ```bash npm list # equivalent to... npm query ":root > *" npm list --all # equivalent to... npm query "*" --query-output list npm list <pkg> # equivalent to... npm query "#<pkg>" --query-output list ``` #### `npm explain` ```bash npm explain <pkg> # equivalent to... npm query "#<pkg>" --query-output explain ``` #### `npm outdated` ```bash npm outdated # equivalent to... npm query ":root > *:outdated" --query-output outdated npm outdated --all # equivalent to... npm query "*:outdated" --query-output outdated ``` #### `npm fund` ```bash npm fund # equivalent to... npm query ":root > *[funding]" --query-output funding npm fund --all # equivalent to... npm query "*[funding]" --query-output funding npm fund <pkg> # equivalent to... npm query "#<pkg>" --query-output funding ``` #### `npm audit` ```bash npm audit # equivalent to... npm query ":root > *:vulnerable" --query-output audit npm audit --all # equivalent to... npm query "*:vulnerable" --query-output audit npm audit <pkg> # equivalent to... npm query "#<pkg>" --query-output audit ``` #### `npm find-dupes` ```bash npm find-dupes # equivalent to... npm query "*:deduped" --query-output duplicates ``` #### `npm view` ```bash npm view # equivalent to... npm query ":root" --query-output view npm view <pkg> # equivalent to... npm query "#<pkg>" --query-output view ``` ## Commands _could_ read from `stdin` In a future RFC & major version, `npm` could begin reading from `stdin` to chain commands together with a common understand of a dependency object. All of the below commands would add the able to execute as if they were passed package specs. ``` audit, bugs, ci, config, deprecate, diff, dist-tag, docs, edit, exec, explain, explore, find-dupes, fund, get, install, install-ci-test, install-test, link, list, outdated, pack, pkg, prune, publish, rebuild, repo, restart, run-script, set, set-script, shrinkwrap, star, stars, start, stop, team, test, token, uninstall, unpublish, unstar, update, version, view ``` ### Example of piping from `npm query` to other commands ```bash # list workspaces w/ peer deps npm query ".workspace:has(*.peer)" | npm ls # install the same dev deps across all workspaces npm query ":root > *.dev" | npm install --workspaces # find and enforce a single version of dependencies npm query "" # find cve npm query "*:cve(<id>)" # show audit details for vulnerable deps that aren't ReDoS dev deps npm query "*:vulnerable:not(.dev[cwe~=1333]:not(cwe=))" | npm audit explain ``` ## Prior Art - [`HTML`](https://html.spec.whatwg.org/) & [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model) - [`CSS`](https://www.w3.org/Style/CSS/specs.en.html), [Selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) & [Pseudo Classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes) - [`estree`](https://github.com/estree/estree) - [`abstract-syntax-tree`](https://www.npmjs.com/package/abstract-syntax-tree) - [`postcss-selector-parser`](https://www.npmjs.com/package/postcss-selector-parser) / [API](https://github.com/postcss/postcss-selector-parser/blob/master/API.md) - [`css-selector-parser`](https://www.npmjs.com/package/css-selector-parser) - [`@npmcli/arborist`'s `Node` Class](https://github.com/npm/cli/tree/latest/workspaces/arborist) - [pnpm's --filter](https://pnpm.io/filtering) - [Gzemnid](https://github.com/nodejs/Gzemnid) ## Unresolved Questions and Bikeshedding - Q. Is there any such thing as a bare specifier? - A. No. Unlike CSS Selectors, there's no `"element"`, or `"package"` in this context, equivalent. The selector syntax presuposes all entities are packages. - Q. Should this syntax cover **all** possible queries of a dependency graph? - A. No. This spec is meant to provide a sufficently mature mechanism/building block for (re. 80/20 rule applies here) - Q. These are conflicting statements: - there is no "type" or "tag" selectors (ex. `div, h1, a`) as a dependency is the only type of `Node` to query - `dependency` & `link` types