All files / src proxify.js

95.45% Statements 42/44
85.71% Branches 24/28
100% Functions 11/11
95.24% Lines 40/42

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136                                                                                                      116x                   1x 556x 116x   440x 440x 440x           440x   1x 116x       116x 116x 116x   116x   116x 5x 5x 1x   4x   4x     116x 109x 5x 1x 8x       116x     11986x   116x   33396x     33396x 1x   33396x     110x 4x   106x 4x   106x 97x       116x     1x  
/*
Class wrapper throwing errors when getting/setting undeclared properties.
 
```javascript
const obj = proxify({ foo: 42 });
 
console.log(obj.foo);
console.log(obj.bar); // throw
```
 
Some configuration:
 
```javascript
proxify({ foo: 42 }, {
  name: 'object',
  seal: true,
  sealGet: false,
  deprecated: [],
  exposeApi: false,
  apiNamespace: '__proxy__',
});
```
 
- `name` : the name of the proxified object for warnings
- `seal`: does `proxify` throw error on set undeclared properties?
- `sealGet`: does `proxify` throw error on get undeclared properties? this options is separated from `seal` because getting undefined properties can be usefull for type checking for example
- `deprecated`: array of property names which produce a deprecate warning on get/set
- `warnDeprecationOnce`: only warn once per deprecated property
- `exposeApi`: expose an api in the object to manipulate properties (described below)
- `apiNamespace`: in which namespace api is exposed
 
##### proxify API
 
```javascript
const obj = proxify({ foo: 42 }, { exposeApi: true });
 
console.log(obj.foo); // OK
console.log(obj.bar); // throw error
 
obj.__proxy__.registerProp('bar');
 
console.log(obj.bar); // OK
 
const hasProp = obj.__proxy__.hasProp('baz'); // check WITHOUT warning
 
obj.__proxy__.unregisterProp('bar');
 
console.log(obj.bar); // throw error
```
*/
 
const getOptions = options => (Object.assign({}, {
  name: 'object',
  seal: true,
  sealGet: false,
  deprecated: [],
  warnDeprecationOnce: true,
  exposeApi: false,
  apiNamespace: '__proxy__'
}, options));
 
const getPropertyNames = obj => {
  if (!obj) {
    return [];
  }
  const methods = Object.getOwnPropertyNames(obj.prototype || obj);
  const proto = Object.getPrototypeOf(obj);
  return deleteDuplicates([
    ...methods,
    ...getPropertyNames(proto)
  ]);
};
 
const deleteDuplicates = arr => [...new Set(arr)];
 
const proxify = (obj, opts = {}) => {
  Iif (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) {
    throw Error('proxify only applies on non-null object');
  }
 
  const options = getOptions(opts);
  const properties = new Set(['inspect']);
  const deprecated = new Set(options.deprecated);
 
  const warnedDeprecation = new Set();
 
  const warnDeprecation = name => {
    const hash = `${options.name}.${name}`;
    if (options.warnDeprecationOnce && warnedDeprecation.has(hash)) {
      return;
    }
    warnedDeprecation.add(hash);
    // eslint-disable-next-line no-console
    console.warn(`Warning: ${hash} is deprecated`);
  };
 
  if (options.exposeApi) {
    obj[options.apiNamespace] = {
      registerProp: name => properties.add(name),
      unregisterProp: name => properties.delete(name),
      hasProp: name => properties.has(name)
    };
  }
 
  [
    ...Object.getOwnPropertyNames(obj),
    ...getPropertyNames(obj)
  ].forEach(prop => properties.add(prop));
 
  const handler = {
    get: (target, name) => {
      Iif (options.sealGet && !properties.has(name)) {
        throw new Error(`${options.name}.${name} is not defined`);
      }
      if (deprecated.has(name)) {
        warnDeprecation(name);
      }
      return target[name];
    },
    set: (target, name, value) => {
      if (options.seal && !properties.has(name)) {
        throw new Error(`Cannot set a value to the undefined '${name}' property in '${options.name}'`);
      }
      if (deprecated.has(name)) {
        warnDeprecation(name);
      }
      target[name] = value;
      return true;
    }
  };
 
  return new Proxy(obj, handler);
};
 
module.exports = proxify;