compago-ajax
Advanced tools
Comparing version 1.0.1 to 2.0.0
{ | ||
"name": "compago-ajax", | ||
"version": "1.0.1", | ||
"description": "An AJAX storage engine for the Compago framework.", | ||
"version": "2.0.0", | ||
"description": "An Ajax storage engine for the Compago framework.", | ||
"main": "dist/node/index.js", | ||
@@ -26,12 +26,23 @@ "keywords": [ | ||
"license": "MIT", | ||
"jspm": { | ||
"main": "index", | ||
"format": "es6", | ||
"jspmNodeConversion": false, | ||
"directories": { | ||
"lib": "src" | ||
}, | ||
"dependencies": { | ||
"compago-listener": "npm:compago-listener@^1.0.4", | ||
"compago-model": "npm:compago-model@^1.0.1" | ||
"eslintConfig": { | ||
"extends": "airbnb-base", | ||
"rules": { | ||
"no-underscore-dangle": 0, | ||
"no-nested-ternary": 1, | ||
"no-plusplus": 0, | ||
"no-bitwise": 1, | ||
"eqeqeq": [ | ||
2, | ||
"smart" | ||
], | ||
"valid-jsdoc": [ | ||
2, | ||
{ | ||
"prefer": { | ||
"return": "returns" | ||
}, | ||
"requireReturnDescription": false, | ||
"requireParamDescription": false | ||
} | ||
] | ||
} | ||
@@ -41,29 +52,21 @@ }, | ||
"presets": [ | ||
"node5" | ||
"node6" | ||
] | ||
}, | ||
"eslintConfig": { | ||
"extends": "airbnb", | ||
"rules": { | ||
"no-nested-ternary": 1, | ||
"eqeqeq": [2, "smart"] | ||
} | ||
"dependencies": { | ||
"compago-listener": "^1.0.5" | ||
}, | ||
"devDependencies": { | ||
"babel-cli": "^6.3.17", | ||
"babel-preset-node5": "^10.1.2", | ||
"babel-cli": "^6.16.0", | ||
"babel-preset-node6": "^11.0.0", | ||
"codecov.io": "^0.1.6", | ||
"compago-view": "^1.0.0", | ||
"eslint": "^1.10.3", | ||
"eslint-config-airbnb": "^2.1.1", | ||
"eslint-plugin-react": "^3.13.1", | ||
"expect": "^1.13.4", | ||
"istanbul": "^1.0.0-alpha.2", | ||
"mocha": "^2.3.4", | ||
"sinon": "^1.17.2" | ||
}, | ||
"dependencies": { | ||
"compago-listener": "^1.0.4", | ||
"compago-model": "^1.0.1" | ||
"eslint": "^3.7.1", | ||
"eslint-config-airbnb-base": "^8.0.0", | ||
"eslint-plugin-import": "^2.0.1", | ||
"expect": "^1.20.2", | ||
"istanbul": "^1.1.0-alpha.1", | ||
"mkdirp": "^0.5.1", | ||
"mocha": "^3.1.2", | ||
"rimraf": "^2.5.4" | ||
} | ||
} |
127
src/index.js
@@ -1,6 +0,10 @@ | ||
import { Listener } from 'compago-listener'; | ||
import Model from 'compago-model'; | ||
/* eslint-env browser */ | ||
import Listener from 'compago-listener'; | ||
/** Used as a source of default options for methods to avoid creating new objects on every call. */ | ||
const _opt = Object.seal(Object.create(null)); | ||
/** | ||
* Facilitates interaction with a REST server through the XMLHttpRequest API. | ||
* Facilitates interaction with a REST server through the Fetch API. | ||
* | ||
@@ -13,7 +17,10 @@ * @mixes Listener | ||
* @param {Object} [options] | ||
* @param {string} [options.url] the URL for requests, by default uses the window's origin | ||
* @param {string} [options.url] the base URL for requests, by default uses the window's origin | ||
* @param {Object} [options.init] an options object for custom settings | ||
* to use as the `init` parameter in calls to the global fetch() | ||
*/ | ||
constructor(options = {}) { | ||
constructor({ url = window.location.origin, init } = _opt) { | ||
Object.assign(this, Listener); | ||
this.url = options.url || window.location.origin; | ||
this.url = url; | ||
this.init = init || { headers: { 'X-Requested-With': 'XMLHttpRequest' }, credentials: 'include' }; | ||
} | ||
@@ -25,3 +32,3 @@ | ||
* @param {Model} model the model to be checked | ||
* @return {boolean} True if the model is already stored on the server | ||
* @returns {boolean} True if the model is already stored on the server | ||
*/ | ||
@@ -36,67 +43,50 @@ static isStored(model) { | ||
* @param {string} method a method name to execute. | ||
* Internal method names are mapped to ajax methods in `Ajax.methods`. | ||
* Internal method names are mapped to HTTP methods in `Ajax.methods`. | ||
* @param {(Model|Collection)} model a model or a collection to by synchronized | ||
* @param {Object} options | ||
* @param {Function} options.success the function called if the request succeeds | ||
* @param {Function} options.error the function called if the server returns an error | ||
* @param {string} [options.url] a specific url for the request, in case it's different from the default url of the storage | ||
* @param {boolean} [options.silent] whether to avoid firing any events | ||
* @param {Boolean} [options.patch] whether to send only changed attributes (if present) using `PATCH` method | ||
* @return {XMLHttpRequest} an request object | ||
* @param {Boolean} [options.patch] whether to send only changed attributes (if present) | ||
* using the `PATCH` method | ||
* @param {string} [options.url] a specific url for the request, | ||
* in case it's different from the default url of the storage | ||
* @param {Object} [options.init] an options object for custom settings | ||
* to use as the `init` parameter in calls to the global fetch() | ||
* @returns {Promise} | ||
*/ | ||
sync(method, model, options) { | ||
sync(method, model, { silent, patch, url = this.url, init = this.init } = _opt) { | ||
const options = Object.assign({}, init); | ||
const methods = this.constructor.methods; | ||
let type = methods[method]; | ||
if (!type) return false; | ||
options.method = methods[method]; | ||
if (!options.method) return Promise.reject(new Error('Method is not found.')); | ||
const { patch, silent, success, error } = options; | ||
let { url = this.url } = options; | ||
const xhr = new XMLHttpRequest(); | ||
const isStored = this.constructor.isStored(model); | ||
const self = this; | ||
const changes = (patch && model.changes) ? model.changes : false; | ||
if (isStored) { | ||
url += '/' + model.id; | ||
if (method === 'write') type = changes ? methods.patch : methods.update; | ||
} else { | ||
if (method === 'erase') { | ||
success(); | ||
return false; | ||
} | ||
if (method === 'read' && (model instanceof Model)) { | ||
error('Model has not been saved yet.'); | ||
return false; | ||
} | ||
url += `/${model.id}`; | ||
if (method === 'write') options.method = changes ? methods.patch : methods.update; | ||
} | ||
options.xhr = xhr; | ||
options.model = model; | ||
xhr.open(type, url); | ||
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); | ||
let data; | ||
if (method === 'write') { | ||
xhr.setRequestHeader('Content-Type', 'application/json'); | ||
data = changes ? changes : model.toJSON(); | ||
options.headers['Content-Type'] = 'application/json'; | ||
options.body = JSON.stringify(changes || model.toJSON()); | ||
} | ||
xhr.onreadystatechange = () => { | ||
const status = xhr.status; | ||
if (xhr.readyState === 4) { | ||
if (!silent) self.emit('response', options); | ||
const response = Ajax._isJSONString(xhr.responseText) ? JSON.parse(xhr.responseText) : xhr.responseText; | ||
if ((status >= 200) && (status < 300 || status === 304)) { | ||
success(response); | ||
if (!silent) this.emit('request', { model, options }); | ||
return fetch(url, options) | ||
.then((response) => { | ||
if (!silent) this.emit('response', { model, options, response }); | ||
if (response.ok || response.status === 304) { | ||
const contentType = response.headers.get('content-type'); | ||
if (contentType && ~contentType.indexOf('application/json')) return response.json(); | ||
} else { | ||
error(response); | ||
const error = new Error(response.status); | ||
error.response = response; | ||
throw error; | ||
} | ||
} | ||
}; | ||
if (!silent) this.emit('before:request', options); | ||
xhr.send(JSON.stringify(data)); | ||
if (!silent) this.emit('request', options); | ||
return xhr; | ||
}) | ||
.catch((error) => { | ||
throw error; | ||
}); | ||
} | ||
@@ -109,3 +99,3 @@ | ||
* @param {boolean} [options.silent] whether to avoid emitting the `dispose` event. | ||
* @return {this} | ||
* @returns {this} | ||
*/ | ||
@@ -117,17 +107,2 @@ dispose(options = {}) { | ||
} | ||
/** | ||
* Checks whether the provided value is a valid JSON string | ||
* | ||
* @param {*} val the value to be checked | ||
* @return {Boolean} True if the value is a valid JSON string | ||
*/ | ||
static _isJSONString(val) { | ||
try { | ||
JSON.parse(val); | ||
} catch (e) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
} | ||
@@ -139,9 +114,9 @@ | ||
Ajax.methods = { | ||
'write': 'POST', | ||
'erase': 'DELETE', | ||
'read': 'GET', | ||
'update': 'PUT', | ||
'patch': 'PATCH', | ||
write: 'POST', | ||
erase: 'DELETE', | ||
read: 'GET', | ||
update: 'PUT', | ||
patch: 'PATCH', | ||
}; | ||
export default Ajax; |
/* eslint-env node, mocha */ | ||
import expect from 'expect'; | ||
import Ajax from '../src/index'; | ||
import Model from 'compago-model'; | ||
import expect from 'expect'; | ||
import sinon from 'sinon'; | ||
class MockResponse { | ||
constructor(status, headers, body) { | ||
this.status = status; | ||
this.headers = new Map(); | ||
Object.keys(headers).forEach((key) => { | ||
this.headers.set(key, headers[key]); | ||
}); | ||
this.body = body; | ||
} | ||
get ok() { | ||
return (this.status >= 200) && (this.status < 300); | ||
} | ||
json() { | ||
return this.body ? Promise.resolve(this.body) : Promise.reject(); | ||
} | ||
} | ||
describe('Ajax', () => { | ||
@@ -31,164 +47,145 @@ let storage; | ||
describe('sync', () => { | ||
let xhr; | ||
let model; | ||
let options; | ||
let requests; | ||
let response; | ||
beforeEach(() => { | ||
xhr = sinon.useFakeXMLHttpRequest(); | ||
global.XMLHttpRequest = xhr; | ||
requests = []; | ||
/* eslint no-shadow: 1*/ | ||
xhr.onCreate = (xhr) => { | ||
requests.push(xhr); | ||
}; | ||
model = { id: 42 }; | ||
model.toJSON = () => { | ||
return model; | ||
}; | ||
options = {}; | ||
options.success = expect.createSpy(); | ||
options.error = expect.createSpy(); | ||
model.toJSON = () => model; | ||
response = new MockResponse(200, { 'content-type': 'application/json' }, {}); | ||
}); | ||
afterEach(() => { | ||
xhr.restore(); | ||
it('rejects if an invalid method is used', () => { | ||
return storage.sync().catch((error) => { | ||
expect(error.message).toBe('Method is not found.'); | ||
}); | ||
}); | ||
it('returns `false` if no valid operation type is provided', () => { | ||
expect(storage.sync()).toBe(false); | ||
expect(requests.length).toBe(0); | ||
it('reads a model', () => { | ||
const result = { name: 'Arthur' }; | ||
response.body = result; | ||
global.fetch = expect.createSpy().andReturn(Promise.resolve(response)); | ||
return storage.sync('read', model).then((data) => { | ||
expect(global.fetch.calls.length).toBe(1); | ||
expect(global.fetch.calls[0].arguments).toEqual(['http://example.com/posts/42', { | ||
method: 'GET', | ||
headers: { 'X-Requested-With': 'XMLHttpRequest' }, | ||
credentials: 'include', | ||
}]); | ||
expect(data).toEqual(result); | ||
}); | ||
}); | ||
it('parses JSON responses', () => { | ||
storage.sync('read', model, options); | ||
requests[0].respond(200, { 'Content-Type': 'application/json' }, '{"name":"Arthur"}'); | ||
expect(options.success.calls[0].arguments).toEqual([{ name: 'Arthur' }]); | ||
expect(options.error).toNotHaveBeenCalled(); | ||
}); | ||
it('retrieves a model', () => { | ||
storage.sync('read', model, options); | ||
expect(requests.length).toBe(1); | ||
expect(requests[0].method).toBe('GET'); | ||
expect(options.success).toNotHaveBeenCalled(); | ||
requests[0].respond(200, { 'Content-Type': 'text/plain' }, 'Ok'); | ||
expect(options.success).toHaveBeenCalledWith('Ok'); | ||
expect(options.error).toNotHaveBeenCalled(); | ||
}); | ||
it('invokes `option.error` when attempts to retrieve an unsaved model', () => { | ||
const m = new Model(); | ||
storage.sync('read', m, options); | ||
expect(options.error).toHaveBeenCalled(); | ||
expect(requests[0].readyState).toBe(0); | ||
}); | ||
it('retrieves all models in a collection', () => { | ||
it('reads all models in a collection', () => { | ||
const collection = {}; | ||
storage.sync('read', collection, options); | ||
expect(requests[0].url).toBe('http://example.com/posts'); | ||
expect(requests[0].method).toBe('GET'); | ||
expect(options.success).toNotHaveBeenCalled(); | ||
requests[0].respond(200, { 'Content-Type': 'text/plain' }, 'Ok'); | ||
expect(options.success).toHaveBeenCalledWith('Ok'); | ||
expect(options.error).toNotHaveBeenCalled(); | ||
response.headers.set('content-type', 'text/plain'); | ||
global.fetch = expect.createSpy().andReturn(Promise.resolve(response)); | ||
return storage.sync('read', collection).then(() => { | ||
expect(global.fetch.calls.length).toBe(1); | ||
expect(global.fetch.calls[0].arguments).toEqual(['http://example.com/posts', { | ||
method: 'GET', | ||
headers: { 'X-Requested-With': 'XMLHttpRequest' }, | ||
credentials: 'include', | ||
}]); | ||
}); | ||
}); | ||
it('saves a model', () => { | ||
it('creates a model', () => { | ||
delete model.id; | ||
model.name = 'Arthur'; | ||
storage.sync('write', model, options); | ||
expect(requests[0].url).toBe('http://example.com/posts'); | ||
expect(requests[0].method).toBe('POST'); | ||
expect(requests[0].requestBody).toEqual('{"name":"Arthur"}'); | ||
const result = { name: 'Arthur', id: 1 }; | ||
response.body = result; | ||
global.fetch = expect.createSpy().andReturn(Promise.resolve(response)); | ||
return storage.sync('write', model).then((data) => { | ||
expect(global.fetch.calls.length).toBe(1); | ||
expect(global.fetch.calls[0].arguments).toEqual(['http://example.com/posts', { | ||
method: 'POST', | ||
body: JSON.stringify(model), | ||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json' }, | ||
credentials: 'include', | ||
}]); | ||
expect(data).toEqual(result); | ||
}); | ||
}); | ||
it('updates a model', () => { | ||
storage.sync('write', model, options); | ||
expect(requests[0].url).toBe('http://example.com/posts/42'); | ||
expect(requests[0].method).toBe('PUT'); | ||
expect(requests[0].requestBody).toEqual('{"id":42}'); | ||
global.fetch = expect.createSpy().andReturn(Promise.resolve(response)); | ||
return storage.sync('write', model).then(() => { | ||
expect(global.fetch.calls.length).toBe(1); | ||
expect(global.fetch.calls[0].arguments).toEqual(['http://example.com/posts/42', { | ||
method: 'PUT', | ||
body: JSON.stringify(model), | ||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json' }, | ||
credentials: 'include', | ||
}]); | ||
}); | ||
}); | ||
it('sends only changes if `patch:true`', () => { | ||
it('patches a model if `patch:true` and model.changes are present', () => { | ||
model.changes = { name: 'Arthur' }; | ||
options.patch = true; | ||
storage.sync('write', model, options); | ||
expect(requests[0].url).toBe('http://example.com/posts/42'); | ||
expect(requests[0].method).toBe('PATCH'); | ||
expect(requests[0].requestBody).toEqual('{"name":"Arthur"}'); | ||
global.fetch = expect.createSpy().andReturn(Promise.resolve(response)); | ||
return storage.sync('write', model, { patch: true }).then(() => { | ||
expect(global.fetch.calls.length).toBe(1); | ||
expect(global.fetch.calls[0].arguments).toEqual(['http://example.com/posts/42', { | ||
method: 'PATCH', | ||
body: JSON.stringify(model.changes), | ||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json' }, | ||
credentials: 'include', | ||
}]); | ||
}); | ||
}); | ||
it('sends all attributes if no changes found despite `patch:true`', () => { | ||
options.patch = true; | ||
storage.sync('write', model, options); | ||
expect(requests[0].url).toBe('http://example.com/posts/42'); | ||
expect(requests[0].method).toBe('PUT'); | ||
expect(requests[0].requestBody).toEqual('{"id":42}'); | ||
model.changes = false; | ||
it('deletes a model', () => { | ||
global.fetch = expect.createSpy().andReturn(Promise.resolve(response)); | ||
return storage.sync('erase', model).then(() => { | ||
expect(global.fetch.calls.length).toBe(1); | ||
expect(global.fetch.calls[0].arguments).toEqual(['http://example.com/posts/42', { | ||
method: 'DELETE', | ||
headers: { 'X-Requested-With': 'XMLHttpRequest' }, | ||
credentials: 'include', | ||
}]); | ||
}); | ||
}); | ||
it('deletes a single model', () => { | ||
storage.sync('erase', model, options); | ||
expect(requests[0].url).toBe('http://example.com/posts/42'); | ||
expect(requests[0].method).toBe('DELETE'); | ||
it('rejects if server returns an error', () => { | ||
response.status = 404; | ||
response.headers.set('content-type', 'text/plain'); | ||
global.fetch = expect.createSpy().andReturn(Promise.resolve(response)); | ||
return storage.sync('read', model).catch((error) => { | ||
expect(global.fetch.calls.length).toBe(1); | ||
expect(global.fetch.calls[0].arguments).toEqual(['http://example.com/posts/42', { | ||
method: 'GET', | ||
headers: { 'X-Requested-With': 'XMLHttpRequest' }, | ||
credentials: 'include', | ||
}]); | ||
expect(error.message).toBe('404'); | ||
}); | ||
}); | ||
it('invokes `option.success` when attempts to delete an unsaved model', () => { | ||
delete model.id; | ||
storage.sync('erase', model, options); | ||
expect(options.success).toHaveBeenCalled(); | ||
expect(requests[0].readyState).toBe(0); | ||
}); | ||
it('invokes `options.error` if a error is returned by the server', () => { | ||
storage.sync('read', model, options); | ||
expect(requests[0].url).toBe('http://example.com/posts/42'); | ||
expect(requests[0].method).toBe('GET'); | ||
expect(options.success).toNotHaveBeenCalled(); | ||
requests[0].respond(404, { 'Content-Type': 'text/plain' }, 'Not Ok'); | ||
expect(options.success).toNotHaveBeenCalled(); | ||
expect(options.error).toHaveBeenCalledWith('Not Ok'); | ||
}); | ||
it('fires `before:request`, `request`, and `response` events unless `silent:true`', () => { | ||
it('fires `request` and `response` events unless `silent:true`', () => { | ||
storage.someMethod = expect.createSpy(); | ||
storage.otherMethod = expect.createSpy(); | ||
storage.anotherMethod = expect.createSpy(); | ||
storage.on(storage, 'before:request', storage.someMethod); | ||
storage.on(storage, 'request', storage.otherMethod); | ||
storage.on(storage, 'response', storage.anotherMethod); | ||
storage.sync('read', model, options); | ||
requests[0].respond(200, { 'Content-Type': 'text/plain' }, 'Not Ok'); | ||
expect(storage.someMethod).toHaveBeenCalled(); | ||
expect(storage.otherMethod).toHaveBeenCalled(); | ||
expect(storage.anotherMethod).toHaveBeenCalled(); | ||
storage.on(storage, 'request', storage.someMethod); | ||
storage.on(storage, 'response', storage.otherMethod); | ||
global.fetch = expect.createSpy().andReturn(Promise.resolve(response)); | ||
return storage.sync('read', model).then(() => { | ||
expect(storage.someMethod).toHaveBeenCalled(); | ||
expect(storage.otherMethod).toHaveBeenCalled(); | ||
}); | ||
}); | ||
it('does not fire events if `silent:true`', () => { | ||
it('does not fire any events if `silent:true`', () => { | ||
storage.someMethod = expect.createSpy(); | ||
storage.otherMethod = expect.createSpy(); | ||
storage.anotherMethod = expect.createSpy(); | ||
storage.on(storage, 'before:request', storage.someMethod); | ||
storage.on(storage, 'request', storage.otherMethod); | ||
storage.on(storage, 'response', storage.anotherMethod); | ||
options.silent = true; | ||
storage.sync('read', model, options); | ||
requests[0].respond(200, { 'Content-Type': 'text/plain' }, 'Not Ok'); | ||
expect(storage.someMethod).toNotHaveBeenCalled(); | ||
expect(storage.otherMethod).toNotHaveBeenCalled(); | ||
expect(storage.anotherMethod).toNotHaveBeenCalled(); | ||
storage.on(storage, 'request', storage.someMethod); | ||
storage.on(storage, 'response', storage.otherMethod); | ||
global.fetch = expect.createSpy().andReturn(Promise.resolve(response)); | ||
return storage.sync('read', model, { silent: true }).then(() => { | ||
expect(storage.someMethod).toNotHaveBeenCalled(); | ||
expect(storage.otherMethod).toNotHaveBeenCalled(); | ||
}); | ||
}); | ||
}); | ||
describe('_isJSONString', () => { | ||
it('checks whether the passed argument is a JSON string or not', () => { | ||
expect(Ajax._isJSONString('{"name":"Arthur"}')).toBe(true); | ||
expect(Ajax._isJSONString('Arthur')).toBe(false); | ||
}); | ||
}); | ||
describe('dispose', () => { | ||
it('prepares the storage controller to be disposed', () => { | ||
it('prepares the storage to be disposed', () => { | ||
storage.on(storage, 'dispose', () => { | ||
@@ -202,2 +199,3 @@ }); | ||
}); | ||
it('fires `dispose` event unless `silent:true`', () => { | ||
@@ -204,0 +202,0 @@ storage.someMethod = expect.createSpy(); |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1
15055
290
2
- Removedcompago-model@^1.0.1
- Removedcall-bind@1.0.7(transitive)
- Removedclone@1.0.4(transitive)
- Removedcompago-model@1.0.2(transitive)
- Removeddeep-equal@1.1.2(transitive)
- Removeddefine-data-property@1.1.4(transitive)
- Removeddefine-properties@1.2.1(transitive)
- Removedes-define-property@1.0.0(transitive)
- Removedes-errors@1.3.0(transitive)
- Removedfunction-bind@1.1.2(transitive)
- Removedfunctions-have-names@1.2.3(transitive)
- Removedget-intrinsic@1.2.4(transitive)
- Removedgopd@1.0.1(transitive)
- Removedhas-property-descriptors@1.0.2(transitive)
- Removedhas-proto@1.0.3(transitive)
- Removedhas-symbols@1.0.3(transitive)
- Removedhas-tostringtag@1.0.2(transitive)
- Removedhasown@2.0.2(transitive)
- Removedis-arguments@1.1.1(transitive)
- Removedis-date-object@1.0.5(transitive)
- Removedis-regex@1.1.4(transitive)
- Removedobject-is@1.1.6(transitive)
- Removedobject-keys@1.1.1(transitive)
- Removedregexp.prototype.flags@1.5.3(transitive)
- Removedset-function-length@1.2.2(transitive)
- Removedset-function-name@2.0.2(transitive)
Updatedcompago-listener@^1.0.5