diff --git a/.travis.yml b/.travis.yml index ad846a9..22a1218 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ dist: trusty before_script: - npm install - 'export PATH=$PWD/node_modules/.bin:$PATH' -node_js: lts/* +node_js: node script: - npm run test - npm run bench diff --git a/README.md b/README.md index eeb49ee..0b6ffef 100644 --- a/README.md +++ b/README.md @@ -44,23 +44,26 @@ $ npm install jsonpatcherproxy --save ### In a web browser -* Include `dist/jsonpatcherproxy.min.js`, as in: +Load the bundled distribution script: + +```html + ``` - + +In [browsers that support ECMAScript modules](https://caniuse.com/#feat=es6-module), the below code uses this library as a module: + +```html + ``` **You can use rawgit.com as a CDN**. ### In Node.js -Call require to get the instance: - -```js -var JSONPatcherProxy = require('jsonpatcherproxy'); -``` -Or in ES6 and TS: ```js -import JSONPatcherProxy from 'jsonpatcherproxy'; +import { JSONPatcherProxy } from 'jsonpatcherproxy'; ``` ## Usage diff --git a/dist/jsonpatcherproxy.js b/dist/jsonpatcherproxy.js index 8233157..17cc1a2 100644 --- a/dist/jsonpatcherproxy.js +++ b/dist/jsonpatcherproxy.js @@ -88,15 +88,21 @@ var JSONPatcherProxy = /************************************************************************/ /******/ ([ /* 0 */ -/***/ (function(module, exports, __webpack_require__) { +/***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "JSONPatcherProxy", function() { return JSONPatcherProxy; }); /*! * https://github.com/Palindrom/JSONPatcherProxy - * (c) 2017 Starcounter + * (c) 2017 Starcounter * MIT license + * + * Vocabulary used in this file: + * * root - root object that is deeply observed by JSONPatcherProxy + * * tree - any subtree within the root or the root */ /** Class representing a JS Object observer */ @@ -124,16 +130,16 @@ const JSONPatcherProxy = (function() { /** * Walk up the parenthood tree to get the path - * @param {JSONPatcherProxy} instance - * @param {Object} obj the object you need to find its path + * @param {JSONPatcherProxy} instance + * @param {Object} tree the object you need to find its path */ - function findObjectPath(instance, obj) { + function getPathToTree(instance, tree) { const pathComponents = []; - let parentAndPath = instance.parenthoodMap.get(obj); - while (parentAndPath && parentAndPath.path) { + let parenthood = instance._parenthoodMap.get(tree); + while (parenthood && parenthood.key) { // because we're walking up-tree, we need to use the array as a stack - pathComponents.unshift(parentAndPath.path); - parentAndPath = instance.parenthoodMap.get(parentAndPath.parent); + pathComponents.unshift(parenthood.key); + parenthood = instance._parenthoodMap.get(parenthood.parent); } if (pathComponents.length) { const path = pathComponents.join('/'); @@ -142,25 +148,19 @@ const JSONPatcherProxy = (function() { return ''; } /** - * A callback to be used as th proxy set trap callback. - * It updates parenthood map if needed, proxifies nested newly-added objects, calls default callbacks with the changes occurred. + * A callback to be used as the proxy set trap callback. + * It updates parenthood map if needed, proxifies nested newly-added objects, calls default callback with the changes occurred. * @param {JSONPatcherProxy} instance JSONPatcherProxy instance - * @param {Object} target the affected object + * @param {Object} tree the affected object * @param {String} key the effect property's name * @param {Any} newValue the value being set */ - function setTrap(instance, target, key, newValue) { - const parentPath = findObjectPath(instance, target); + function trapForSet(instance, tree, key, newValue) { + const pathToKey = getPathToTree(instance, tree) + '/' + escapePathComponent(key); + const subtreeMetadata = instance._treeMetadataMap.get(newValue); - const destinationPropKey = parentPath + '/' + escapePathComponent(key); - - if (instance.proxifiedObjectsMap.has(newValue)) { - const newValueOriginalObject = instance.proxifiedObjectsMap.get(newValue); - - instance.parenthoodMap.set(newValueOriginalObject.originalObject, { - parent: target, - path: key - }); + if (instance._treeMetadataMap.has(newValue)) { + instance._parenthoodMap.set(subtreeMetadata.originalObject, { parent: tree, key }); } /* mark already proxified values as inherited. @@ -172,210 +172,246 @@ const JSONPatcherProxy = (function() { by default, the second operation would revoke the proxy, and this renders arr revoked. That's why we need to remember the proxies that are inherited. */ - const revokableInstance = instance.proxifiedObjectsMap.get(newValue); /* - Why do we need to check instance.isProxifyingTreeNow? + Why do we need to check instance._isProxifyingTreeNow? - We need to make sure we mark revokables as inherited ONLY when we're observing, - because throughout the first proxification, a sub-object is proxified and then assigned to + We need to make sure we mark revocables as inherited ONLY when we're observing, + because throughout the first proxification, a sub-object is proxified and then assigned to its parent object. This assignment of a pre-proxified object can fool us into thinking - that it's a proxified object moved around, while in fact it's the first assignment ever. + that it's a proxified object moved around, while in fact it's the first assignment ever. - Checking isProxifyingTreeNow ensures this is not happening in the first proxification, + Checking _isProxifyingTreeNow ensures this is not happening in the first proxification, but in fact is is a proxified object moved around the tree */ - if (revokableInstance && !instance.isProxifyingTreeNow) { - revokableInstance.inherited = true; + if (subtreeMetadata && !instance._isProxifyingTreeNow) { + subtreeMetadata.inherited = true; } + let warnedAboutNonIntegrerArrayProp = false; + const isTreeAnArray = Array.isArray(tree); + const isNonSerializableArrayProperty = isTreeAnArray && !Number.isInteger(+key.toString()); + // if the new value is an object, make sure to watch it if ( newValue && typeof newValue == 'object' && - !instance.proxifiedObjectsMap.has(newValue) + !instance._treeMetadataMap.has(newValue) ) { - instance.parenthoodMap.set(newValue, { - parent: target, - path: key - }); - newValue = instance._proxifyObjectTreeRecursively(target, newValue, key); + if (isNonSerializableArrayProperty) { + // This happens in Vue 1-2 (should not happen in Vue 3). See: https://github.com/vuejs/vue/issues/427, https://github.com/vuejs/vue/issues/9259 + console.warn(`JSONPatcherProxy noticed a non-integer property ('${key}') was set for an array. This interception will not emit a patch. The value is an object, but it was not proxified, because it would not be addressable in JSON-Pointer`); + warnedAboutNonIntegrerArrayProp = true; + } + else { + instance._parenthoodMap.set(newValue, { parent: tree, key }); + newValue = instance._proxifyTreeRecursively(tree, newValue, key); + } } // let's start with this operation, and may or may not update it later + const valueBeforeReflection = tree[key]; + const wasKeyInTreeBeforeReflection = tree.hasOwnProperty(key); + if (isTreeAnArray && !isNonSerializableArrayProperty) { + const index = parseInt(key, 10); + if (index > tree.length) { + // force call trapForSet for implicit undefined elements of the array added by the JS engine + // because JSON-Patch spec prohibits adding an index that is higher than array.length + trapForSet(instance, tree, (index - 1) + '', undefined); + } + } + const reflectionResult = Reflect.set(tree, key, newValue); const operation = { op: 'remove', - path: destinationPropKey + path: pathToKey }; if (typeof newValue == 'undefined') { // applying De Morgan's laws would be a tad faster, but less readable - if (!Array.isArray(target) && !target.hasOwnProperty(key)) { + if (!isTreeAnArray && !wasKeyInTreeBeforeReflection) { // `undefined` is being set to an already undefined value, keep silent - return Reflect.set(target, key, newValue); + return reflectionResult; } else { + if (wasKeyInTreeBeforeReflection && !isSignificantChange(valueBeforeReflection, newValue, isTreeAnArray)) { + return reflectionResult; // Value wasn't actually changed with respect to its JSON projection + } // when array element is set to `undefined`, should generate replace to `null` - if (Array.isArray(target)) { - // undefined array elements are JSON.stringified to `null` - (operation.op = 'replace'), (operation.value = null); + if (isTreeAnArray) { + operation.value = null; + if (wasKeyInTreeBeforeReflection) { + operation.op = 'replace'; + } + else { + operation.op = 'add'; + } } - const oldValue = instance.proxifiedObjectsMap.get(target[key]); - // was the deleted a proxified object? - if (oldValue) { - instance.parenthoodMap.delete(target[key]); - instance.disableTrapsForProxy(oldValue); - instance.proxifiedObjectsMap.delete(oldValue); + const oldSubtreeMetadata = instance._treeMetadataMap.get(valueBeforeReflection); + if (oldSubtreeMetadata) { + //TODO there is no test for this! + instance._parenthoodMap.delete(valueBeforeReflection); + instance._disableTrapsForTreeMetadata(oldSubtreeMetadata); + instance._treeMetadataMap.delete(oldSubtreeMetadata); } } } else { - if (Array.isArray(target) && !Number.isInteger(+key.toString())) { + if (isNonSerializableArrayProperty) { /* array props (as opposed to indices) don't emit any patches, to avoid needless `length` patches */ - if(key != 'length') { - console.warn('JSONPatcherProxy noticed a non-integer prop was set for an array. This will not emit a patch'); + if(key != 'length' && !warnedAboutNonIntegrerArrayProp) { + console.warn(`JSONPatcherProxy noticed a non-integer property ('${key}') was set for an array. This interception will not emit a patch`); } - return Reflect.set(target, key, newValue); + return reflectionResult; } operation.op = 'add'; - if (target.hasOwnProperty(key)) { - if (typeof target[key] !== 'undefined' || Array.isArray(target)) { + if (wasKeyInTreeBeforeReflection) { + if (typeof valueBeforeReflection !== 'undefined' || isTreeAnArray) { + if (!isSignificantChange(valueBeforeReflection, newValue, isTreeAnArray)) { + return reflectionResult; // Value wasn't actually changed with respect to its JSON projection + } operation.op = 'replace'; // setting `undefined` array elements is a `replace` op } } operation.value = newValue; } - const reflectionResult = Reflect.set(target, key, newValue); - instance.defaultCallback(operation); + instance._defaultCallback(operation); return reflectionResult; } /** - * A callback to be used as th proxy delete trap callback. + * Test if replacing old value with new value is a significant change, i.e. whether or not + * it soiuld result in a patch being generated. + * @param {*} oldValue old value + * @param {*} newValue new value + * @param {boolean} isTreeAnArray value resides in an array + */ + function isSignificantChange(oldValue, newValue, isTreeAnArray) { + if (isTreeAnArray) { + return isSignificantChangeInArray(oldValue, newValue); + } else { + return isSignificantChangeInObject(oldValue, newValue); + } + } + /** + * Test if replacing old value with new value is a significant change in an object, i.e. + * whether or not it should result in a patch being generated. + * @param {*} oldValue old value + * @param {*} newValue new value + */ + function isSignificantChangeInObject(oldValue, newValue) { + return oldValue !== newValue; + } + /** + * Test if replacing old value with new value is a significant change in an array, i.e. + * whether or not it should result in a patch being generated. + * @param {*} oldValue old value + * @param {*} newValue new value + */ + function isSignificantChangeInArray(oldValue, newValue) { + if (typeof oldValue === 'undefined') { + oldValue = null; + } + if (typeof newValue === 'undefined') { + newValue = null; + } + return oldValue !== newValue; + } + /** + * A callback to be used as the proxy delete trap callback. * It updates parenthood map if needed, calls default callbacks with the changes occurred. * @param {JSONPatcherProxy} instance JSONPatcherProxy instance - * @param {Object} target the effected object + * @param {Object} tree the effected object * @param {String} key the effected property's name */ - function deleteTrap(instance, target, key) { - if (typeof target[key] !== 'undefined') { - const parentPath = findObjectPath(instance, target); - const destinationPropKey = parentPath + '/' + escapePathComponent(key); - - const revokableProxyInstance = instance.proxifiedObjectsMap.get( - target[key] - ); + function trapForDeleteProperty(instance, tree, key) { + const oldValue = tree[key]; + const reflectionResult = Reflect.deleteProperty(tree, key); + if (typeof oldValue !== 'undefined') { + const pathToKey = getPathToTree(instance, tree) + '/' + escapePathComponent(key); + const subtreeMetadata = instance._treeMetadataMap.get(oldValue); - if (revokableProxyInstance) { - if (revokableProxyInstance.inherited) { + if (subtreeMetadata) { + if (subtreeMetadata.inherited) { /* - this is an inherited proxy (an already proxified object that was moved around), + this is an inherited proxy (an already proxified object that was moved around), we shouldn't revoke it, because even though it was removed from path1, it is still used in path2. And we know that because we mark moved proxies with `inherited` flag when we move them - it is a good idea to remove this flag if we come across it here, in deleteProperty trap. + it is a good idea to remove this flag if we come across it here, in trapForDeleteProperty. We DO want to revoke the proxy if it was removed again. */ - revokableProxyInstance.inherited = false; + subtreeMetadata.inherited = false; } else { - instance.parenthoodMap.delete(revokableProxyInstance.originalObject); - instance.disableTrapsForProxy(revokableProxyInstance); - instance.proxifiedObjectsMap.delete(target[key]); + instance._parenthoodMap.delete(subtreeMetadata.originalObject); + instance._disableTrapsForTreeMetadata(subtreeMetadata); + instance._treeMetadataMap.delete(oldValue); } } - const reflectionResult = Reflect.deleteProperty(target, key); - instance.defaultCallback({ + instance._defaultCallback({ op: 'remove', - path: destinationPropKey + path: pathToKey }); - - return reflectionResult; } - } - /* pre-define resume and pause functions to enhance constructors performance */ - function resume() { - this.defaultCallback = operation => { - this.isRecording && this.patches.push(operation); - this.userCallback && this.userCallback(operation); - }; - this.isObserving = true; - } - function pause() { - this.defaultCallback = () => {}; - this.isObserving = false; + return reflectionResult; } /** - * Creates an instance of JSONPatcherProxy around your object of interest `root`. + * Creates an instance of JSONPatcherProxy around your object of interest `root`. * @param {Object|Array} root - the object you want to wrap - * @param {Boolean} [showDetachedWarning = true] - whether to log a warning when a detached sub-object is modified @see {@link https://github.com/Palindrom/JSONPatcherProxy#detached-objects} + * @param {Boolean} [showDetachedWarning = true] - whether to log a warning when a detached sub-object is modified @see {@link https://github.com/Palindrom/JSONPatcherProxy#detached-objects} * @returns {JSONPatcherProxy} * @constructor */ function JSONPatcherProxy(root, showDetachedWarning) { - this.isProxifyingTreeNow = false; - this.isObserving = false; - this.proxifiedObjectsMap = new Map(); - this.parenthoodMap = new Map(); + this._isProxifyingTreeNow = false; + this._isObserving = false; + this._treeMetadataMap = new Map(); + this._parenthoodMap = new Map(); // default to true if (typeof showDetachedWarning !== 'boolean') { showDetachedWarning = true; } - this.showDetachedWarning = showDetachedWarning; - this.originalObject = root; - this.cachedProxy = null; - this.isRecording = false; - this.userCallback; - /** - * @memberof JSONPatcherProxy - * Restores callback back to the original one provided to `observe`. - */ - this.resume = resume.bind(this); - /** - * @memberof JSONPatcherProxy - * Replaces your callback with a noop function. - */ - this.pause = pause.bind(this); + this._showDetachedWarning = showDetachedWarning; + this._originalRoot = root; + this._cachedProxy = null; + this._isRecording = false; + this._userCallback; + this._defaultCallback; + this._patches; } - JSONPatcherProxy.prototype.generateProxyAtPath = function(parent, obj, path) { - if (!obj) { - return obj; + JSONPatcherProxy.prototype._generateProxyAtKey = function(parent, tree, key) { + if (!tree) { + return tree; } - const traps = { - set: (target, key, value, receiver) => - setTrap(this, target, key, value, receiver), - deleteProperty: (target, key) => deleteTrap(this, target, key) + const handler = { + set: (...args) => trapForSet(this, ...args), + deleteProperty: (...args) => trapForDeleteProperty(this, ...args) }; - const revocableInstance = Proxy.revocable(obj, traps); - // cache traps object to disable them later. - revocableInstance.trapsInstance = traps; - revocableInstance.originalObject = obj; + const treeMetadata = Proxy.revocable(tree, handler); + // cache the object that contains traps to disable them later. + treeMetadata.handler = handler; + treeMetadata.originalObject = tree; - /* keeping track of object's parent and path */ - - this.parenthoodMap.set(obj, { parent, path }); + /* keeping track of the object's parent and the key within the parent */ + this._parenthoodMap.set(tree, { parent, key }); /* keeping track of all the proxies to be able to revoke them later */ - this.proxifiedObjectsMap.set(revocableInstance.proxy, revocableInstance); - return revocableInstance.proxy; + this._treeMetadataMap.set(treeMetadata.proxy, treeMetadata); + return treeMetadata.proxy; }; // grab tree's leaves one by one, encapsulate them into a proxy and return - JSONPatcherProxy.prototype._proxifyObjectTreeRecursively = function( - parent, - root, - path - ) { - for (let key in root) { - if (root.hasOwnProperty(key)) { - if (root[key] instanceof Object) { - root[key] = this._proxifyObjectTreeRecursively( - root, - root[key], + JSONPatcherProxy.prototype._proxifyTreeRecursively = function(parent, tree, key) { + for (let key in tree) { + if (tree.hasOwnProperty(key)) { + if (tree[key] instanceof Object) { + tree[key] = this._proxifyTreeRecursively( + tree, + tree[key], escapePathComponent(key) ); } } } - return this.generateProxyAtPath(parent, root, path); + return this._generateProxyAtKey(parent, tree, key); }; // this function is for aesthetic purposes - JSONPatcherProxy.prototype.proxifyObjectTree = function(root) { + JSONPatcherProxy.prototype._proxifyRoot = function(root) { /* while proxifying object tree, the proxifying operation itself is being @@ -384,54 +420,52 @@ const JSONPatcherProxy = (function() { initial process; */ this.pause(); - this.isProxifyingTreeNow = true; - const proxifiedObject = this._proxifyObjectTreeRecursively( + this._isProxifyingTreeNow = true; + const proxifiedRoot = this._proxifyTreeRecursively( undefined, root, '' ); /* OK you can record now */ - this.isProxifyingTreeNow = false; + this._isProxifyingTreeNow = false; this.resume(); - return proxifiedObject; + return proxifiedRoot; }; /** * Turns a proxified object into a forward-proxy object; doesn't emit any patches anymore, like a normal object - * @param {Proxy} proxy - The target proxy object + * @param {Object} treeMetadata */ - JSONPatcherProxy.prototype.disableTrapsForProxy = function( - revokableProxyInstance - ) { - if (this.showDetachedWarning) { + JSONPatcherProxy.prototype._disableTrapsForTreeMetadata = function(treeMetadata) { + if (this._showDetachedWarning) { const message = "You're accessing an object that is detached from the observedObject tree, see https://github.com/Palindrom/JSONPatcherProxy#detached-objects"; - revokableProxyInstance.trapsInstance.set = ( - targetObject, - propKey, + treeMetadata.handler.set = ( + parent, + key, newValue ) => { console.warn(message); - return Reflect.set(targetObject, propKey, newValue); + return Reflect.set(parent, key, newValue); }; - revokableProxyInstance.trapsInstance.set = ( - targetObject, - propKey, + treeMetadata.handler.set = ( + parent, + key, newValue ) => { console.warn(message); - return Reflect.set(targetObject, propKey, newValue); + return Reflect.set(parent, key, newValue); }; - revokableProxyInstance.trapsInstance.deleteProperty = ( - targetObject, - propKey + treeMetadata.handler.deleteProperty = ( + parent, + key ) => { - return Reflect.deleteProperty(targetObject, propKey); + return Reflect.deleteProperty(parent, key); }; } else { - delete revokableProxyInstance.trapsInstance.set; - delete revokableProxyInstance.trapsInstance.get; - delete revokableProxyInstance.trapsInstance.deleteProperty; + delete treeMetadata.handler.set; + delete treeMetadata.handler.get; + delete treeMetadata.handler.deleteProperty; } }; /** @@ -443,32 +477,32 @@ const JSONPatcherProxy = (function() { if (!record && !callback) { throw new Error('You need to either record changes or pass a callback'); } - this.isRecording = record; - this.userCallback = callback; + this._isRecording = record; + this._userCallback = callback; /* I moved it here to remove it from `unobserve`, this will also make the constructor faster, why initiate the array before they decide to actually observe with recording? They might need to use only a callback. */ - if (record) this.patches = []; - this.cachedProxy = this.proxifyObjectTree(this.originalObject); - return this.cachedProxy; + if (record) this._patches = []; + this._cachedProxy = this._proxifyRoot(this._originalRoot); + return this._cachedProxy; }; /** * If the observed is set to record, it will synchronously return all the patches and empties patches array. */ JSONPatcherProxy.prototype.generate = function() { - if (!this.isRecording) { + if (!this._isRecording) { throw new Error('You should set record to true to get patches later'); } - return this.patches.splice(0, this.patches.length); + return this._patches.splice(0, this._patches.length); }; /** - * Revokes all proxies rendering the observed object useless and good for garbage collection @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/revocable} + * Revokes all proxies, rendering the observed object useless and good for garbage collection @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/revocable} */ JSONPatcherProxy.prototype.revoke = function() { - this.proxifiedObjectsMap.forEach(el => { + this._treeMetadataMap.forEach(el => { el.revoke(); }); }; @@ -476,15 +510,29 @@ const JSONPatcherProxy = (function() { * Disables all proxies' traps, turning the observed object into a forward-proxy object, like a normal object that you can modify silently. */ JSONPatcherProxy.prototype.disableTraps = function() { - this.proxifiedObjectsMap.forEach(this.disableTrapsForProxy, this); + this._treeMetadataMap.forEach(this._disableTrapsForTreeMetadata, this); + }; + /** + * Restores callback back to the original one provided to `observe`. + */ + JSONPatcherProxy.prototype.resume = function() { + this._defaultCallback = operation => { + this._isRecording && this._patches.push(operation); + this._userCallback && this._userCallback(operation); + }; + this._isObserving = true; }; + /** + * Replaces callback with a noop function. + */ + JSONPatcherProxy.prototype.pause = function() { + this._defaultCallback = () => {}; + this._isObserving = false; + } return JSONPatcherProxy; })(); -if (true) { - module.exports = JSONPatcherProxy; - module.exports.default = JSONPatcherProxy; -} + /***/ }) diff --git a/dist/jsonpatcherproxy.min.js b/dist/jsonpatcherproxy.min.js index b032b72..4954893 100644 --- a/dist/jsonpatcherproxy.min.js +++ b/dist/jsonpatcherproxy.min.js @@ -1,7 +1,12 @@ /*! JSONPatcherProxy version: 0.0.10 */ -var JSONPatcherProxy=function(e){var t={};function r(o){if(t[o])return t[o].exports;var i=t[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,r),i.l=!0,i.exports}return r.m=e,r.c=t,r.d=function(e,t,o){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(r.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)r.d(o,i,function(t){return e[t]}.bind(null,i));return o},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t,r){"use strict"; +var JSONPatcherProxy=function(e){var t={};function r(o){if(t[o])return t[o].exports;var n=t[o]={i:o,l:!1,exports:{}};return e[o].call(n.exports,n,n.exports,r),n.l=!0,n.exports}return r.m=e,r.c=t,r.d=function(e,t,o){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(r.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)r.d(o,n,function(t){return e[t]}.bind(null,n));return o},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t,r){"use strict";r.r(t),r.d(t,"JSONPatcherProxy",function(){return o}); /*! * https://github.com/Palindrom/JSONPatcherProxy - * (c) 2017 Starcounter + * (c) 2017 Starcounter * MIT license - */const o=function(){function e(e){return-1==e.indexOf("/")&&-1==e.indexOf("~")?e:e.replace(/~/g,"~0").replace(/\//g,"~1")}function t(e,t){const r=[];let o=e.parenthoodMap.get(t);for(;o&&o.path;)r.unshift(o.path),o=e.parenthoodMap.get(o.parent);if(r.length){return"/"+r.join("/")}return""}function r(e,t){this.isProxifyingTreeNow=!1,this.isObserving=!1,this.proxifiedObjectsMap=new Map,this.parenthoodMap=new Map,"boolean"!=typeof t&&(t=!0),this.showDetachedWarning=t,this.originalObject=e,this.cachedProxy=null,this.isRecording=!1,this.userCallback,this.resume=function(){this.defaultCallback=e=>{this.isRecording&&this.patches.push(e),this.userCallback&&this.userCallback(e)},this.isObserving=!0}.bind(this),this.pause=function(){this.defaultCallback=()=>{},this.isObserving=!1}.bind(this)}return r.deepClone=function(e){switch(typeof e){case"object":return JSON.parse(JSON.stringify(e));case"undefined":return null;default:return e}},r.escapePathComponent=e,r.prototype.generateProxyAtPath=function(r,o,i){if(!o)return o;const n={set:(r,o,i,n)=>(function(r,o,i,n){const s=t(r,o)+"/"+e(i);if(r.proxifiedObjectsMap.has(n)){const e=r.proxifiedObjectsMap.get(n);r.parenthoodMap.set(e.originalObject,{parent:o,path:i})}const a=r.proxifiedObjectsMap.get(n);a&&!r.isProxifyingTreeNow&&(a.inherited=!0),n&&"object"==typeof n&&!r.proxifiedObjectsMap.has(n)&&(r.parenthoodMap.set(n,{parent:o,path:i}),n=r._proxifyObjectTreeRecursively(o,n,i));const c={op:"remove",path:s};if(void 0===n){if(!Array.isArray(o)&&!o.hasOwnProperty(i))return Reflect.set(o,i,n);{Array.isArray(o)&&(c.op="replace",c.value=null);const e=r.proxifiedObjectsMap.get(o[i]);e&&(r.parenthoodMap.delete(o[i]),r.disableTrapsForProxy(e),r.proxifiedObjectsMap.delete(e))}}else{if(Array.isArray(o)&&!Number.isInteger(+i.toString()))return"length"!=i&&console.warn("JSONPatcherProxy noticed a non-integer prop was set for an array. This will not emit a patch"),Reflect.set(o,i,n);c.op="add",o.hasOwnProperty(i)&&(void 0!==o[i]||Array.isArray(o))&&(c.op="replace"),c.value=n}const p=Reflect.set(o,i,n);return r.defaultCallback(c),p})(this,r,o,i),deleteProperty:(r,o)=>(function(r,o,i){if(void 0!==o[i]){const n=t(r,o)+"/"+e(i),s=r.proxifiedObjectsMap.get(o[i]);s&&(s.inherited?s.inherited=!1:(r.parenthoodMap.delete(s.originalObject),r.disableTrapsForProxy(s),r.proxifiedObjectsMap.delete(o[i])));const a=Reflect.deleteProperty(o,i);return r.defaultCallback({op:"remove",path:n}),a}})(this,r,o)},s=Proxy.revocable(o,n);return s.trapsInstance=n,s.originalObject=o,this.parenthoodMap.set(o,{parent:r,path:i}),this.proxifiedObjectsMap.set(s.proxy,s),s.proxy},r.prototype._proxifyObjectTreeRecursively=function(t,r,o){for(let t in r)r.hasOwnProperty(t)&&r[t]instanceof Object&&(r[t]=this._proxifyObjectTreeRecursively(r,r[t],e(t)));return this.generateProxyAtPath(t,r,o)},r.prototype.proxifyObjectTree=function(e){this.pause(),this.isProxifyingTreeNow=!0;const t=this._proxifyObjectTreeRecursively(void 0,e,"");return this.isProxifyingTreeNow=!1,this.resume(),t},r.prototype.disableTrapsForProxy=function(e){if(this.showDetachedWarning){const t="You're accessing an object that is detached from the observedObject tree, see https://github.com/Palindrom/JSONPatcherProxy#detached-objects";e.trapsInstance.set=(e,r,o)=>(console.warn(t),Reflect.set(e,r,o)),e.trapsInstance.set=(e,r,o)=>(console.warn(t),Reflect.set(e,r,o)),e.trapsInstance.deleteProperty=(e,t)=>Reflect.deleteProperty(e,t)}else delete e.trapsInstance.set,delete e.trapsInstance.get,delete e.trapsInstance.deleteProperty},r.prototype.observe=function(e,t){if(!e&&!t)throw new Error("You need to either record changes or pass a callback");return this.isRecording=e,this.userCallback=t,e&&(this.patches=[]),this.cachedProxy=this.proxifyObjectTree(this.originalObject),this.cachedProxy},r.prototype.generate=function(){if(!this.isRecording)throw new Error("You should set record to true to get patches later");return this.patches.splice(0,this.patches.length)},r.prototype.revoke=function(){this.proxifiedObjectsMap.forEach(e=>{e.revoke()})},r.prototype.disableTraps=function(){this.proxifiedObjectsMap.forEach(this.disableTrapsForProxy,this)},r}();e.exports=o,e.exports.default=o}]).default; \ No newline at end of file + * + * Vocabulary used in this file: + * * root - root object that is deeply observed by JSONPatcherProxy + * * tree - any subtree within the root or the root + */ +const o=function(){function e(e){return-1==e.indexOf("/")&&-1==e.indexOf("~")?e:e.replace(/~/g,"~0").replace(/\//g,"~1")}function t(e,t){const r=[];let o=e._parenthoodMap.get(t);for(;o&&o.key;)r.unshift(o.key),o=e._parenthoodMap.get(o.parent);if(r.length){return"/"+r.join("/")}return""}function r(e,t,r){return r?function(e,t){void 0===e&&(e=null);void 0===t&&(t=null);return e!==t}(e,t):function(e,t){return e!==t}(e,t)}function o(e,t){this._isProxifyingTreeNow=!1,this._isObserving=!1,this._treeMetadataMap=new Map,this._parenthoodMap=new Map,"boolean"!=typeof t&&(t=!0),this._showDetachedWarning=t,this._originalRoot=e,this._cachedProxy=null,this._isRecording=!1,this._userCallback,this._defaultCallback,this._patches}return o.deepClone=function(e){switch(typeof e){case"object":return JSON.parse(JSON.stringify(e));case"undefined":return null;default:return e}},o.escapePathComponent=e,o.prototype._generateProxyAtKey=function(o,n,a){if(!n)return n;const i={set:(...o)=>(function o(n,a,i,s){const c=t(n,a)+"/"+e(i),l=n._treeMetadataMap.get(s);n._treeMetadataMap.has(s)&&n._parenthoodMap.set(l.originalObject,{parent:a,key:i}),l&&!n._isProxifyingTreeNow&&(l.inherited=!0);let p=!1;const u=Array.isArray(a),d=u&&!Number.isInteger(+i.toString());s&&"object"==typeof s&&!n._treeMetadataMap.has(s)&&(d?(console.warn(`JSONPatcherProxy noticed a non-integer property ('${i}') was set for an array. This interception will not emit a patch. The value is an object, but it was not proxified, because it would not be addressable in JSON-Pointer`),p=!0):(n._parenthoodMap.set(s,{parent:a,key:i}),s=n._proxifyTreeRecursively(a,s,i)));const h=a[i],f=a.hasOwnProperty(i);if(u&&!d){const e=parseInt(i,10);e>a.length&&o(n,a,e-1+"",void 0)}const y=Reflect.set(a,i,s),_={op:"remove",path:c};if(void 0===s){if(!u&&!f)return y;{if(f&&!r(h,s,u))return y;u&&(_.value=null,_.op=f?"replace":"add");const e=n._treeMetadataMap.get(h);e&&(n._parenthoodMap.delete(h),n._disableTrapsForTreeMetadata(e),n._treeMetadataMap.delete(e))}}else{if(d)return"length"==i||p||console.warn(`JSONPatcherProxy noticed a non-integer property ('${i}') was set for an array. This interception will not emit a patch`),y;if(_.op="add",f&&(void 0!==h||u)){if(!r(h,s,u))return y;_.op="replace"}_.value=s}return n._defaultCallback(_),y})(this,...o),deleteProperty:(...r)=>(function(r,o,n){const a=o[n],i=Reflect.deleteProperty(o,n);if(void 0!==a){const i=t(r,o)+"/"+e(n),s=r._treeMetadataMap.get(a);s&&(s.inherited?s.inherited=!1:(r._parenthoodMap.delete(s.originalObject),r._disableTrapsForTreeMetadata(s),r._treeMetadataMap.delete(a))),r._defaultCallback({op:"remove",path:i})}return i})(this,...r)},s=Proxy.revocable(n,i);return s.handler=i,s.originalObject=n,this._parenthoodMap.set(n,{parent:o,key:a}),this._treeMetadataMap.set(s.proxy,s),s.proxy},o.prototype._proxifyTreeRecursively=function(t,r,o){for(let t in r)r.hasOwnProperty(t)&&r[t]instanceof Object&&(r[t]=this._proxifyTreeRecursively(r,r[t],e(t)));return this._generateProxyAtKey(t,r,o)},o.prototype._proxifyRoot=function(e){this.pause(),this._isProxifyingTreeNow=!0;const t=this._proxifyTreeRecursively(void 0,e,"");return this._isProxifyingTreeNow=!1,this.resume(),t},o.prototype._disableTrapsForTreeMetadata=function(e){if(this._showDetachedWarning){const t="You're accessing an object that is detached from the observedObject tree, see https://github.com/Palindrom/JSONPatcherProxy#detached-objects";e.handler.set=(e,r,o)=>(console.warn(t),Reflect.set(e,r,o)),e.handler.set=(e,r,o)=>(console.warn(t),Reflect.set(e,r,o)),e.handler.deleteProperty=(e,t)=>Reflect.deleteProperty(e,t)}else delete e.handler.set,delete e.handler.get,delete e.handler.deleteProperty},o.prototype.observe=function(e,t){if(!e&&!t)throw new Error("You need to either record changes or pass a callback");return this._isRecording=e,this._userCallback=t,e&&(this._patches=[]),this._cachedProxy=this._proxifyRoot(this._originalRoot),this._cachedProxy},o.prototype.generate=function(){if(!this._isRecording)throw new Error("You should set record to true to get patches later");return this._patches.splice(0,this._patches.length)},o.prototype.revoke=function(){this._treeMetadataMap.forEach(e=>{e.revoke()})},o.prototype.disableTraps=function(){this._treeMetadataMap.forEach(this._disableTrapsForTreeMetadata,this)},o.prototype.resume=function(){this._defaultCallback=e=>{this._isRecording&&this._patches.push(e),this._userCallback&&this._userCallback(e)},this._isObserving=!0},o.prototype.pause=function(){this._defaultCallback=()=>{},this._isObserving=!1},o}()}]).default; \ No newline at end of file diff --git a/jasmine-run.js b/jasmine-run.js new file mode 100644 index 0000000..6357a27 --- /dev/null +++ b/jasmine-run.js @@ -0,0 +1,26 @@ +import glob from 'glob'; +import Jasmine from 'jasmine'; + +const jasmine = new Jasmine(); + +const pattern = process.argv[2] || 'test/spec/*Spec.js'; + +jasmine.loadConfig({ + random: false, + stopSpecOnExpectationFailure: false +}); +// Load your specs +glob(pattern, function (er, files) { + Promise.all( + files + // Use relative paths + .map(f => f.replace(/^([^\/])/, './$1')) + .map(f => import(f) + .catch(e => { + console.error('** Error loading ' + f + ': '); + console.error(e); + process.exit(1); + })) + ) + .then(() => jasmine.execute()); +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f3ce1b0..c8e5ebf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1067,11 +1067,6 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" - }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -1484,12 +1479,9 @@ "dev": true }, "fast-json-patch": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-2.1.0.tgz", - "integrity": "sha512-PipOsAKamRw7+CXtKiieehyjUeDVPJ5J7b2kdJYerEf6TSUQoD2ijpVyZ88KQm5YXziff4h762bz3+vzf56khg==", - "requires": { - "deep-equal": "^1.0.1" - } + "version": "3.0.0-0", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.0.0-0.tgz", + "integrity": "sha512-ZVybQyRPbt+FnAI8R3ob6tnrSQHclW4ii2XbeFBaW9TrM0Cb681r3oW4yUCCGiBEok1VQpAmveSiNnC93y4EHw==" }, "fast-json-stable-stringify": { "version": "2.0.0", diff --git a/package.json b/package.json index 3496f6b..51e912e 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,14 @@ "description": "Lean and mean Javascript implementation of the JSON-Patch standard (RFC 6902). Update JSON documents using delta patches.", "main": "src/jsonpatcherproxy.js", "typings": "index.d.ts", + "type": "module", "scripts": { "build": "webpack", "build-watch": "webpack --watch", - "bench": "node test/spec/proxyBenchmark.js", - "bench-compare": "node test/spec/proxyBenchmark.js --compare", - "test": "jasmine DUPLEX=proxy JASMINE_CONFIG_PATH=test/jasmine.json", - "test-debug": "node --inspect-brk node_modules/jasmine/bin/jasmine.js DUPLEX=proxy JASMINE_CONFIG_PATH=test/jasmine.json", + "bench": "node --experimental-modules test/spec/proxyBenchmark.js", + "bench-compare": "node --experimental-modules test/spec/proxyBenchmark.js --compare", + "test": "node --experimental-modules jasmine-run.js", + "test-debug": "node --inspect-brk --experimental-modules jasmine-run.js", "version": "webpack && git add -A" }, "keywords": [ @@ -32,7 +33,7 @@ "webpack-cli": "^3.2.3" }, "dependencies": { - "fast-json-patch": "^2.1.0" + "fast-json-patch": "^3.0.0-0" }, "//comments": "fast-json-patch is a dependency, though it is only used for type definitions and performance benchmark" } diff --git a/src/jsonpatcherproxy.js b/src/jsonpatcherproxy.js index 45375bb..609366e 100644 --- a/src/jsonpatcherproxy.js +++ b/src/jsonpatcherproxy.js @@ -437,7 +437,4 @@ const JSONPatcherProxy = (function() { return JSONPatcherProxy; })(); -if (typeof module !== 'undefined') { - module.exports = JSONPatcherProxy; - module.exports.default = JSONPatcherProxy; -} +export { JSONPatcherProxy }; diff --git a/test/helpers/fake-module.js b/test/helpers/fake-module.js new file mode 100644 index 0000000..3bbc276 --- /dev/null +++ b/test/helpers/fake-module.js @@ -0,0 +1,2 @@ +/** Fake module, to allow importing (without using) in browser */ +export function createRequire(){}; \ No newline at end of file diff --git a/test/index.html b/test/index.html index fb5c851..4ab4ee1 100644 --- a/test/index.html +++ b/test/index.html @@ -22,12 +22,17 @@ - - - + + - - + + diff --git a/test/jasmine.json b/test/jasmine.json deleted file mode 100644 index 89452f1..0000000 --- a/test/jasmine.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "spec_dir": "test", - "spec_files": [ - "**/*[sS]pec.js" - ], - "helpers": [ - "helpers/**/*.js" - ], - "stopSpecOnExpectationFailure": false, - "random": false -} diff --git a/test/spec/proxyBenchmark.js b/test/spec/proxyBenchmark.js index 7573f09..61edd46 100644 --- a/test/spec/proxyBenchmark.js +++ b/test/spec/proxyBenchmark.js @@ -6,6 +6,12 @@ $ npm run bench */ + import { JSONPatcherProxy } from '../../src/jsonpatcherproxy.js'; + import * as jsonpatch from '../../node_modules/fast-json-patch/index.mjs'; + +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); + let includeComparisons = true; const isNode = (typeof window === 'undefined'); @@ -21,14 +27,6 @@ if (isNode) { } } -if (typeof jsonpatch === 'undefined') { - global.jsonpatch = require('fast-json-patch'); -} - -if (typeof JSONPatcherProxy === 'undefined') { - global.JSONPatcherProxy = require('../../src/jsonpatcherproxy.js'); -} - if (typeof Benchmark === 'undefined') { global.Benchmark = require('benchmark'); global.benchmarkResultsToConsole = require('./../helpers/benchmarkReporter.js').benchmarkResultsToConsole; diff --git a/test/spec/proxySpec.js b/test/spec/proxySpec.js index b70edba..ae032ee 100644 --- a/test/spec/proxySpec.js +++ b/test/spec/proxySpec.js @@ -1,9 +1,5 @@ -if (typeof jsonpatch === 'undefined') { - global.jsonpatch = require('fast-json-patch'); -} -if (typeof JSONPatcherProxy === 'undefined') { - global.JSONPatcherProxy = require('../../src/jsonpatcherproxy'); -} +import { JSONPatcherProxy } from '../../src/jsonpatcherproxy.js'; +import * as jsonpatch from '../../node_modules/fast-json-patch/index.mjs'; function getPatchesUsingGenerate(objFactory, objChanger) { const obj = objFactory(); @@ -697,7 +693,7 @@ describe('proxy', function() { foo: undefined }; }; - + const objChanger = function(obj) { delete obj.foo; };