Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pkg): add support to empty bracket syntax #3539

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/content/commands/npm-pkg.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ Returned values are always in **json** format.
npm pkg set contributors[0].name='Foo' contributors[0].email='foo@bar.ca'
```
You may also append items to the end of an array using the special
empty bracket notation:
```bash
npm pkg set contributors[].name='Foo' contributors[].name='Bar'
```
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:
Expand Down
89 changes: 75 additions & 14 deletions lib/utils/queryable.js
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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
Expand All @@ -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 = []
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand All @@ -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.`),
ruyadorno marked this conversation as resolved.
Show resolved Hide resolved
{ code: 'ENOADDPROP' }
)
}

return typeof _data[_key] === 'object' ? _data[_key] || {} : {}
}

Expand Down
32 changes: 32 additions & 0 deletions test/lib/pkg.js
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,38 @@ t.test('set single field', t => {
})
})

t.test('push to array syntax', t => {
const json = {
name: 'foo',
version: '1.1.1',
keywords: [
'foo',
],
}
npm.localPrefix = t.testdir({
'package.json': JSON.stringify(json),
})

pkg.exec(['set', 'keywords[]=bar', 'keywords[]=baz'], err => {
if (err)
throw err

t.strictSame(
readPackageJson(),
{
...json,
keywords: [
'foo',
'bar',
'baz',
],
},
'should append to arrays using empty bracket syntax'
)
t.end()
})
})

t.test('set multiple fields', t => {
const json = {
name: 'foo',
Expand Down
Loading