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
path: root/lib
diff options
context:
space:
mode:
authorRuy Adorno <ruyadorno@hotmail.com>2021-07-13 01:18:05 +0300
committerRuy Adorno <ruyadorno@hotmail.com>2021-07-15 20:48:02 +0300
commit8371d7ddd94fe56e19f7ed00b62030e9cbca55e3 (patch)
tree5bee21355a83b5a4c4dc5401bb27cf6317b2a862 /lib
parent98905ae3759165cd6d6f6306f31acc6a2baa4cde (diff)
feat(pkg): add support to empty bracket syntax
Adds ability to using empty bracket syntax as a shortcut to appending items to the end of an array when using `npm pkg set`, e.g: npm pkg set keywords[]=foo Relates to: https://github.com/npm/rfcs/pull/402 PR-URL: https://github.com/npm/cli/pull/3539 Credit: @ruyadorno Close: #3539 Reviewed-by: @darcyclarke, @ljharb
Diffstat (limited to 'lib')
-rw-r--r--lib/utils/queryable.js89
1 files changed, 75 insertions, 14 deletions
diff --git a/lib/utils/queryable.js b/lib/utils/queryable.js
index 173877e64..e10eba3b5 100644
--- a/lib/utils/queryable.js
+++ b/lib/utils/queryable.js
@@ -1,14 +1,27 @@
const util = require('util')
const _data = Symbol('data')
const _delete = Symbol('delete')
+const _append = Symbol('append')
-const sqBracketsMatcher = str => str.match(/(.+)\[([^\]]+)\](.*)$/)
+const sqBracketsMatcher = str => str.match(/(.+)\[([^\]]+)\]\.?(.*)$/)
-const cleanLeadingDot = str =>
- str && str.startsWith('.') ? str.substr(1) : str
+// replaces any occurence of an empty-brackets (e.g: []) with a special
+// Symbol(append) to represent it, this is going to be useful for the setter
+// method that will push values to the end of the array when finding these
+const replaceAppendSymbols = str => {
+ const matchEmptyBracket = str.match(/^(.*)\[\]\.?(.*)$/)
+
+ if (matchEmptyBracket) {
+ const [, pre, post] = matchEmptyBracket
+ return [...replaceAppendSymbols(pre), _append, post].filter(Boolean)
+ }
+
+ return [str]
+}
const parseKeys = (key) => {
const sqBracketItems = new Set()
+ sqBracketItems.add(_append)
const parseSqBrackets = (str) => {
const index = sqBracketsMatcher(str)
@@ -21,7 +34,7 @@ const parseKeys = (key) => {
// 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])
+ const postSqBracketPortion = 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
@@ -43,7 +56,11 @@ const parseKeys = (key) => {
]
}
- return [str]
+ // at the end of parsing, any usage of the special empty-bracket syntax
+ // (e.g: foo.array[]) has not yet been parsed, here we'll take care
+ // of parsing it and adding a special symbol to represent it in
+ // the resulting list of keys
+ return replaceAppendSymbols(str)
}
const res = []
@@ -79,6 +96,14 @@ const getter = ({ data, key }) => {
let label = ''
for (const k of keys) {
+ // empty-bracket-shortcut-syntax is not supported on getter
+ if (k === _append) {
+ throw Object.assign(
+ new Error('Empty brackets are not valid syntax for retrieving values.'),
+ { code: 'EINVALIDSYNTAX' }
+ )
+ }
+
// 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:
@@ -119,13 +144,39 @@ const setter = ({ data, key, value, force }) => {
// ['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)) {
+ // handles array indexes, converting valid integers to numbers,
+ // note that occurences of Symbol(append) will throw,
+ // so we just ignore these for now
+ let maybeIndex = Number.NaN
+ try {
+ maybeIndex = Number(_key)
+ } catch (err) {}
+ if (!Number.isNaN(maybeIndex))
_key = maybeIndex
- if (!Object.keys(_data).length)
- _data = []
+
+ // creates new array in case key is an index
+ // and the array obj is not yet defined
+ const keyIsAnArrayIndex = _key === maybeIndex || _key === _append
+ const dataHasNoItems = !Object.keys(_data).length
+ if (keyIsAnArrayIndex && dataHasNoItems && !Array.isArray(_data))
+ _data = []
+
+ // converting from array to an object is also possible, in case the
+ // user is using force mode, we should also convert existing arrays
+ // to an empty object if the current _data is an array
+ if (force && Array.isArray(_data) && !keyIsAnArrayIndex)
+ _data = { ..._data }
+
+ // the _append key is a special key that is used to represent
+ // the empty-bracket notation, e.g: arr[] -> arr[arr.length]
+ if (_key === _append) {
+ if (!Array.isArray(_data)) {
+ throw Object.assign(
+ new Error(`Can't use append syntax in non-Array element`),
+ { code: 'ENOAPPEND' }
+ )
+ }
+ _key = _data.length
}
// retrieves the next data object to recursively iterate on,
@@ -141,20 +192,30 @@ const setter = ({ data, key, value, force }) => {
// 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 =
+ typeof keys[0] !== 'symbol' &&
Array.isArray(_data[_key]) &&
Number.isNaN(Number(keys[0]))
const overrideError =
haveContents &&
- (shouldNotOverrideLiteralValue || shouldNotAddPropsToArrays)
-
+ shouldNotOverrideLiteralValue
if (overrideError) {
throw Object.assign(
- new Error(`Property ${key} already has a value in place.`),
+ new Error(`Property ${_key} already exists and is not an Array or Object.`),
{ code: 'EOVERRIDEVALUE' }
)
}
+ const addPropsToArrayError =
+ haveContents &&
+ shouldNotAddPropsToArrays
+ if (addPropsToArrayError) {
+ throw Object.assign(
+ new Error(`Can't add property ${key} to an Array.`),
+ { code: 'ENOADDPROP' }
+ )
+ }
+
return typeof _data[_key] === 'object' ? _data[_key] || {} : {}
}