Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/npm/cli.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRuy Adorno <ruyadorno@hotmail.com>2021-06-28 18:37:08 +0300
committerRuy Adorno <ruyadorno@hotmail.com>2021-07-12 18:04:38 +0300
commitf17aca5cdf355aaa7e1f517d1b3bb4213f4df092 (patch)
treec3bbe8872bc9ec52a2fa24c3efb5cc2fb3d48ad5
parent74c99755e522f9cfc0d602841568d5e1f835fcaf (diff)
feat: npm pkg
Implements `npm pkg get|set|delete` support. It enables retrieving and modifying values in a `package.json` file of any given project. Included are the implementation based on https://github.com/npm/rfcs/pull/402 along with extensive tests and user documentation. Relates to: https://github.com/npm/rfcs/pull/402 Fixes: https://github.com/npm/statusboard/issues/368 PR-URL: https://github.com/npm/cli/pull/3487 Credit: @ruyadorno Close: #3487 Reviewed-by: @wraithgar
-rw-r--r--docs/content/commands/npm-audit.md4
-rw-r--r--docs/content/commands/npm-config.md3
-rw-r--r--docs/content/commands/npm-explain.md3
-rw-r--r--docs/content/commands/npm-fund.md3
-rw-r--r--docs/content/commands/npm-init.md1
-rw-r--r--docs/content/commands/npm-ls.md3
-rw-r--r--docs/content/commands/npm-org.md3
-rw-r--r--docs/content/commands/npm-outdated.md3
-rw-r--r--docs/content/commands/npm-pack.md3
-rw-r--r--docs/content/commands/npm-pkg.md238
-rw-r--r--docs/content/commands/npm-profile.md3
-rw-r--r--docs/content/commands/npm-prune.md3
-rw-r--r--docs/content/commands/npm-search.md3
-rw-r--r--docs/content/commands/npm-team.md3
-rw-r--r--docs/content/commands/npm-unpublish.md1
-rw-r--r--docs/content/commands/npm-version.md3
-rw-r--r--docs/content/commands/npm-view.md5
-rw-r--r--docs/content/using-npm/config.md4
-rw-r--r--lib/pkg.js152
-rw-r--r--lib/utils/cmd-list.js1
-rw-r--r--lib/utils/config/definitions.js4
-rw-r--r--lib/utils/queryable.js253
-rw-r--r--lib/view.js56
-rw-r--r--smoke-tests/index.js32
-rw-r--r--tap-snapshots/smoke-tests/index.js.test.cjs91
-rw-r--r--tap-snapshots/test/lib/load-all-commands.js.test.cjs18
-rw-r--r--tap-snapshots/test/lib/utils/cmd-list.js.test.cjs1
-rw-r--r--tap-snapshots/test/lib/utils/config/definitions.js.test.cjs4
-rw-r--r--tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs4
-rw-r--r--tap-snapshots/test/lib/utils/npm-usage.js.test.cjs48
-rw-r--r--test/lib/pkg.js705
-rw-r--r--test/lib/utils/queryable.js705
32 files changed, 2293 insertions, 70 deletions
diff --git a/docs/content/commands/npm-audit.md b/docs/content/commands/npm-audit.md
index 704d7a15f..94b16b27b 100644
--- a/docs/content/commands/npm-audit.md
+++ b/docs/content/commands/npm-audit.md
@@ -232,6 +232,7 @@ mistakes, unnecessary performance degradation, and malicious input.
* Allow unpublishing all versions of a published package.
* Allow conflicting peerDependencies to be installed in the root project.
* Implicitly set `--yes` during `npm init`.
+* Allow clobbering existing values in `npm pkg`
If you don't have a clear idea of what you want to do, it is strongly
recommended that you do not use this option!
@@ -243,6 +244,9 @@ recommended that you do not use this option!
Whether or not to output JSON data, rather than the normal output.
+* In `npm pkg set` it enables parsing set values with JSON.parse() before
+ saving them to your `package.json`.
+
Not supported by all npm commands.
#### `package-lock-only`
diff --git a/docs/content/commands/npm-config.md b/docs/content/commands/npm-config.md
index f2868cb89..992e9ef70 100644
--- a/docs/content/commands/npm-config.md
+++ b/docs/content/commands/npm-config.md
@@ -104,6 +104,9 @@ global config.
Whether or not to output JSON data, rather than the normal output.
+* In `npm pkg set` it enables parsing set values with JSON.parse() before
+ saving them to your `package.json`.
+
Not supported by all npm commands.
#### `global`
diff --git a/docs/content/commands/npm-explain.md b/docs/content/commands/npm-explain.md
index 0e50d7ae4..3a87ee8e4 100644
--- a/docs/content/commands/npm-explain.md
+++ b/docs/content/commands/npm-explain.md
@@ -63,6 +63,9 @@ node_modules/nyc/node_modules/find-up
Whether or not to output JSON data, rather than the normal output.
+* In `npm pkg set` it enables parsing set values with JSON.parse() before
+ saving them to your `package.json`.
+
Not supported by all npm commands.
#### `workspace`
diff --git a/docs/content/commands/npm-fund.md b/docs/content/commands/npm-fund.md
index 3dc5292b4..ec5f5a37f 100644
--- a/docs/content/commands/npm-fund.md
+++ b/docs/content/commands/npm-fund.md
@@ -73,6 +73,9 @@ test-workspaces-fund@1.0.0
Whether or not to output JSON data, rather than the normal output.
+* In `npm pkg set` it enables parsing set values with JSON.parse() before
+ saving them to your `package.json`.
+
Not supported by all npm commands.
#### `browser`
diff --git a/docs/content/commands/npm-init.md b/docs/content/commands/npm-init.md
index 23e8e70d9..54c3bdb4b 100644
--- a/docs/content/commands/npm-init.md
+++ b/docs/content/commands/npm-init.md
@@ -175,6 +175,7 @@ mistakes, unnecessary performance degradation, and malicious input.
* Allow unpublishing all versions of a published package.
* Allow conflicting peerDependencies to be installed in the root project.
* Implicitly set `--yes` during `npm init`.
+* Allow clobbering existing values in `npm pkg`
If you don't have a clear idea of what you want to do, it is strongly
recommended that you do not use this option!
diff --git a/docs/content/commands/npm-ls.md b/docs/content/commands/npm-ls.md
index 1f401fa95..350f40a99 100644
--- a/docs/content/commands/npm-ls.md
+++ b/docs/content/commands/npm-ls.md
@@ -91,6 +91,9 @@ upon by the current project.
Whether or not to output JSON data, rather than the normal output.
+* In `npm pkg set` it enables parsing set values with JSON.parse() before
+ saving them to your `package.json`.
+
Not supported by all npm commands.
#### `long`
diff --git a/docs/content/commands/npm-org.md b/docs/content/commands/npm-org.md
index e6df560ac..269f5cc3e 100644
--- a/docs/content/commands/npm-org.md
+++ b/docs/content/commands/npm-org.md
@@ -87,6 +87,9 @@ password, npm will prompt on the command line for one.
Whether or not to output JSON data, rather than the normal output.
+* In `npm pkg set` it enables parsing set values with JSON.parse() before
+ saving them to your `package.json`.
+
Not supported by all npm commands.
#### `parseable`
diff --git a/docs/content/commands/npm-outdated.md b/docs/content/commands/npm-outdated.md
index bc9263d7a..40e5feafd 100644
--- a/docs/content/commands/npm-outdated.md
+++ b/docs/content/commands/npm-outdated.md
@@ -104,6 +104,9 @@ upon by the current project.
Whether or not to output JSON data, rather than the normal output.
+* In `npm pkg set` it enables parsing set values with JSON.parse() before
+ saving them to your `package.json`.
+
Not supported by all npm commands.
#### `long`
diff --git a/docs/content/commands/npm-pack.md b/docs/content/commands/npm-pack.md
index 950702627..cd4a17591 100644
--- a/docs/content/commands/npm-pack.md
+++ b/docs/content/commands/npm-pack.md
@@ -34,6 +34,9 @@ Note: This is NOT honored by other network related commands, eg `dist-tags`,
Whether or not to output JSON data, rather than the normal output.
+* In `npm pkg set` it enables parsing set values with JSON.parse() before
+ saving them to your `package.json`.
+
Not supported by all npm commands.
#### `pack-destination`
diff --git a/docs/content/commands/npm-pkg.md b/docs/content/commands/npm-pkg.md
new file mode 100644
index 000000000..7ff0a4d97
--- /dev/null
+++ b/docs/content/commands/npm-pkg.md
@@ -0,0 +1,238 @@
+---
+title: npm-pkg
+section: 1
+description: Manages your package.json
+---
+
+### Synopsis
+
+```bash
+npm pkg get [<field> [.<subfield> ...]]
+npm pkg set <field>=<value> [.<subfield>=<value> ...]
+npm pkg delete <field> [.<subfield> ...]
+```
+
+### Description
+
+A command that automates the management of `package.json` files.
+`npm pkg` provide 3 different sub commands that allow you to modify or retrieve
+values for given object keys in your `packge.json`.
+
+The syntax to retrieve and set fields is a dot separated representation of
+the nested object properties to be found within your `package.json`, it's the
+same notation used in [`npm view`](/commands/npm-view) to retrieve information
+from the registry manifest, below you can find more examples on how to use it.
+
+Returned values are always in **json** format.
+
+* `npm pkg get <field>`
+
+ Retrieves a value `key`, defined in your `package.json` file.
+
+ For example, in order to retrieve the name of the current package, you
+ can run:
+
+ ```bash
+ npm pkg get name
+ ```
+
+ It's also possible to retrieve multiple values at once:
+
+ ```bash
+ npm pkg get name version
+ ```
+
+ You can view child fields by separating them with a period. To retrieve
+ the value of a test `script` value, you would run the following command:
+
+ ```bash
+ npm pkg get scripts.test
+ ```
+
+ For fields that are arrays, requesting a non-numeric field will return
+ all of the values from the objects in the list. For example, to get all
+ the contributor emails for a package, you would run:
+
+ ```bash
+ npm pkg get contributors.email
+ ```
+
+ You may also use numeric indices in square braces to specifically select
+ an item in an array field. To just get the email address of the first
+ contributor in the list, you can run:
+
+ ```bash
+ npm pkg get contributors[0].email
+ ```
+
+* `npm pkg set <field>=<value>`
+
+ Sets a `value` in your `package.json` based on the `field` value. When
+ saving to your `package.json` file the same set of rules used during
+ `npm install` and other cli commands that touches the `package.json` file
+ are used, making sure to respect the existing indentation and possibly
+ applying some validation prior to saving values to the file.
+
+ The same syntax used to retrieve values from your package can also be used
+ to define new properties or overriding existing ones, below are some
+ examples of how the dot separated syntax can be used to edit your
+ `package.json` file.
+
+ Defining a new bin named `mynewcommand` in your `package.json` that points
+ to a file `cli.js`:
+
+ ```bash
+ npm pkg set bin.mynewcommand=cli.js
+ ```
+
+ Setting multiple fields at once is also possible:
+
+ ```bash
+ npm pkg set description='Awesome package' engines.node='>=10'
+ ```
+
+ It's also possible to add to array values, for example to add a new
+ contributor entry:
+
+ ```bash
+ npm pkg set contributors[0].name='Foo' contributors[0].email='foo@bar.ca'
+ ```
+
+ It's also possible to parse values as json prior to saving them to your
+ `package.json` file, for example in order to set a `"private": true`
+ property:
+
+ ```bash
+ npm pkg set private=true --json
+ ```
+
+ It also enables saving values as numbers:
+
+ ```bash
+ npm pkg set tap.timeout=60 --json
+ ```
+
+* `npm pkg delete <key>`
+
+ Deletes a `key` from your `package.json`
+
+ The same syntax used to set values from your package can also be used
+ to remove existing ones. For example, in order to remove a script named
+ build:
+
+ ```bash
+ npm pkg delete scripts.build
+ ```
+
+### Workspaces support
+
+You can set/get/delete items across your configured workspaces by using the
+`workspace` or `workspaces` config options.
+
+For example, setting a `funding` value across all configured workspaces
+of a project:
+
+```bash
+npm pkg set funding=https://example.com --ws
+```
+
+When using `npm pkg get` to retrieve info from your configured workspaces, the
+returned result will be in a json format in which top level keys are the
+names of each workspace, the values of these keys will be the result values
+returned from each of the configured workspaces, e.g:
+
+```
+npm pkg get name version --ws
+{
+ "a": {
+ "name": "a",
+ "version": "1.0.0"
+ },
+ "b": {
+ "name": "b",
+ "version": "1.0.0"
+ }
+}
+```
+
+### Configuration
+
+<!-- AUTOGENERATED CONFIG DESCRIPTIONS START -->
+<!-- automatically generated, do not edit manually -->
+#### `force`
+
+* Default: false
+* Type: Boolean
+
+Removes various protections against unfortunate side effects, common
+mistakes, unnecessary performance degradation, and malicious input.
+
+* Allow clobbering non-npm files in global installs.
+* Allow the `npm version` command to work on an unclean git repository.
+* Allow deleting the cache folder with `npm cache clean`.
+* Allow installing packages that have an `engines` declaration requiring a
+ different version of npm.
+* Allow installing packages that have an `engines` declaration requiring a
+ different version of `node`, even if `--engine-strict` is enabled.
+* Allow `npm audit fix` to install modules outside your stated dependency
+ range (including SemVer-major changes).
+* Allow unpublishing all versions of a published package.
+* Allow conflicting peerDependencies to be installed in the root project.
+* Implicitly set `--yes` during `npm init`.
+* Allow clobbering existing values in `npm pkg`
+
+If you don't have a clear idea of what you want to do, it is strongly
+recommended that you do not use this option!
+
+#### `json`
+
+* Default: false
+* Type: Boolean
+
+Whether or not to output JSON data, rather than the normal output.
+
+* In `npm pkg set` it enables parsing set values with JSON.parse() before
+ saving them to your `package.json`.
+
+Not supported by all npm commands.
+
+#### `workspace`
+
+* Default:
+* Type: String (can be set multiple times)
+
+Enable running a command in the context of the configured workspaces of the
+current project while filtering by running only the workspaces defined by
+this configuration option.
+
+Valid values for the `workspace` config are either:
+
+* Workspace names
+* Path to a workspace directory
+* Path to a parent workspace directory (will result to selecting all of the
+ nested workspaces)
+
+When set for the `npm init` command, this may be set to the folder of a
+workspace which does not yet exist, to create the folder and set it up as a
+brand new workspace within the project.
+
+This value is not exported to the environment for child processes.
+
+#### `workspaces`
+
+* Default: false
+* Type: Boolean
+
+Enable running a command in the context of **all** the configured
+workspaces.
+
+This value is not exported to the environment for child processes.
+
+<!-- AUTOGENERATED CONFIG DESCRIPTIONS END -->
+## See Also
+
+* [npm install](/commands/npm-install)
+* [npm init](/commands/npm-init)
+* [npm config](/commands/npm-config)
+* [npm set-script](/commands/npm-set-script)
+* [workspaces](/using-npm/workspaces)
diff --git a/docs/content/commands/npm-profile.md b/docs/content/commands/npm-profile.md
index 63aa46540..079440d78 100644
--- a/docs/content/commands/npm-profile.md
+++ b/docs/content/commands/npm-profile.md
@@ -91,6 +91,9 @@ The base URL of the npm registry.
Whether or not to output JSON data, rather than the normal output.
+* In `npm pkg set` it enables parsing set values with JSON.parse() before
+ saving them to your `package.json`.
+
Not supported by all npm commands.
#### `parseable`
diff --git a/docs/content/commands/npm-prune.md b/docs/content/commands/npm-prune.md
index ecb6bdcd6..d9b5b068f 100644
--- a/docs/content/commands/npm-prune.md
+++ b/docs/content/commands/npm-prune.md
@@ -75,6 +75,9 @@ Note: This is NOT honored by other network related commands, eg `dist-tags`,
Whether or not to output JSON data, rather than the normal output.
+* In `npm pkg set` it enables parsing set values with JSON.parse() before
+ saving them to your `package.json`.
+
Not supported by all npm commands.
#### `workspace`
diff --git a/docs/content/commands/npm-search.md b/docs/content/commands/npm-search.md
index 08c955e64..e30287635 100644
--- a/docs/content/commands/npm-search.md
+++ b/docs/content/commands/npm-search.md
@@ -55,6 +55,9 @@ Show extended information in `ls`, `search`, and `help-search`.
Whether or not to output JSON data, rather than the normal output.
+* In `npm pkg set` it enables parsing set values with JSON.parse() before
+ saving them to your `package.json`.
+
Not supported by all npm commands.
#### `color`
diff --git a/docs/content/commands/npm-team.md b/docs/content/commands/npm-team.md
index 31b09c7ce..c7d5defcc 100644
--- a/docs/content/commands/npm-team.md
+++ b/docs/content/commands/npm-team.md
@@ -138,6 +138,9 @@ Output parseable results from commands that write to standard output. For
Whether or not to output JSON data, rather than the normal output.
+* In `npm pkg set` it enables parsing set values with JSON.parse() before
+ saving them to your `package.json`.
+
Not supported by all npm commands.
<!-- AUTOGENERATED CONFIG DESCRIPTIONS END -->
diff --git a/docs/content/commands/npm-unpublish.md b/docs/content/commands/npm-unpublish.md
index bc8fbc7a5..82779ab65 100644
--- a/docs/content/commands/npm-unpublish.md
+++ b/docs/content/commands/npm-unpublish.md
@@ -82,6 +82,7 @@ mistakes, unnecessary performance degradation, and malicious input.
* Allow unpublishing all versions of a published package.
* Allow conflicting peerDependencies to be installed in the root project.
* Implicitly set `--yes` during `npm init`.
+* Allow clobbering existing values in `npm pkg`
If you don't have a clear idea of what you want to do, it is strongly
recommended that you do not use this option!
diff --git a/docs/content/commands/npm-version.md b/docs/content/commands/npm-version.md
index d24207d1e..a3e34153a 100644
--- a/docs/content/commands/npm-version.md
+++ b/docs/content/commands/npm-version.md
@@ -47,6 +47,9 @@ Tag the commit when using the `npm version` command.
Whether or not to output JSON data, rather than the normal output.
+* In `npm pkg set` it enables parsing set values with JSON.parse() before
+ saving them to your `package.json`.
+
Not supported by all npm commands.
#### `preid`
diff --git a/docs/content/commands/npm-view.md b/docs/content/commands/npm-view.md
index 8cbd3267b..b3d5df86e 100644
--- a/docs/content/commands/npm-view.md
+++ b/docs/content/commands/npm-view.md
@@ -49,7 +49,7 @@ npm view opts@$(npm view ronn dependencies.opts)
For fields that are arrays, requesting a non-numeric field will return
all of the values from the objects in the list. For example, to get all
-the contributor names for the `express` package, you would run:
+the contributor email addresses for the `express` package, you would run:
```bash
npm view express contributors.email
@@ -105,6 +105,9 @@ npm view connect versions
Whether or not to output JSON data, rather than the normal output.
+* In `npm pkg set` it enables parsing set values with JSON.parse() before
+ saving them to your `package.json`.
+
Not supported by all npm commands.
#### `workspace`
diff --git a/docs/content/using-npm/config.md b/docs/content/using-npm/config.md
index 103689510..1feae3d64 100644
--- a/docs/content/using-npm/config.md
+++ b/docs/content/using-npm/config.md
@@ -495,6 +495,7 @@ mistakes, unnecessary performance degradation, and malicious input.
* Allow unpublishing all versions of a published package.
* Allow conflicting peerDependencies to be installed in the root project.
* Implicitly set `--yes` during `npm init`.
+* Allow clobbering existing values in `npm pkg`
If you don't have a clear idea of what you want to do, it is strongly
recommended that you do not use this option!
@@ -694,6 +695,9 @@ number, if not already set in package.json.
Whether or not to output JSON data, rather than the normal output.
+* In `npm pkg set` it enables parsing set values with JSON.parse() before
+ saving them to your `package.json`.
+
Not supported by all npm commands.
#### `key`
diff --git a/lib/pkg.js b/lib/pkg.js
new file mode 100644
index 000000000..9ba92c930
--- /dev/null
+++ b/lib/pkg.js
@@ -0,0 +1,152 @@
+const PackageJson = require('@npmcli/package-json')
+const BaseCommand = require('./base-command.js')
+const Queryable = require('./utils/queryable.js')
+
+class Pkg extends BaseCommand {
+ static get description () {
+ return 'Manages your package.json'
+ }
+
+ /* istanbul ignore next - see test/lib/load-all-commands.js */
+ static get name () {
+ return 'pkg'
+ }
+
+ /* istanbul ignore next - see test/lib/load-all-commands.js */
+ static get usage () {
+ return [
+ 'set <key>=<value> [<key>=<value> ...]',
+ 'get [<key> [<key> ...]]',
+ 'delete <key> [<key> ...]',
+ ]
+ }
+
+ /* istanbul ignore next - see test/lib/load-all-commands.js */
+ static get params () {
+ return [
+ 'force',
+ 'json',
+ 'workspace',
+ 'workspaces',
+ ]
+ }
+
+ exec (args, cb) {
+ this.prefix = this.npm.localPrefix
+ this.pkg(args).then(() => cb()).catch(cb)
+ }
+
+ execWorkspaces (args, filters, cb) {
+ this.pkgWorkspaces(args, filters).then(() => cb()).catch(cb)
+ }
+
+ async pkg (args) {
+ if (this.npm.config.get('global')) {
+ throw Object.assign(
+ new Error(`There's no package.json file to manage on global mode`),
+ { code: 'EPKGGLOBAL' }
+ )
+ }
+
+ const [cmd, ..._args] = args
+ switch (cmd) {
+ case 'get':
+ return this.get(_args)
+ case 'set':
+ return this.set(_args)
+ case 'delete':
+ return this.delete(_args)
+ default:
+ throw this.usageError()
+ }
+ }
+
+ async pkgWorkspaces (args, filters) {
+ await this.setWorkspaces(filters)
+ const result = {}
+ for (const [workspaceName, workspacePath] of this.workspaces.entries()) {
+ this.prefix = workspacePath
+ result[workspaceName] = await this.pkg(args)
+ }
+ // 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))
+ }
+
+ async get (args) {
+ const pkgJson = await PackageJson.load(this.prefix)
+
+ const { content } = pkgJson
+ let result = !args.length && content
+
+ if (!result) {
+ const q = new Queryable(content)
+ result = q.query(args)
+
+ // in case there's only a single result from the query
+ // just prints that one element to stdout
+ if (Object.keys(result).length === 1)
+ result = result[args]
+ }
+
+ // only outputs if not running with workspaces config,
+ // in case you're retrieving info for workspaces the pkgWorkspaces
+ // will handle the output to make sure it get keyed by ws name
+ if (!this.workspaces)
+ this.npm.output(JSON.stringify(result, null, 2))
+
+ return result
+ }
+
+ async set (args) {
+ const setError = () =>
+ Object.assign(
+ new TypeError('npm pkg set expects a key=value pair of args.'),
+ { code: 'EPKGSET' }
+ )
+
+ if (!args.length)
+ throw setError()
+
+ const force = this.npm.config.get('force')
+ const json = this.npm.config.get('json')
+ const pkgJson = await PackageJson.load(this.prefix)
+ const q = new Queryable(pkgJson.content)
+ for (const arg of args) {
+ const [key, ...rest] = arg.split('=')
+ const value = rest.join('=')
+ if (!key || !value)
+ throw setError()
+
+ q.set(key, json ? JSON.parse(value) : value, { force })
+ }
+
+ pkgJson.update(q.toJSON())
+ await pkgJson.save()
+ }
+
+ async delete (args) {
+ const setError = () =>
+ Object.assign(
+ new TypeError('npm pkg delete expects key args.'),
+ { code: 'EPKGDELETE' }
+ )
+
+ if (!args.length)
+ throw setError()
+
+ const pkgJson = await PackageJson.load(this.prefix)
+ const q = new Queryable(pkgJson.content)
+ for (const key of args) {
+ if (!key)
+ throw setError()
+
+ q.delete(key)
+ }
+
+ pkgJson.update(q.toJSON())
+ await pkgJson.save()
+ }
+}
+
+module.exports = Pkg
diff --git a/lib/utils/cmd-list.js b/lib/utils/cmd-list.js
index c865cdabb..26da53900 100644
--- a/lib/utils/cmd-list.js
+++ b/lib/utils/cmd-list.js
@@ -122,6 +122,7 @@ const cmdList = [
'diff',
'dist-tag',
'ping',
+ 'pkg',
'test',
'stop',
diff --git a/lib/utils/config/definitions.js b/lib/utils/config/definitions.js
index d540b0fc6..5375aeced 100644
--- a/lib/utils/config/definitions.js
+++ b/lib/utils/config/definitions.js
@@ -715,6 +715,7 @@ define('force', {
* Allow unpublishing all versions of a published package.
* Allow conflicting peerDependencies to be installed in the root project.
* Implicitly set \`--yes\` during \`npm init\`.
+ * Allow clobbering existing values in \`npm pkg\`
If you don't have a clear idea of what you want to do, it is strongly
recommended that you do not use this option!
@@ -1029,6 +1030,9 @@ define('json', {
description: `
Whether or not to output JSON data, rather than the normal output.
+ * In \`npm pkg set\` it enables parsing set values with JSON.parse()
+ before saving them to your \`package.json\`.
+
Not supported by all npm commands.
`,
flatten,
diff --git a/lib/utils/queryable.js b/lib/utils/queryable.js
new file mode 100644
index 000000000..173877e64
--- /dev/null
+++ b/lib/utils/queryable.js
@@ -0,0 +1,253 @@
+const util = require('util')
+const _data = Symbol('data')
+const _delete = Symbol('delete')
+
+const sqBracketsMatcher = str => str.match(/(.+)\[([^\]]+)\](.*)$/)
+
+const cleanLeadingDot = str =>
+ str && str.startsWith('.') ? str.substr(1) : str
+
+const parseKeys = (key) => {
+ const sqBracketItems = new Set()
+ const parseSqBrackets = (str) => {
+ const index = sqBracketsMatcher(str)
+
+ // once we find square brackets, we recursively parse all these
+ if (index) {
+ const preSqBracketPortion = index[1]
+
+ // we want to have a `new String` wrapper here in order to differentiate
+ // between multiple occurences of the same string, e.g:
+ // foo.bar[foo.bar] should split into { foo: { bar: { 'foo.bar': {} } }
+ /* eslint-disable-next-line no-new-wrappers */
+ const foundKey = new String(index[2])
+ const postSqBracketPortion = cleanLeadingDot(index[3])
+
+ // we keep track of items found during this step to make sure
+ // we don't try to split-separate keys that were defined within
+ // square brackets, since the key name itself might contain dots
+ sqBracketItems.add(foundKey)
+
+ // returns an array that contains either dot-separate items (that will
+ // be splitted appart during the next step OR the fully parsed keys
+ // read from square brackets, e.g:
+ // foo.bar[1.0.0].a.b -> ['foo.bar', '1.0.0', 'a.b']
+ return [
+ ...parseSqBrackets(preSqBracketPortion),
+ foundKey,
+ ...(
+ postSqBracketPortion
+ ? parseSqBrackets(postSqBracketPortion)
+ : []
+ ),
+ ]
+ }
+
+ return [str]
+ }
+
+ const res = []
+ // starts by parsing items defined as square brackets, those might be
+ // representing properties that have a dot in the name or just array
+ // indexes, e.g: foo[1.0.0] or list[0]
+ const sqBracketKeys = parseSqBrackets(key.trim())
+
+ for (const k of sqBracketKeys) {
+ // keys parsed from square brackets should just be added to list of
+ // resulting keys as they might have dots as part of the key
+ if (sqBracketItems.has(k))
+ res.push(k)
+ else {
+ // splits the dot-sep property names and add them to the list of keys
+ for (const splitKey of k.split('.'))
+ /* eslint-disable-next-line no-new-wrappers */
+ res.push(new String(splitKey))
+ }
+ }
+
+ // returns an ordered list of strings in which each entry
+ // represents a key in an object defined by the previous entry
+ return res
+}
+
+const getter = ({ data, key }) => {
+ // keys are a list in which each entry represents the name of
+ // a property that should be walked through the object in order to
+ // return the final found value
+ const keys = parseKeys(key)
+ let _data = data
+ let label = ''
+
+ for (const k of keys) {
+ // extra logic to take into account printing array, along with its
+ // special syntax in which using a dot-sep property name after an
+ // arry will expand it's results, e.g:
+ // arr.name -> arr[0].name=value, arr[1].name=value, ...
+ const maybeIndex = Number(k)
+ if (Array.isArray(_data) && !Number.isInteger(maybeIndex)) {
+ _data = _data.reduce((acc, i, index) => {
+ acc[`${label}[${index}].${k}`] = i[k]
+ return acc
+ }, {})
+ return _data
+ } else {
+ // if can't find any more values, it means it's just over
+ // and there's nothing to return
+ if (!_data[k])
+ return undefined
+
+ // otherwise sets the next value
+ _data = _data[k]
+ }
+
+ label += k
+ }
+
+ // these are some legacy expectations from
+ // the old API consumed by lib/view.js
+ if (Array.isArray(_data) && _data.length <= 1)
+ _data = _data[0]
+
+ return {
+ [key]: _data,
+ }
+}
+
+const setter = ({ data, key, value, force }) => {
+ // setter goes to recursively transform the provided data obj,
+ // setting properties from the list of parsed keys, e.g:
+ // ['foo', 'bar', 'baz'] -> { foo: { bar: { baz: {} } }
+ const keys = parseKeys(key)
+ const setKeys = (_data, _key) => {
+ // handles array indexes, making sure the new array is created if
+ // missing and properly casting the index to a number
+ const maybeIndex = Number(_key)
+ if (!Number.isNaN(maybeIndex)) {
+ _key = maybeIndex
+ if (!Object.keys(_data).length)
+ _data = []
+ }
+
+ // retrieves the next data object to recursively iterate on,
+ // throws if trying to override a literal value or add props to an array
+ const next = () => {
+ const haveContents =
+ !force &&
+ _data[_key] != null &&
+ value !== _delete
+ const shouldNotOverrideLiteralValue =
+ !(typeof _data[_key] === 'object')
+ // if the next obj to recurse is an array and the next key to be
+ // appended to the resulting obj is not an array index, then it
+ // should throw since we can't append arbitrary props to arrays
+ const shouldNotAddPropsToArrays =
+ Array.isArray(_data[_key]) &&
+ Number.isNaN(Number(keys[0]))
+
+ const overrideError =
+ haveContents &&
+ (shouldNotOverrideLiteralValue || shouldNotAddPropsToArrays)
+
+ if (overrideError) {
+ throw Object.assign(
+ new Error(`Property ${key} already has a value in place.`),
+ { code: 'EOVERRIDEVALUE' }
+ )
+ }
+
+ return typeof _data[_key] === 'object' ? _data[_key] || {} : {}
+ }
+
+ // sets items from the parsed array of keys as objects, recurses to
+ // setKeys in case there are still items to be handled, otherwise it
+ // just sets the original value set by the user
+ if (keys.length)
+ _data[_key] = setKeys(next(), keys.shift())
+ else {
+ // handles special deletion cases for obj props / array items
+ if (value === _delete) {
+ if (Array.isArray(_data))
+ _data.splice(_key, 1)
+ else
+ delete _data[_key]
+ } else
+ // finally, sets the value in its right place
+ _data[_key] = value
+ }
+
+ return _data
+ }
+
+ setKeys(data, keys.shift())
+}
+
+class Queryable {
+ constructor (obj) {
+ if (!obj || typeof obj !== 'object') {
+ throw Object.assign(
+ new Error('Queryable needs an object to query properties from.'),
+ { code: 'ENOQUERYABLEOBJ' }
+ )
+ }
+
+ this[_data] = obj
+ }
+
+ query (queries) {
+ // this ugly interface here is meant to be a compatibility layer
+ // with the legacy API lib/view.js is consuming, if at some point
+ // we refactor that command then we can revisit making this nicer
+ if (queries === '')
+ return { '': this[_data] }
+
+ const q = query => getter({
+ data: this[_data],
+ key: query,
+ })
+
+ if (Array.isArray(queries)) {
+ let res = {}
+ for (const query of queries)
+ res = { ...res, ...q(query) }
+ return res
+ } else
+ return q(queries)
+ }
+
+ // return the value for a single query if found, otherwise returns undefined
+ get (query) {
+ const obj = this.query(query)
+ if (obj)
+ return obj[query]
+ }
+
+ // creates objects along the way for the provided `query` parameter
+ // and assigns `value` to the last property of the query chain
+ set (query, value, { force } = {}) {
+ setter({
+ data: this[_data],
+ key: query,
+ value,
+ force,
+ })
+ }
+
+ // deletes the value of the property found at `query`
+ delete (query) {
+ setter({
+ data: this[_data],
+ key: query,
+ value: _delete,
+ })
+ }
+
+ toJSON () {
+ return this[_data]
+ }
+
+ [util.inspect.custom] () {
+ return this.toJSON()
+ }
+}
+
+module.exports = Queryable
diff --git a/lib/view.js b/lib/view.js
index 788df3ed0..47e631f55 100644
--- a/lib/view.js
+++ b/lib/view.js
@@ -17,6 +17,7 @@ const { packument } = require('pacote')
const readFile = promisify(fs.readFile)
const readJson = async file => jsonParse(await readFile(file, 'utf8'))
+const Queryable = require('./utils/queryable.js')
const BaseCommand = require('./base-command.js')
class View extends BaseCommand {
/* istanbul ignore next - see test/lib/load-all-commands.js */
@@ -459,56 +460,13 @@ function showFields (data, version, fields) {
o[k] = s[k]
})
})
- return search(o, fields.split('.'), version.version, fields)
-}
-function search (data, fields, version, title) {
- let field
- const tail = fields
- while (!field && fields.length)
- field = tail.shift()
- fields = [field].concat(tail)
- let o
- if (!field && !tail.length) {
- o = {}
- o[version] = {}
- o[version][title] = data
- return o
- }
- let index = field.match(/(.+)\[([^\]]+)\]$/)
- if (index) {
- field = index[1]
- index = index[2]
- if (data[field] && data[field][index])
- return search(data[field][index], tail, version, title)
- else
- field = field + '[' + index + ']'
- }
- if (Array.isArray(data)) {
- if (data.length === 1)
- return search(data[0], fields, version, title)
-
- let results = []
- data.forEach((data, i) => {
- const tl = title.length
- const newt = title.substr(0, tl - fields.join('.').length - 1) +
- '[' + i + ']' + [''].concat(fields).join('.')
- results.push(search(data, fields.slice(), version, newt))
- })
- results = results.reduce(reducer, {})
- return results
- }
- if (!data[field])
- return undefined
- data = data[field]
- if (tail.length) {
- // there are more fields to deal with.
- return search(data, tail, version, title)
- }
- o = {}
- o[version] = {}
- o[version][title] = data
- return o
+ const queryable = new Queryable(o)
+ const s = queryable.query(fields)
+ const res = { [version.version]: s }
+
+ if (s)
+ return res
}
function cleanup (data) {
diff --git a/smoke-tests/index.js b/smoke-tests/index.js
index c7b2d2a1c..9235c8960 100644
--- a/smoke-tests/index.js
+++ b/smoke-tests/index.js
@@ -209,3 +209,35 @@ t.test('npm uninstall', async t => {
'should have expected uninstall lockfile result'
)
})
+
+t.test('npm pkg', async t => {
+ let cmd = `${npmBin} pkg get license`
+ let cmdRes = await exec(cmd)
+ t.matchSnapshot(cmdRes.replace(/in.*s/, ''),
+ 'should have expected pkg get output')
+
+ cmd = `${npmBin} pkg set tap[test-env][0]=LC_ALL=sk`
+ cmdRes = await exec(cmd)
+ t.matchSnapshot(cmdRes.replace(/in.*s/, ''),
+ 'should have expected pkg set output')
+
+ t.matchSnapshot(
+ readFile('package.json'),
+ 'should have expected npm pkg set modified package.json result'
+ )
+
+ cmd = `${npmBin} pkg get`
+ cmdRes = await exec(cmd)
+ t.matchSnapshot(cmdRes.replace(/in.*s/, ''),
+ 'should print package.json contents')
+
+ cmd = `${npmBin} pkg delete tap`
+ cmdRes = await exec(cmd)
+ t.matchSnapshot(cmdRes.replace(/in.*s/, ''),
+ 'should have expected pkg delete output')
+
+ t.matchSnapshot(
+ readFile('package.json'),
+ 'should have expected npm pkg delete modified package.json result'
+ )
+})
diff --git a/tap-snapshots/smoke-tests/index.js.test.cjs b/tap-snapshots/smoke-tests/index.js.test.cjs
index 89c0cb20b..0a79e38cd 100644
--- a/tap-snapshots/smoke-tests/index.js.test.cjs
+++ b/tap-snapshots/smoke-tests/index.js.test.cjs
@@ -26,10 +26,10 @@ All commands:
edit, exec, explain, explore, find-dupes, fund, get, help,
hook, init, install, install-ci-test, install-test, link,
ll, login, logout, ls, org, outdated, owner, pack, ping,
- prefix, profile, prune, publish, rebuild, repo, restart,
- root, run-script, search, set, set-script, shrinkwrap, star,
- stars, start, stop, team, test, token, uninstall, unpublish,
- unstar, update, version, view, whoami
+ pkg, prefix, profile, prune, publish, rebuild, repo,
+ restart, root, run-script, search, set, set-script,
+ shrinkwrap, star, stars, start, stop, team, test, token,
+ uninstall, unpublish, unstar, update, version, view, whoami
Specify configs in the ini-formatted file:
{CWD}/smoke-tests/tap-testdir-index/.npmrc
@@ -482,6 +482,89 @@ abbrev 1.0.4 1.1.1 1.1.1 node_modules/abbrev project
`
+exports[`smoke-tests/index.js TAP npm pkg > should have expected npm pkg delete modified package.json result 1`] = `
+{
+ "name": "project",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo /"Error: no test specified/" && exit 1",
+ "hello": "echo Hello"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "abbrev": "^1.0.4"
+ }
+}
+
+`
+
+exports[`smoke-tests/index.js TAP npm pkg > should have expected npm pkg set modified package.json result 1`] = `
+{
+ "name": "project",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo /"Error: no test specified/" && exit 1",
+ "hello": "echo Hello"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "abbrev": "^1.0.4"
+ },
+ "tap": {
+ "test-env": [
+ "LC_ALL=sk"
+ ]
+ }
+}
+
+`
+
+exports[`smoke-tests/index.js TAP npm pkg > should have expected pkg delete output 1`] = `
+
+`
+
+exports[`smoke-tests/index.js TAP npm pkg > should have expected pkg get output 1`] = `
+"ISC"
+
+`
+
+exports[`smoke-tests/index.js TAP npm pkg > should have expected pkg set output 1`] = `
+
+`
+
+exports[`smoke-tests/index.js TAP npm pkg > should print package.json contents 1`] = `
+{
+ "name": "project",
+ "version": "1.0.0",
+ "description": "",
+ "ma",
+ "scripts": {
+ "test": "echo /"Error: no test specified/" && exit 1",
+ "hello": "echo Hello"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "abbrev": "^1.0.4"
+ },
+ "tap": {
+ "test-env": [
+ "LC_ALL=sk"
+ ]
+ }
+}
+
+`
+
exports[`smoke-tests/index.js TAP npm prefix > should have expected prefix output 1`] = `
{CWD}/smoke-tests/tap-testdir-index/project
diff --git a/tap-snapshots/test/lib/load-all-commands.js.test.cjs b/tap-snapshots/test/lib/load-all-commands.js.test.cjs
index 3575783a6..3b0754c52 100644
--- a/tap-snapshots/test/lib/load-all-commands.js.test.cjs
+++ b/tap-snapshots/test/lib/load-all-commands.js.test.cjs
@@ -678,6 +678,24 @@ Options:
Run "npm help ping" for more info
`
+exports[`test/lib/load-all-commands.js TAP load each command pkg > must match snapshot 1`] = `
+npm pkg
+
+Manages your package.json
+
+Usage:
+npm pkg set <key>=<value> [<key>=<value> ...]
+npm pkg get [<key> [<key> ...]]
+npm pkg delete <key> [<key> ...]
+
+Options:
+[-f|--force] [--json]
+[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
+[-ws|--workspaces]
+
+Run "npm help pkg" for more info
+`
+
exports[`test/lib/load-all-commands.js TAP load each command prefix > must match snapshot 1`] = `
npm prefix
diff --git a/tap-snapshots/test/lib/utils/cmd-list.js.test.cjs b/tap-snapshots/test/lib/utils/cmd-list.js.test.cjs
index 832f85601..971580792 100644
--- a/tap-snapshots/test/lib/utils/cmd-list.js.test.cjs
+++ b/tap-snapshots/test/lib/utils/cmd-list.js.test.cjs
@@ -158,6 +158,7 @@ Object {
"diff",
"dist-tag",
"ping",
+ "pkg",
"test",
"stop",
"start",
diff --git a/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs b/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs
index 12df9ec89..7b2ffbd8d 100644
--- a/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs
+++ b/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs
@@ -648,6 +648,7 @@ mistakes, unnecessary performance degradation, and malicious input.
* Allow unpublishing all versions of a published package.
* Allow conflicting peerDependencies to be installed in the root project.
* Implicitly set \`--yes\` during \`npm init\`.
+* Allow clobbering existing values in \`npm pkg\`
If you don't have a clear idea of what you want to do, it is strongly
recommended that you do not use this option!
@@ -949,6 +950,9 @@ exports[`test/lib/utils/config/definitions.js TAP > config description for json
Whether or not to output JSON data, rather than the normal output.
+* In \`npm pkg set\` it enables parsing set values with JSON.parse() before
+ saving them to your \`package.json\`.
+
Not supported by all npm commands.
`
diff --git a/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs b/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs
index daa071b64..babed3253 100644
--- a/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs
+++ b/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs
@@ -374,6 +374,7 @@ mistakes, unnecessary performance degradation, and malicious input.
* Allow unpublishing all versions of a published package.
* Allow conflicting peerDependencies to be installed in the root project.
* Implicitly set \`--yes\` during \`npm init\`.
+* Allow clobbering existing values in \`npm pkg\`
If you don't have a clear idea of what you want to do, it is strongly
recommended that you do not use this option!
@@ -573,6 +574,9 @@ number, if not already set in package.json.
Whether or not to output JSON data, rather than the normal output.
+* In \`npm pkg set\` it enables parsing set values with JSON.parse() before
+ saving them to your \`package.json\`.
+
Not supported by all npm commands.
#### \`key\`
diff --git a/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs b/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs
index 3987f6a73..f417d41b6 100644
--- a/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs
+++ b/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs
@@ -26,10 +26,10 @@ All commands:
edit, exec, explain, explore, find-dupes, fund, get, help,
hook, init, install, install-ci-test, install-test, link,
ll, login, logout, ls, org, outdated, owner, pack, ping,
- prefix, profile, prune, publish, rebuild, repo, restart,
- root, run-script, search, set, set-script, shrinkwrap, star,
- stars, start, stop, team, test, token, uninstall, unpublish,
- unstar, update, version, view, whoami
+ pkg, prefix, profile, prune, publish, rebuild, repo,
+ restart, root, run-script, search, set, set-script,
+ shrinkwrap, star, stars, start, stop, team, test, token,
+ uninstall, unpublish, unstar, update, version, view, whoami
Specify configs in the ini-formatted file:
/some/config/file/.npmrc
@@ -62,10 +62,10 @@ All commands:
edit, exec, explain, explore, find-dupes, fund, get, help,
hook, init, install, install-ci-test, install-test, link,
ll, login, logout, ls, org, outdated, owner, pack, ping,
- prefix, profile, prune, publish, rebuild, repo, restart,
- root, run-script, search, set, set-script, shrinkwrap, star,
- stars, start, stop, team, test, token, uninstall, unpublish,
- unstar, update, version, view, whoami
+ pkg, prefix, profile, prune, publish, rebuild, repo,
+ restart, root, run-script, search, set, set-script,
+ shrinkwrap, star, stars, start, stop, team, test, token,
+ uninstall, unpublish, unstar, update, version, view, whoami
Specify configs in the ini-formatted file:
/some/config/file/.npmrc
@@ -98,10 +98,10 @@ All commands:
edit, exec, explain, explore, find-dupes, fund, get, help,
hook, init, install, install-ci-test, install-test, link,
ll, login, logout, ls, org, outdated, owner, pack, ping,
- prefix, profile, prune, publish, rebuild, repo, restart,
- root, run-script, search, set, set-script, shrinkwrap, star,
- stars, start, stop, team, test, token, uninstall, unpublish,
- unstar, update, version, view, whoami
+ pkg, prefix, profile, prune, publish, rebuild, repo,
+ restart, root, run-script, search, set, set-script,
+ shrinkwrap, star, stars, start, stop, team, test, token,
+ uninstall, unpublish, unstar, update, version, view, whoami
Specify configs in the ini-formatted file:
/some/config/file/.npmrc
@@ -134,10 +134,10 @@ All commands:
edit, exec, explain, explore, find-dupes, fund, get, help,
hook, init, install, install-ci-test, install-test, link,
ll, login, logout, ls, org, outdated, owner, pack, ping,
- prefix, profile, prune, publish, rebuild, repo, restart,
- root, run-script, search, set, set-script, shrinkwrap, star,
- stars, start, stop, team, test, token, uninstall, unpublish,
- unstar, update, version, view, whoami
+ pkg, prefix, profile, prune, publish, rebuild, repo,
+ restart, root, run-script, search, set, set-script,
+ shrinkwrap, star, stars, start, stop, team, test, token,
+ uninstall, unpublish, unstar, update, version, view, whoami
Specify configs in the ini-formatted file:
/some/config/file/.npmrc
@@ -762,6 +762,22 @@ All commands:
Run "npm help ping" for more info
+ pkg npm pkg
+
+ Manages your package.json
+
+ Usage:
+ npm pkg set <key>=<value> [<key>=<value> ...]
+ npm pkg get [<key> [<key> ...]]
+ npm pkg delete <key> [<key> ...]
+
+ Options:
+ [-f|--force] [--json]
+ [-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
+ [-ws|--workspaces]
+
+ Run "npm help pkg" for more info
+
prefix npm prefix
Display prefix
diff --git a/test/lib/pkg.js b/test/lib/pkg.js
new file mode 100644
index 000000000..42eb7c0cc
--- /dev/null
+++ b/test/lib/pkg.js
@@ -0,0 +1,705 @@
+const { resolve } = require('path')
+const { readFileSync } = require('fs')
+const t = require('tap')
+const { fake: mockNpm } = require('../fixtures/mock-npm')
+
+const redactCwd = (path) => {
+ const normalizePath = p => p
+ .replace(/\\+/g, '/')
+ .replace(/\r\n/g, '\n')
+ return normalizePath(path)
+ .replace(new RegExp(normalizePath(process.cwd()), 'g'), '{CWD}')
+}
+
+t.cleanSnapshot = (str) => redactCwd(str)
+
+let OUTPUT = ''
+const config = {
+ global: false,
+ force: false,
+ 'pkg-cast': 'string',
+}
+const npm = mockNpm({
+ localPrefix: t.testdirName,
+ config,
+ output: (str) => {
+ OUTPUT += str
+ },
+})
+
+const Pkg = require('../../lib/pkg.js')
+const pkg = new Pkg(npm)
+
+const readPackageJson = (path) => {
+ path = path || npm.localPrefix
+ return JSON.parse(readFileSync(resolve(path, 'package.json'), 'utf8'))
+}
+
+t.afterEach(() => {
+ config.global = false
+ config.json = false
+ npm.localPrefix = t.testdirName
+ OUTPUT = ''
+})
+
+t.test('no args', t => {
+ pkg.exec([], err => {
+ t.match(
+ err,
+ { code: 'EUSAGE' },
+ 'should throw usage error'
+ )
+ t.end()
+ })
+})
+
+t.test('no global mode', t => {
+ config.global = true
+ pkg.exec(['get', 'foo'], err => {
+ t.match(
+ err,
+ { code: 'EPKGGLOBAL' },
+ 'should throw no global mode error'
+ )
+ t.end()
+ })
+})
+
+t.test('get no args', t => {
+ npm.localPrefix = t.testdir({
+ 'package.json': JSON.stringify({
+ name: 'foo',
+ version: '1.1.1',
+ }),
+ })
+
+ pkg.exec(['get'], err => {
+ if (err)
+ throw err
+
+ t.strictSame(
+ JSON.parse(OUTPUT),
+ {
+ name: 'foo',
+ version: '1.1.1',
+ },
+ 'should print package.json content'
+ )
+ t.end()
+ })
+})
+
+t.test('get single arg', t => {
+ npm.localPrefix = t.testdir({
+ 'package.json': JSON.stringify({
+ name: 'foo',
+ version: '1.1.1',
+ }),
+ })
+
+ pkg.exec(['get', 'version'], err => {
+ if (err)
+ throw err
+
+ t.strictSame(
+ JSON.parse(OUTPUT),
+ '1.1.1',
+ 'should print retrieved package.json field'
+ )
+ t.end()
+ })
+})
+
+t.test('get nested arg', t => {
+ npm.localPrefix = t.testdir({
+ 'package.json': JSON.stringify({
+ name: 'foo',
+ version: '1.1.1',
+ scripts: {
+ test: 'node test.js',
+ },
+ }),
+ })
+
+ pkg.exec(['get', 'scripts.test'], err => {
+ if (err)
+ throw err
+
+ t.strictSame(
+ JSON.parse(OUTPUT),
+ 'node test.js',
+ 'should print retrieved nested field'
+ )
+ t.end()
+ })
+})
+
+t.test('get array field', t => {
+ const files = [
+ 'index.js',
+ 'cli.js',
+ ]
+ npm.localPrefix = t.testdir({
+ 'package.json': JSON.stringify({
+ name: 'foo',
+ version: '1.1.1',
+ files,
+ }),
+ })
+
+ pkg.exec(['get', 'files'], err => {
+ if (err)
+ throw err
+
+ t.strictSame(
+ JSON.parse(OUTPUT),
+ files,
+ 'should print retrieved array field'
+ )
+ t.end()
+ })
+})
+
+t.test('get array item', t => {
+ const files = [
+ 'index.js',
+ 'cli.js',
+ ]
+ npm.localPrefix = t.testdir({
+ 'package.json': JSON.stringify({
+ name: 'foo',
+ version: '1.1.1',
+ files,
+ }),
+ })
+
+ pkg.exec(['get', 'files[0]'], err => {
+ if (err)
+ throw err
+
+ t.strictSame(
+ JSON.parse(OUTPUT),
+ 'index.js',
+ 'should print retrieved array field'
+ )
+ t.end()
+ })
+})
+
+t.test('get array nested items notation', t => {
+ const contributors = [
+ {
+ name: 'Ruy',
+ url: 'http://example.com/ruy',
+ },
+ {
+ name: 'Gar',
+ url: 'http://example.com/gar',
+ },
+ ]
+ npm.localPrefix = t.testdir({
+ 'package.json': JSON.stringify({
+ name: 'foo',
+ version: '1.1.1',
+ contributors,
+ }),
+ })
+
+ pkg.exec(['get', 'contributors.name'], err => {
+ if (err)
+ throw err
+
+ t.strictSame(
+ JSON.parse(OUTPUT),
+ {
+ 'contributors[0].name': 'Ruy',
+ 'contributors[1].name': 'Gar',
+ },
+ 'should print json result containing matching results'
+ )
+ t.end()
+ })
+})
+
+t.test('set no args', t => {
+ npm.localPrefix = t.testdir({
+ 'package.json': JSON.stringify({ name: 'foo' }),
+ })
+ pkg.exec(['set'], err => {
+ t.match(
+ err,
+ { code: 'EPKGSET' },
+ 'should throw an error if no args'
+ )
+
+ t.end()
+ })
+})
+
+t.test('set missing value', t => {
+ npm.localPrefix = t.testdir({
+ 'package.json': JSON.stringify({ name: 'foo' }),
+ })
+ pkg.exec(['set', 'key='], err => {
+ t.match(
+ err,
+ { code: 'EPKGSET' },
+ 'should throw an error if missing value'
+ )
+
+ t.end()
+ })
+})
+
+t.test('set missing key', t => {
+ npm.localPrefix = t.testdir({
+ 'package.json': JSON.stringify({ name: 'foo' }),
+ })
+ pkg.exec(['set', '=value'], err => {
+ t.match(
+ err,
+ { code: 'EPKGSET' },
+ 'should throw an error if missing key'
+ )
+
+ t.end()
+ })
+})
+
+t.test('set single field', t => {
+ const json = {
+ name: 'foo',
+ version: '1.1.1',
+ }
+ npm.localPrefix = t.testdir({
+ 'package.json': JSON.stringify(json),
+ })
+
+ pkg.exec(['set', 'description=Awesome stuff'], err => {
+ if (err)
+ throw err
+
+ t.strictSame(
+ readPackageJson(),
+ {
+ ...json,
+ description: 'Awesome stuff',
+ },
+ 'should add single field to package.json'
+ )
+ t.end()
+ })
+})
+
+t.test('set multiple fields', t => {
+ const json = {
+ name: 'foo',
+ version: '1.1.1',
+ }
+ npm.localPrefix = t.testdir({
+ 'package.json': JSON.stringify(json),
+ })
+
+ pkg.exec(['set', 'bin.foo=foo.js', 'scripts.test=node test.js'], err => {
+ if (err)
+ throw err
+
+ t.strictSame(
+ readPackageJson(),
+ {
+ ...json,
+ bin: {
+ foo: 'foo.js',
+ },
+ scripts: {
+ test: 'node test.js',
+ },
+ },
+ 'should add single field to package.json'
+ )
+ t.end()
+ })
+})
+
+t.test('set = separate value', t => {
+ const json = {
+ name: 'foo',
+ version: '1.1.1',
+ }
+ npm.localPrefix = t.testdir({
+ 'package.json': JSON.stringify(json),
+ })
+
+ pkg.exec(['set', 'tap[test-env][0]=LC_ALL=sk'], err => {
+ if (err)
+ throw err
+
+ t.strictSame(
+ readPackageJson(),
+ {
+ ...json,
+ tap: {
+ 'test-env': [
+ 'LC_ALL=sk',
+ ],
+ },
+ },
+ 'should add single field to package.json'
+ )
+ t.end()
+ })
+})
+
+t.test('set --json', async t => {
+ config.json = true
+ npm.localPrefix = t.testdir({
+ 'package.json': JSON.stringify({
+ name: 'foo',
+ version: '1.1.1',
+ }),
+ })
+
+ await new Promise((res, rej) => {
+ pkg.exec(['set', 'private=true'], err => {
+ if (err)
+ rej(err)
+
+ t.strictSame(
+ readPackageJson(),
+ {
+ name: 'foo',
+ version: '1.1.1',
+ private: true,
+ },
+ 'should add boolean field to package.json'
+ )
+ res()
+ })
+ })
+
+ await new Promise((res, rej) => {
+ pkg.exec(['set', 'tap.timeout=60'], err => {
+ if (err)
+ rej(err)
+
+ t.strictSame(
+ readPackageJson(),
+ {
+ name: 'foo',
+ version: '1.1.1',
+ private: true,
+ tap: {
+ timeout: 60,
+ },
+ },
+ 'should add number field to package.json'
+ )
+ res()
+ })
+ })
+
+ await new Promise((res, rej) => {
+ pkg.exec(['set', 'foo={ "bar": { "baz": "BAZ" } }'], err => {
+ if (err)
+ rej(err)
+
+ t.strictSame(
+ readPackageJson(),
+ {
+ name: 'foo',
+ version: '1.1.1',
+ private: true,
+ tap: {
+ timeout: 60,
+ },
+ foo: {
+ bar: {
+ baz: 'BAZ',
+ },
+ },
+ },
+ 'should add object field to package.json'
+ )
+ res()
+ })
+ })
+
+ await new Promise((res, rej) => {
+ pkg.exec(['set', 'workspaces=["packages/*"]'], err => {
+ if (err)
+ rej(err)
+
+ t.strictSame(
+ readPackageJson(),
+ {
+ name: 'foo',
+ version: '1.1.1',
+ private: true,
+ workspaces: [
+ 'packages/*',
+ ],
+ tap: {
+ timeout: 60,
+ },
+ foo: {
+ bar: {
+ baz: 'BAZ',
+ },
+ },
+ },
+ 'should add object field to package.json'
+ )
+ res()
+ })
+ })
+
+ await new Promise((res, rej) => {
+ pkg.exec(['set', 'description="awesome"'], err => {
+ if (err)
+ rej(err)
+
+ t.strictSame(
+ readPackageJson(),
+ {
+ name: 'foo',
+ version: '1.1.1',
+ description: 'awesome',
+ private: true,
+ workspaces: [
+ 'packages/*',
+ ],
+ tap: {
+ timeout: 60,
+ },
+ foo: {
+ bar: {
+ baz: 'BAZ',
+ },
+ },
+ },
+ 'should add object field to package.json'
+ )
+ res()
+ })
+ })
+})
+
+t.test('delete no args', t => {
+ npm.localPrefix = t.testdir({
+ 'package.json': JSON.stringify({ name: 'foo' }),
+ })
+ pkg.exec(['delete'], err => {
+ t.match(
+ err,
+ { code: 'EPKGDELETE' },
+ 'should throw an error if deleting no args'
+ )
+
+ t.end()
+ })
+})
+
+t.test('delete invalid key', t => {
+ npm.localPrefix = t.testdir({
+ 'package.json': JSON.stringify({ name: 'foo' }),
+ })
+ pkg.exec(['delete', ''], err => {
+ t.match(
+ err,
+ { code: 'EPKGDELETE' },
+ 'should throw an error if deleting invalid args'
+ )
+
+ t.end()
+ })
+})
+
+t.test('delete single field', t => {
+ npm.localPrefix = t.testdir({
+ 'package.json': JSON.stringify({
+ name: 'foo',
+ version: '1.0.0',
+ }),
+ })
+ pkg.exec(['delete', 'version'], err => {
+ if (err)
+ throw err
+
+ t.strictSame(
+ readPackageJson(),
+ {
+ name: 'foo',
+ },
+ 'should delete single field from package.json'
+ )
+
+ t.end()
+ })
+})
+
+t.test('delete multiple field', t => {
+ npm.localPrefix = t.testdir({
+ 'package.json': JSON.stringify({
+ name: 'foo',
+ version: '1.0.0',
+ description: 'awesome',
+ }),
+ })
+ pkg.exec(['delete', 'version', 'description'], err => {
+ if (err)
+ throw err
+
+ t.strictSame(
+ readPackageJson(),
+ {
+ name: 'foo',
+ },
+ 'should delete multiple fields from package.json'
+ )
+
+ t.end()
+ })
+})
+
+t.test('delete nested field', t => {
+ npm.localPrefix = t.testdir({
+ 'package.json': JSON.stringify({
+ name: 'foo',
+ version: '1.0.0',
+ info: {
+ foo: {
+ bar: [
+ {
+ baz: 'deleteme',
+ },
+ ],
+ },
+ },
+ }),
+ })
+ pkg.exec(['delete', 'info.foo.bar[0].baz'], err => {
+ if (err)
+ throw err
+
+ t.strictSame(
+ readPackageJson(),
+ {
+ name: 'foo',
+ version: '1.0.0',
+ info: {
+ foo: {
+ bar: [
+ {},
+ ],
+ },
+ },
+ },
+ 'should delete nested fields from package.json'
+ )
+
+ t.end()
+ })
+})
+
+t.test('workspaces', async t => {
+ npm.localPrefix = t.testdir({
+ 'package.json': JSON.stringify({
+ name: 'root',
+ version: '1.0.0',
+ workspaces: [
+ 'packages/*',
+ ],
+ }),
+ packages: {
+ a: {
+ 'package.json': JSON.stringify({
+ name: 'a',
+ version: '1.0.0',
+ }),
+ },
+ b: {
+ 'package.json': JSON.stringify({
+ name: 'b',
+ version: '1.2.3',
+ }),
+ },
+ },
+ })
+
+ await new Promise((res, rej) => {
+ pkg.execWorkspaces(['get', 'name', 'version'], [], err => {
+ if (err)
+ rej(err)
+
+ t.strictSame(
+ JSON.parse(OUTPUT),
+ {
+ a: {
+ name: 'a',
+ version: '1.0.0',
+ },
+ b: {
+ name: 'b',
+ version: '1.2.3',
+ },
+ },
+ 'should return expected result for configured workspaces'
+ )
+ res()
+ })
+ })
+
+ await new Promise((res, rej) => {
+ pkg.execWorkspaces(['set', 'funding=http://example.com'], [], err => {
+ if (err)
+ rej(err)
+
+ t.strictSame(
+ readPackageJson(resolve(npm.localPrefix, 'packages/a')),
+ {
+ name: 'a',
+ version: '1.0.0',
+ funding: 'http://example.com',
+ },
+ 'should add field to workspace a'
+ )
+
+ t.strictSame(
+ readPackageJson(resolve(npm.localPrefix, 'packages/b')),
+ {
+ name: 'b',
+ version: '1.2.3',
+ funding: 'http://example.com',
+ },
+ 'should add field to workspace b'
+ )
+ res()
+ })
+ })
+
+ await new Promise((res, rej) => {
+ pkg.execWorkspaces(['delete', 'version'], [], err => {
+ if (err)
+ rej(err)
+
+ t.strictSame(
+ readPackageJson(resolve(npm.localPrefix, 'packages/a')),
+ {
+ name: 'a',
+ funding: 'http://example.com',
+ },
+ 'should delete version field from workspace a'
+ )
+
+ t.strictSame(
+ readPackageJson(resolve(npm.localPrefix, 'packages/b')),
+ {
+ name: 'b',
+ funding: 'http://example.com',
+ },
+ 'should delete version field from workspace b'
+ )
+ res()
+ })
+ })
+})
diff --git a/test/lib/utils/queryable.js b/test/lib/utils/queryable.js
new file mode 100644
index 000000000..2e66eeeb9
--- /dev/null
+++ b/test/lib/utils/queryable.js
@@ -0,0 +1,705 @@
+const { inspect } = require('util')
+const t = require('tap')
+const Queryable = require('../../../lib/utils/queryable.js')
+
+t.test('retrieve single nested property', async t => {
+ const fixture = {
+ foo: {
+ bar: 'bar',
+ baz: 'baz',
+ },
+ lorem: {
+ ipsum: 'ipsum',
+ },
+ }
+ const q = new Queryable(fixture)
+ const query = 'foo.bar'
+ t.strictSame(q.query(query), { [query]: 'bar' },
+ 'should retrieve property value when querying for dot-sep name')
+})
+
+t.test('query', async t => {
+ const fixture = {
+ o: 'o',
+ single: [
+ 'item',
+ ],
+ w: [
+ 'a',
+ 'b',
+ 'c',
+ ],
+ list: [
+ {
+ name: 'first',
+ },
+ {
+ name: 'second',
+ },
+ ],
+ foo: {
+ bar: 'bar',
+ baz: 'baz',
+ },
+ lorem: {
+ ipsum: 'ipsum',
+ dolor: [
+ 'a',
+ 'b',
+ 'c',
+ {
+ sit: [
+ 'amet',
+ ],
+ },
+ ],
+ },
+ a: [
+ [
+ [
+ {
+ b: [
+ [
+ {
+ c: 'd',
+ },
+ ],
+ ],
+ },
+ ],
+ ],
+ ],
+ }
+ const q = new Queryable(fixture)
+ t.strictSame(
+ q.query(['foo.baz', 'lorem.dolor[0]']),
+ {
+ 'foo.baz': 'baz',
+ 'lorem.dolor[0]': 'a',
+ },
+ 'should retrieve property values when querying for multiple dot-sep names')
+ t.strictSame(
+ q.query('lorem.dolor[3].sit[0]'),
+ {
+ 'lorem.dolor[3].sit[0]': 'amet',
+ },
+ 'should retrieve property from nested array items')
+ t.strictSame(
+ q.query('a[0][0][0].b[0][0].c'),
+ {
+ 'a[0][0][0].b[0][0].c': 'd',
+ },
+ 'should retrieve property from deep nested array items')
+ t.strictSame(
+ q.query('o'),
+ {
+ o: 'o',
+ },
+ 'should retrieve single level property value')
+ t.strictSame(
+ q.query('list.name'),
+ {
+ 'list[0].name': 'first',
+ 'list[1].name': 'second',
+ },
+ 'should automatically expand arrays')
+ t.strictSame(
+ q.query(['list.name']),
+ {
+ 'list[0].name': 'first',
+ 'list[1].name': 'second',
+ },
+ 'should automatically expand multiple arrays')
+ t.strictSame(
+ q.query('w'),
+ {
+ w: ['a', 'b', 'c'],
+ },
+ 'should return arrays')
+ t.strictSame(
+ q.query('single'),
+ {
+ single: 'item',
+ },
+ 'should return single item')
+ t.strictSame(
+ q.query('missing'),
+ undefined,
+ 'should return undefined')
+ t.strictSame(
+ q.query('missing[bar]'),
+ undefined,
+ 'should return undefined also')
+
+ const qq = new Queryable({
+ foo: {
+ bar: 'bar',
+ },
+ })
+ t.strictSame(
+ qq.query(''),
+ {
+ '': {
+ foo: {
+ bar: 'bar',
+ },
+ },
+ },
+ 'should return an object with results in an empty key'
+ )
+})
+
+t.test('missing key', async t => {
+ const fixture = {
+ foo: {
+ bar: 'bar',
+ },
+ }
+ const q = new Queryable(fixture)
+ const query = 'foo.missing'
+ t.equal(q.query(query), undefined,
+ 'should retrieve no results')
+})
+
+t.test('no data object', async t => {
+ t.throws(
+ () => new Queryable(),
+ { code: 'ENOQUERYABLEOBJ' },
+ 'should throw ENOQUERYABLEOBJ error'
+ )
+ t.throws(
+ () => new Queryable(1),
+ { code: 'ENOQUERYABLEOBJ' },
+ 'should throw ENOQUERYABLEOBJ error'
+ )
+})
+
+t.test('get values', async t => {
+ const q = new Queryable({
+ foo: {
+ bar: 'bar',
+ },
+ })
+ t.equal(q.get('foo.bar'), 'bar', 'should retrieve value')
+ t.equal(q.get('missing'), undefined, 'should return undefined')
+})
+
+t.test('set property values', async t => {
+ const fixture = {
+ foo: {
+ bar: 'bar',
+ },
+ }
+ const q = new Queryable(fixture)
+ q.set('foo.baz', 'baz')
+ t.strictSame(
+ q.toJSON(),
+ {
+ foo: {
+ bar: 'bar',
+ baz: 'baz',
+ },
+ },
+ 'should add new property and its assigned value'
+ )
+ q.set('foo[lorem.ipsum]', 'LOREM IPSUM')
+ t.strictSame(
+ q.toJSON(),
+ {
+ foo: {
+ bar: 'bar',
+ baz: 'baz',
+ 'lorem.ipsum': 'LOREM IPSUM',
+ },
+ },
+ 'should be able to set square brackets props'
+ )
+ q.set('a.b[c.d]', 'omg')
+ t.strictSame(
+ q.toJSON(),
+ {
+ foo: {
+ bar: 'bar',
+ baz: 'baz',
+ 'lorem.ipsum': 'LOREM IPSUM',
+ },
+ a: {
+ b: {
+ 'c.d': 'omg',
+ },
+ },
+ },
+ 'should be able to nest square brackets props'
+ )
+ q.set('a.b[e][f.g][1.0.0]', 'multiple')
+ t.strictSame(
+ q.toJSON(),
+ {
+ foo: {
+ bar: 'bar',
+ baz: 'baz',
+ 'lorem.ipsum': 'LOREM IPSUM',
+ },
+ a: {
+ b: {
+ 'c.d': 'omg',
+ e: {
+ 'f.g': {
+ '1.0.0': 'multiple',
+ },
+ },
+ },
+ },
+ },
+ 'should be able to nest multiple square brackets props'
+ )
+ q.set('a.b[e][f.g][2.0.0].author.name', 'Ruy Adorno')
+ t.strictSame(
+ q.toJSON(),
+ {
+ foo: {
+ bar: 'bar',
+ baz: 'baz',
+ 'lorem.ipsum': 'LOREM IPSUM',
+ },
+ a: {
+ b: {
+ 'c.d': 'omg',
+ e: {
+ 'f.g': {
+ '1.0.0': 'multiple',
+ '2.0.0': {
+ author: {
+ name: 'Ruy Adorno',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ 'should be able to use dot-sep notation after square bracket props'
+ )
+ q.set('a.b[e][f.g][2.0.0].author[url]', 'https://npmjs.com')
+ t.strictSame(
+ q.toJSON(),
+ {
+ foo: {
+ bar: 'bar',
+ baz: 'baz',
+ 'lorem.ipsum': 'LOREM IPSUM',
+ },
+ a: {
+ b: {
+ 'c.d': 'omg',
+ e: {
+ 'f.g': {
+ '1.0.0': 'multiple',
+ '2.0.0': {
+ author: {
+ name: 'Ruy Adorno',
+ url: 'https://npmjs.com',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ 'should be able to have multiple, separated, square brackets props'
+ )
+ q.set('a.b[e][f.g][2.0.0].author[foo][bar].lorem.ipsum[dolor][sit][amet].omg', 'O_O')
+ t.strictSame(
+ q.toJSON(),
+ {
+ foo: {
+ bar: 'bar',
+ baz: 'baz',
+ 'lorem.ipsum': 'LOREM IPSUM',
+ },
+ a: {
+ b: {
+ 'c.d': 'omg',
+ e: {
+ 'f.g': {
+ '1.0.0': 'multiple',
+ '2.0.0': {
+ author: {
+ name: 'Ruy Adorno',
+ url: 'https://npmjs.com',
+ foo: {
+ bar: {
+ lorem: {
+ ipsum: {
+ dolor: {
+ sit: {
+ amet: {
+ omg: 'O_O',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ 'many many times...'
+ )
+ t.throws(
+ () => q.set('foo.bar.nest', 'should throw'),
+ { code: 'EOVERRIDEVALUE' },
+ 'should throw if trying to override a literal value with an object'
+ )
+ q.set('foo.bar.nest', 'use the force!', { force: true })
+ t.strictSame(
+ q.toJSON().foo,
+ {
+ bar: {
+ nest: 'use the force!',
+ },
+ baz: 'baz',
+ 'lorem.ipsum': 'LOREM IPSUM',
+ },
+ 'should allow overriding literal values when using force option'
+ )
+
+ const qq = new Queryable({})
+ qq.set('foo.bar.baz', 'BAZ')
+ t.strictSame(
+ qq.toJSON(),
+ {
+ foo: {
+ bar: {
+ baz: 'BAZ',
+ },
+ },
+ },
+ 'should add new props to qq object'
+ )
+ qq.set('foo.bar.bario', 'bario')
+ t.strictSame(
+ qq.toJSON(),
+ {
+ foo: {
+ bar: {
+ baz: 'BAZ',
+ bario: 'bario',
+ },
+ },
+ },
+ 'should add new props to a previously existing object'
+ )
+ qq.set('lorem', 'lorem')
+ t.strictSame(
+ qq.toJSON(),
+ {
+ foo: {
+ bar: {
+ baz: 'BAZ',
+ bario: 'bario',
+ },
+ },
+ lorem: 'lorem',
+ },
+ 'should append new props added to object later'
+ )
+ qq.set('foo.bar[foo.bar]', 'foo.bar.with.dots')
+ t.strictSame(
+ qq.toJSON(),
+ {
+ foo: {
+ bar: {
+ 'foo.bar': 'foo.bar.with.dots',
+ baz: 'BAZ',
+ bario: 'bario',
+ },
+ },
+ lorem: 'lorem',
+ },
+ 'should append new props added to object later'
+ )
+})
+
+t.test('set arrays', async t => {
+ const q = new Queryable({})
+
+ q.set('foo[1]', 'b')
+ t.strictSame(
+ q.toJSON(),
+ {
+ foo: [
+ undefined,
+ 'b',
+ ],
+ },
+ 'should be able to set items in an array using index references'
+ )
+
+ q.set('foo[0]', 'a')
+ t.strictSame(
+ q.toJSON(),
+ {
+ foo: [
+ 'a',
+ 'b',
+ ],
+ },
+ 'should be able to set a previously missing item to an array'
+ )
+
+ q.set('foo[2]', 'c')
+ t.strictSame(
+ q.toJSON(),
+ {
+ foo: [
+ 'a',
+ 'b',
+ 'c',
+ ],
+ },
+ 'should be able to append more items to an array'
+ )
+
+ q.set('foo[2]', 'C')
+ t.strictSame(
+ q.toJSON(),
+ {
+ foo: [
+ 'a',
+ 'b',
+ 'C',
+ ],
+ },
+ 'should be able to override array items'
+ )
+
+ t.throws(
+ () => q.set('foo[2].bar', 'bar'),
+ { code: 'EOVERRIDEVALUE' },
+ 'should throw if trying to override an array literal item with an obj'
+ )
+
+ q.set('foo[2].bar', 'bar', { force: true })
+ t.strictSame(
+ q.toJSON(),
+ {
+ foo: [
+ 'a',
+ 'b',
+ { bar: 'bar' },
+ ],
+ },
+ 'should be able to override an array string item with an obj'
+ )
+
+ q.set('foo[3].foo', 'surprise surprise, another foo')
+ t.strictSame(
+ q.toJSON(),
+ {
+ foo: [
+ 'a',
+ 'b',
+ { bar: 'bar' },
+ {
+ foo: 'surprise surprise, another foo',
+ },
+ ],
+ },
+ 'should be able to append more items to an array'
+ )
+
+ q.set('foo[3].foo', 'FOO')
+ t.strictSame(
+ q.toJSON(),
+ {
+ foo: [
+ 'a',
+ 'b',
+ { bar: 'bar' },
+ {
+ foo: 'FOO',
+ },
+ ],
+ },
+ 'should be able to override property of an obj inside an array'
+ )
+
+ const qq = new Queryable({})
+ qq.set('foo[0].bar[1].baz.bario[0][0][0]', 'something')
+ t.strictSame(
+ qq.toJSON(),
+ {
+ foo: [
+ {
+ bar: [
+ undefined,
+ {
+ baz: {
+ bario: [[['something']]],
+ },
+ },
+ ],
+ },
+ ],
+ },
+ 'should append as many arrays as necessary'
+ )
+ qq.set('foo[0].bar[1].baz.bario[0][1][0]', 'something else')
+ t.strictSame(
+ qq.toJSON(),
+ {
+ foo: [
+ {
+ bar: [
+ undefined,
+ {
+ baz: {
+ bario: [[
+ ['something'],
+ ['something else'],
+ ]],
+ },
+ },
+ ],
+ },
+ ],
+ },
+ 'should append as many arrays as necessary'
+ )
+ qq.set('foo', null)
+ t.strictSame(
+ qq.toJSON(),
+ {
+ foo: null,
+ },
+ 'should be able to set a value to null'
+ )
+ qq.set('foo.bar', 'bar')
+ t.strictSame(
+ qq.toJSON(),
+ {
+ foo: {
+ bar: 'bar',
+ },
+ },
+ 'should be able to replace a null value with properties'
+ )
+
+ const qqq = new Queryable({
+ arr: [
+ 'a',
+ 'b',
+ ],
+ })
+ t.throws(
+ () => qqq.set('arr.foo', 'foo'),
+ { code: 'EOVERRIDEVALUE' },
+ 'should throw an override error'
+ )
+})
+
+t.test('delete values', async t => {
+ const q = new Queryable({
+ foo: {
+ bar: {
+ lorem: 'lorem',
+ },
+ },
+ })
+ q.delete('foo.bar.lorem')
+ t.strictSame(
+ q.toJSON(),
+ {
+ foo: {
+ bar: {},
+ },
+ },
+ 'should delete queried item'
+ )
+ q.delete('foo')
+ t.strictSame(
+ q.toJSON(),
+ {},
+ 'should delete nested items'
+ )
+ q.set('foo.a.b.c[0]', 'value')
+ q.delete('foo.a.b.c[0]')
+ t.strictSame(
+ q.toJSON(),
+ {
+ foo: {
+ a: {
+ b: {
+ c: [],
+ },
+ },
+ },
+ },
+ 'should delete array item'
+ )
+ // creates an array that has an implicit empty first item
+ q.set('foo.a.b.c[1][0].foo.bar[0][0]', 'value')
+ q.delete('foo.a.b.c[1]')
+ t.strictSame(
+ q.toJSON(),
+ {
+ foo: {
+ a: {
+ b: {
+ c: [null],
+ },
+ },
+ },
+ },
+ 'should delete array item'
+ )
+})
+
+t.test('logger', async t => {
+ const q = new Queryable({})
+ q.set('foo.bar[0].baz', 'baz')
+ t.strictSame(
+ inspect(q, { depth: 10 }),
+ inspect({
+ foo: {
+ bar: [
+ {
+ baz: 'baz',
+ },
+ ],
+ },
+ }, { depth: 10 }),
+ 'should retrieve expected data'
+ )
+})
+
+t.test('bracket lovers', async t => {
+ const q = new Queryable({})
+ q.set('[iLoveBrackets]', 'seriously?')
+ t.strictSame(
+ q.toJSON(),
+ {
+ '[iLoveBrackets]': 'seriously?',
+ },
+ 'should be able to set top-level props using square brackets notation'
+ )
+
+ t.equal(q.get('[iLoveBrackets]'), 'seriously?',
+ 'should bypass square bracket in top-level properties')
+
+ q.set('[0]', '-.-')
+ t.strictSame(
+ q.toJSON(),
+ {
+ '[iLoveBrackets]': 'seriously?',
+ '[0]': '-.-',
+ },
+ 'any top-level item can not be parsed with square bracket notation'
+ )
+})