diff options
author | Ruy Adorno <ruyadorno@hotmail.com> | 2021-07-13 01:18:05 +0300 |
---|---|---|
committer | Ruy Adorno <ruyadorno@hotmail.com> | 2021-07-15 20:48:02 +0300 |
commit | 8371d7ddd94fe56e19f7ed00b62030e9cbca55e3 (patch) | |
tree | 5bee21355a83b5a4c4dc5401bb27cf6317b2a862 /lib | |
parent | 98905ae3759165cd6d6f6306f31acc6a2baa4cde (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.js | 89 |
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] || {} : {} } |