Skip to content

Commit

Permalink
Merge pull request #27 from cb1kenobi/v5
Browse files Browse the repository at this point in the history
v5.0.0
  • Loading branch information
cb1kenobi committed Nov 18, 2020
2 parents 5b8b6e6 + 07107f0 commit 5d16ff1
Show file tree
Hide file tree
Showing 5 changed files with 1,078 additions and 1,008 deletions.
3 changes: 2 additions & 1 deletion gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

Expand Down
32 changes: 16 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "gawk",
"version": "4.7.1",
"version": "5.0.0",
"description": "Observable JavaScript object model",
"main": "./dist/index.js",
"author": "Chris Barber <[email protected]> (https://github.com/cb1kenobi)",
Expand All @@ -23,45 +23,45 @@
"test": "gulp test"
},
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-deep-equal": "^3.1.3",
"source-map-support": "^0.5.19"
},
"devDependencies": {
"@babel/core": "^7.9.6",
"@babel/plugin-transform-modules-commonjs": "^7.9.6",
"@babel/register": "^7.9.0",
"@babel/core": "^7.12.3",
"@babel/plugin-transform-modules-commonjs": "^7.12.1",
"@babel/register": "^7.12.1",
"ansi-colors": "^4.1.1",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"babel-loader": "^8.2.1",
"babel-plugin-istanbul": "^6.0.0",
"chai": "^4.2.0",
"coveralls": "^3.1.0",
"esdoc": "^1.1.0",
"esdoc-ecmascript-proposal-plugin": "^1.0.0",
"esdoc-standard-plugin": "^1.0.0",
"eslint": "^6.8.0",
"eslint-plugin-chai-expect": "^2.1.0",
"eslint-plugin-mocha": "^6.3.0",
"eslint": "^7.13.0",
"eslint-plugin-chai-expect": "^2.2.0",
"eslint-plugin-mocha": "^8.0.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-security": "^1.4.0",
"fancy-log": "^1.3.3",
"fs-extra": "^9.0.0",
"fs-extra": "^9.0.1",
"gulp": "^4.0.2",
"gulp-babel": "^8.0.0",
"gulp-debug": "^4.0.0",
"gulp-eslint": "^6.0.0",
"gulp-load-plugins": "^2.0.3",
"gulp-load-plugins": "^2.0.5",
"gulp-plumber": "^1.2.1",
"gulp-sourcemaps": "^2.6.5",
"mocha": "^7.1.2",
"nyc": "^15.0.1",
"sinon": "^9.0.2",
"gulp-sourcemaps": "^3.0.0",
"mocha": "^8.2.1",
"nyc": "^15.1.0",
"sinon": "^9.2.1",
"sinon-chai": "^3.5.0"
},
"homepage": "https://github.com/cb1kenobi/gawk",
"bugs": "https://github.com/cb1kenobi/gawk/issues",
"repository": "https://github.com/cb1kenobi/gawk",
"engines": {
"node": ">=6.0.0"
"node": ">=10.13.0"
}
}
97 changes: 61 additions & 36 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,53 +55,53 @@ export default function gawk(value, parent) {
gawked = value;
} else {
// gawk it!
gawked = new Proxy(value, {
set: (target, prop, newValue) => {
const revocable = Proxy.revocable(value, {
deleteProperty(target, prop) {
if (prop === '__gawk__') {
throw new Error('Cannot override property \'__gawk__\'');
throw new Error('Cannot delete property \'__gawk__\'');
}

// console.log('setting', prop, newValue);

let changed = true;
const desc = Object.getOwnPropertyDescriptor(target, prop);
// console.log('deleting', prop, target[prop]);

if (desc) {
changed = target[prop] !== newValue;
const parents = isGawked(target[prop]) && target[prop].__gawk__.parents;
if (parents) {
parents.delete(gawked);
if (!parents.size) {
target[prop].__gawk__.parents = null;
}
}
if (!Object.prototype.hasOwnProperty.call(target, prop)) {
return true;
}

// 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];
const parents = isGawked(target[prop]) && target[prop].__gawk__.parents;
if (parents) {
parents.delete(gawked);
if (!parents.size) {
target[prop].__gawk__.parents = null;
}
}

target[prop] = gawk(newValue, gawked);

if (changed) {
const result = delete target[prop];
if (result) {
notify(gawked);
}

return true;
return result;
},

deleteProperty: (target, prop) => {
set(target, prop, newValue) {
if (prop === '__gawk__') {
throw new Error('Cannot delete property \'__gawk__\'');
throw new Error('Cannot override property \'__gawk__\'');
}

// console.log('deleting', prop, target[prop]);
// console.log('setting', prop, newValue);

let result = true;
let changed = true;
const desc = Object.getOwnPropertyDescriptor(target, prop);

if (Object.prototype.hasOwnProperty.call(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) {
parents.delete(gawked);
Expand All @@ -110,16 +110,31 @@ export default function gawk(value, parent) {
}
}

result = delete target[prop];
if (result) {
notify(gawked);
// if the destination property has a setter, then we can't assume we need to
// fire a delete
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);
}

return result;
if (changed) {
notify(gawked);
}
return true;
}
});

gawked = revocable.proxy;

Object.defineProperty(gawked, '__gawk__', {
value: {
/**
Expand Down Expand Up @@ -179,14 +194,24 @@ export default function gawk(value, parent) {
notify(gawked, instance);
}
}
}
},

/**
* Makes this gawked proxy unusable.
*/
revoke: revocable.revoke
}
});

// gawk any object properties
for (const key of Reflect.ownKeys(gawked)) {
if (key !== '__gawk__' && gawked[key] && typeof gawked[key] === 'object') {
gawked[key] = gawk(gawked[key], gawked);
// desc should always be an object since we know the key exists
const desc = Object.getOwnPropertyDescriptor(gawked, key);
if (desc && desc.configurable !== false) {
desc.value = gawk(gawked[key], gawked);
Object.defineProperty(gawked, key, desc);
}
}
}

Expand Down
120 changes: 119 additions & 1 deletion test/test-object.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/* eslint-disable no-prototype-builtins */

import gawk, { Gawk, isGawked } from '../dist/index';
import gawk, { isGawked } from '../dist/index';

import { EventEmitter } from 'events';
import { expect } from 'chai';

const version = require('./../package.json').version;

Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -487,3 +566,42 @@ describe('gawk.mergeDeep()', () => {
expect(gobj.foo.biz.__gawk__.parents.has(gobj.foo)).to.be.true;
});
});

describe('revoke', () => {
it('should revoke the proxy', () => {
const obj = {
foo: {
bar: 'baz'
}
};

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'
}
});

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'
}
});
});
});
Loading

0 comments on commit 5d16ff1

Please sign in to comment.