diff --git a/gulpfile.js b/gulpfile.js index 1513a4e..0489e97 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -130,7 +130,8 @@ function runTests(cover) { args.push(path.join(mocha, 'bin', 'mocha')); // add --inspect - if (process.argv.indexOf('--inspect') !== -1 || process.argv.indexOf('--inspect-brk') !== -1) { + const { argv } = process; + if (argv.includes('--debug') || argv.includes('--inspect') || argv.includes('--inspect-brk')) { args.push('--inspect-brk'); } diff --git a/src/index.js b/src/index.js index e2487f4..ea691e4 100644 --- a/src/index.js +++ b/src/index.js @@ -93,6 +93,14 @@ export default function gawk(value, parent) { const desc = Object.getOwnPropertyDescriptor(target, prop); if (desc) { + if (desc.writable === false) { + // if both writable and configurable are false, then returning anything + // will cause an error because without proxies, setting a non-writable + // property has no effect, but attempting to set a proxied non-writable + // property is a TypeError + return true; + } + changed = target[prop] !== newValue; const parents = isGawked(target[prop]) && target[prop].__gawk__.parents; if (parents) { @@ -104,13 +112,20 @@ export default function gawk(value, parent) { // if the destination property has a setter, then we can't assume we need to // fire a delete - if (typeof desc.set !== 'function' && (!Array.isArray(target) || prop !== 'length')) { - delete target[prop]; + if (typeof desc.set === 'function') { + target[prop] = gawk(newValue, gawked); + + } else { + if (!Array.isArray(target) || prop !== 'length') { + delete target[prop]; + } + desc.value = gawk(newValue, gawked); + Object.defineProperty(target, prop, desc); } + } else { + target[prop] = gawk(newValue, gawked); } - target[prop] = gawk(newValue, gawked); - if (changed) { notify(gawked); } @@ -182,7 +197,7 @@ export default function gawk(value, parent) { }, /** - * Removes the proxy from this object. + * Makes this gawked proxy unusable. */ revoke: revocable.revoke } @@ -191,9 +206,11 @@ export default function gawk(value, parent) { // gawk any object properties for (const key of Reflect.ownKeys(gawked)) { if (key !== '__gawk__' && gawked[key] && typeof gawked[key] === 'object') { + // desc should always be an object since we know the key exists const desc = Object.getOwnPropertyDescriptor(gawked, key); - if (!desc || desc.configurable !== false) { - gawked[key] = gawk(gawked[key], gawked); + if (desc && desc.configurable !== false) { + desc.value = gawk(gawked[key], gawked); + Object.defineProperty(gawked, key, desc); } } } diff --git a/test/test-object.js b/test/test-object.js index 7148aba..c091db4 100644 --- a/test/test-object.js +++ b/test/test-object.js @@ -3,6 +3,7 @@ import gawk, { isGawked } from '../dist/index'; import { EventEmitter } from 'events'; +import { expect } from 'chai'; const version = require('./../package.json').version; @@ -256,6 +257,84 @@ describe('set property', () => { expect(Object.getOwnPropertySymbols(gobj)).to.deep.equal([ id ]); expect(gobj[id]).to.equal('bar'); }); + + it('should set an object with a configurable, enumerable, writable property', () => { + const obj = { + foo: 'bar' + }; + + Object.defineProperty(obj, 'baz', { + configurable: true, + enumerable: true, + value: { + pow: 'wiz' + }, + writable: true + }); + + const gobj = gawk(obj); + expect(JSON.parse(JSON.stringify(gobj))).to.deep.equal({ foo: 'bar', baz: { pow: 'wiz' } }); + expect(gobj.foo).to.equal('bar'); + expect(gobj.baz).to.deep.equal({ pow: 'wiz' }); + expect(gobj.baz.pow).to.equal('wiz'); + + gobj.baz = 'bam'; + expect(JSON.parse(JSON.stringify(gobj))).to.deep.equal({ foo: 'bar', baz: 'bam' }); + expect(gobj.foo).to.equal('bar'); + expect(gobj.baz).to.equal('bam'); + }); + + it('should set an object with a configurable, enumerable, read-only property', () => { + const obj = { + foo: 'bar' + }; + + Object.defineProperty(obj, 'baz', { + configurable: true, + enumerable: true, + value: { + pow: 'wiz' + }, + writable: false + }); + + const gobj = gawk(obj); + expect(JSON.parse(JSON.stringify(gobj))).to.deep.equal({ foo: 'bar', baz: { pow: 'wiz' } }); + expect(gobj.foo).to.equal('bar'); + expect(gobj.baz).to.deep.equal({ pow: 'wiz' }); + expect(gobj.baz.pow).to.equal('wiz'); + + gobj.baz = 'bam'; + expect(JSON.parse(JSON.stringify(gobj))).to.deep.equal({ foo: 'bar', baz: { pow: 'wiz' } }); + expect(gobj.foo).to.equal('bar'); + expect(gobj.baz).to.deep.equal({ pow: 'wiz' }); + expect(gobj.baz.pow).to.equal('wiz'); + }); + + it('should set an object with a non-configurable, non-enumerable, read-only property', () => { + const obj = { + foo: 'bar' + }; + + Object.defineProperty(obj, 'baz', { + configurable: false, + enumerable: false, + value: { + pow: 'wiz' + }, + writable: false + }); + + const gobj = gawk(obj); + expect(JSON.parse(JSON.stringify(gobj))).to.deep.equal({ foo: 'bar' }); + expect(gobj.foo).to.equal('bar'); + expect(gobj.baz).to.deep.equal({ pow: 'wiz' }); + expect(gobj.baz.pow).to.equal('wiz'); + + expect(() => { + gobj.baz = 'bam'; + }).to.throw(TypeError, /'set' on proxy/); + }); }); describe('delete property', () => { @@ -486,22 +565,43 @@ describe('gawk.mergeDeep()', () => { expect(isGawked(gobj.foo.biz)).to.be.true; expect(gobj.foo.biz.__gawk__.parents.has(gobj.foo)).to.be.true; }); +}); - it('should gawk an object with a non-configurable, non-enumerable property', () => { +describe('revoke', () => { + it('should revoke the proxy', () => { const obj = { - foo: 'bar' + foo: { + bar: 'baz' + } }; - Object.defineProperty(obj, 'baz', { - value: { - pow: 'wiz' + const gobj = gawk(obj); + + expect(gobj).to.not.equal(obj); + expect(gobj).to.deep.equal(obj); + + gobj.foo.bar = 'wiz'; + expect(gobj).to.deep.equal({ + foo: { + bar: 'wiz' + } + }); + expect(obj).to.deep.equal({ + foo: { + bar: 'wiz' } }); - const gobj = gawk(obj); - expect(JSON.parse(JSON.stringify(gobj))).to.deep.equal({ foo: 'bar' }); - expect(gobj.foo).to.equal('bar'); - expect(gobj.baz).to.deep.equal({ pow: 'wiz' }); - expect(gobj.baz.pow).to.equal('wiz'); + gobj.__gawk__.revoke(); + + expect(() => { + gobj.foo.bar = 'bam'; + }).to.throw(TypeError, 'Cannot perform \'get\' on a proxy that has been revoked'); + + expect(obj).to.deep.equal({ + foo: { + bar: 'wiz' + } + }); }); });