cfprotected 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/GEMINI.md ADDED
@@ -0,0 +1,25 @@
1
+ # Project: JSAppLib
2
+
3
+ ## General Instructions
4
+
5
+ - Re-read this file when it is updated.
6
+ - Follow the existing coding style when generating new Javascript.
7
+ - Add JSDoc comments for all new functions and classes. Add JSDoc comments to all public functions and classes not currently possessing them.
8
+ - Make use of the cfprotected node module to add support for protected members when creating classes.
9
+ - Always provide a static private member named "spvt" for the class-private member container.
10
+ - Always provide a private member named "pvt" for the instance-private member container.
11
+ - TypeScript is not allowed in this project.
12
+ - Tests use the jest node module.
13
+ - Don't try to guess about what I want you to do. If there is any uncertainty, ask me.
14
+ - Don't run anything on the command line without my review and approval.
15
+ - Don't assume. Question the user.
16
+
17
+ ## Coding Style
18
+
19
+ - Functions can only have a single exit point. This doesn't include exceptions.
20
+ - Each new HTML Custom Element classes must be contained in its own file.
21
+ - The name of a class's file must be the class name prefixed by 'js' and have an extension of '.mjs'.
22
+ - Indentation is always 4 spaces.
23
+ - Never use public fields. If a public field is desired, use a public accessor to a private field.
24
+ - When a class uses private members, make sure to use `saveSelf(this, '$');` in the constructor, and if needed, in the static initializer block of the class.
25
+ - Except in the class constructor and in the static initializer block, private fields must be accessed via `this.$` to protect against proxy access.
package/README.md CHANGED
@@ -132,6 +132,27 @@ const Example = final(class {
132
132
  ### Notes:
133
133
  In order to ensure the functionality of final would not interfere with the ability to access static members, final is implemented using a Proxy. It is therefore very important to use `saveSelf(...)` and the resulting property (or a similar approach) to ensure that access to static private properties is not interrupted.
134
134
 
135
+ ## **define(klass, defs)**
136
+ Adds specified definitions to the class prototype. All supplied definitions will default to {enumerable: true, configurable: true, writable: true} unless otherwise specified. The {writable} attribute will not be defaulted if {value} is not specified. This is for providing public class members that are bound to the prototype instead of the instance objects. Use this function in the `static {}` block of the class.
137
+ ```js
138
+ class Example {
139
+ ...
140
+ static {
141
+ ...
142
+ define(this, {
143
+ ex1: {
144
+ get: () => {}
145
+ },
146
+ ex2: {
147
+ value: 42
148
+ }
149
+ });
150
+ ...
151
+ }
152
+ ...
153
+ }
154
+ ```
155
+
135
156
  ## Other features
136
157
  There will be occasions when a shared function that shadows an ancestor function needs to call the ancestor's function. Unfortunately, `super` cannot give you access to these. There is a similar problem when accessing accessors and data properties. To satisfy this need, the class-specific accessor option is given an additional property: `$uper`. Using this property, it is possible to reach the ancestor version of any shared member.
137
158
 
package/index.js ADDED
@@ -0,0 +1,361 @@
1
+ "use strict"
2
+
3
+ const memos = new WeakMap();
4
+ const ACCESSOR = Symbol();
5
+
6
+ /**
7
+ * General definition of a class constructor function.
8
+ * @typedef Constructor
9
+ * @type new (...args: any[]) => object
10
+ */
11
+
12
+
13
+ /**
14
+ * Returns all public keys of the specified object.
15
+ * @param {object} o
16
+ * @returns {(string|symbol)[]}
17
+ */
18
+ function getAllOwnKeys(o) {
19
+ /** @type {(string|symbol)[]} */
20
+ let retval = Object.getOwnPropertyNames(o);
21
+ return retval.concat(Object.getOwnPropertySymbols(o));
22
+ }
23
+
24
+ /**
25
+ * @typedef Descriptor
26
+ * @property {*} [value]
27
+ * @property {Function} [get]
28
+ * @property {Function} [set]
29
+ *
30
+ * Calls the given function against each descriptor property that itself is
31
+ * also a function.
32
+ * @param {Descriptor} desc
33
+ * @param {Function} fn
34
+ */
35
+ function useDescriptor(desc, fn) {
36
+ if ("value" in desc) {
37
+ if (typeof(desc.value) == "function") {
38
+ fn("value");
39
+ }
40
+ }
41
+ else {
42
+ if (typeof(desc.get) == "function") {
43
+ fn("get");
44
+ }
45
+ if (typeof(desc.set) == "function") {
46
+ fn("set");
47
+ }
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Binds all functions of the descriptor to the given context object.
53
+ * @param {Descriptor} desc
54
+ * @param {object} context
55
+ */
56
+ function bindDescriptor(desc, context) {
57
+ useDescriptor(desc, (key) => {
58
+ desc[key] = desc[key].bind(context);
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Used to both store inherited property information as well as retrieve it.
64
+ * @overload { (inst, klass, members) => object }
65
+ * @overload { (inst, members) => object }
66
+ * @param {object} inst The instance object that will own the shared members.
67
+ * @param {Constructor|object} klass The constructor function of the class sharing
68
+ * members. Optional. If omitted, defaults to inst.
69
+ * @param {object=} members The object containing the properties being shared.
70
+ * @returns {object} The fully constructed inheritance object.
71
+ */
72
+ function share(inst, klass, members) {
73
+ let retval = {};
74
+
75
+ if ((typeof(inst) == "function")
76
+ && klass && (typeof(klass) == "object")
77
+ && (members === void 0)) {
78
+ members = klass;
79
+ klass = inst;
80
+ }
81
+
82
+ if (!inst || !["function", "object"].includes(typeof(inst))) {
83
+ throw new TypeError(`Expected inst to be a function or an object.`);
84
+ }
85
+ if (!klass || (typeof(klass) != "function")) {
86
+ throw new TypeError(`Expected klass to be a function.`);
87
+ }
88
+ if (!members || (typeof(members) != "object")) {
89
+ throw new TypeError(`Expected members to be an object.`);
90
+ }
91
+
92
+
93
+ /*
94
+ * Each class' memo entry has the following structure:
95
+ *
96
+ * inst: {
97
+ * data: <Object> - the actual protected data object
98
+ * inheritance: <Object> - the object of accessor properties to share
99
+ * with descendant classes.
100
+ * }
101
+ */
102
+
103
+ //Find the nearest known registered ancestor class
104
+ let ancestor = Object.getPrototypeOf(klass);
105
+ while (ancestor && !memos.has(ancestor)) {
106
+ ancestor = Object.getPrototypeOf(ancestor);
107
+ }
108
+
109
+ //Get the memo from that ancestor
110
+ let ancestorMemo = memos.get(ancestor) || new WeakMap();
111
+
112
+ //Create a memo map for the current class
113
+ if (!memos.has(klass)) {
114
+ memos.set(klass, new WeakMap());
115
+ }
116
+
117
+ //Get the protected data object.
118
+ let ancestorKey = (inst === klass) ? ancestor : inst;
119
+ let memo = ancestorMemo.get(ancestorKey) || {data: {}, $uper: {}, inheritance: null};
120
+ let protData = memo.data;
121
+
122
+ //Get the details of the protected properties.
123
+ let mDesc = Object.getOwnPropertyDescriptors(members);
124
+ let mKeys = getAllOwnKeys(members);
125
+
126
+ //Add the new members to the prototype chain of protData.
127
+ let prototype = Object.getPrototypeOf(protData);
128
+
129
+ let proto = Object.create(prototype,
130
+ Object.fromEntries(mKeys
131
+ .map(k => {
132
+ // @ts-ignore
133
+ let desc = mDesc[k];
134
+ if (desc.value?.hasOwnProperty(ACCESSOR)) {
135
+ Object.assign(desc, desc.value);
136
+ desc.enumerable = true;
137
+ delete desc[ACCESSOR];
138
+ delete desc.value;
139
+ delete desc.writable;
140
+ }
141
+ bindDescriptor(desc, inst);
142
+ return [k, desc];
143
+ })));
144
+ Object.setPrototypeOf(protData, proto);
145
+
146
+ //Build the accessors for this class.
147
+ mKeys.forEach(m => {
148
+ Object.defineProperty(retval, m, {
149
+ get() { return protData[m]; },
150
+ set(v) { protData[m] = v; }
151
+ });
152
+ });
153
+
154
+ //Define the "$uper" accessors
155
+ Object.defineProperty(retval, "$uper", { value: {} });
156
+
157
+ //Build up the "$uper" object
158
+ for (let key of mKeys) {
159
+ if (key in prototype) {
160
+ let obj = prototype;
161
+ while (!obj.hasOwnProperty(key)) {
162
+ obj = Object.getPrototypeOf(obj);
163
+ }
164
+ Object.defineProperty(retval.$uper, key, Object.getOwnPropertyDescriptor(obj, key));
165
+ }
166
+ }
167
+
168
+ //Attach the super inheritance
169
+ Object.setPrototypeOf(retval.$uper, memo.$uper);
170
+
171
+ //Inherit the inheritance
172
+ Object.setPrototypeOf(retval, memo.inheritance);
173
+
174
+ //Save the inheritance & protected data
175
+ memos.get(klass).set(inst, {
176
+ data: protData,
177
+ inheritance: retval,
178
+ $uper: retval.$uper
179
+ });
180
+
181
+ return retval;
182
+ }
183
+
184
+ /**
185
+ * Binds the class instance to itself to allow code to selectively avoid Proxy
186
+ * issues, especially ones involving private fields. Also binds the class
187
+ * constructor to the instance for static referencing as "cla$$".
188
+ * @param {object} self The current class instance as seen from the constructor.
189
+ * @param {string} name The name of the field on which to bind the instance.
190
+ */
191
+ function saveSelf(self, name) {
192
+ Object.defineProperty(self, name, {value: self});
193
+ if (typeof(self) == "function") {
194
+ Object.defineProperty(self.prototype, "cla$$", {value: self});
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Marks the property as a shared, overrideable accessor and defines the
200
+ * getter and setter methods for it.
201
+ * @param {object} desc Object containing get and/or set definitions for the
202
+ * accessor.
203
+ * @returns {object} A tagged object that will be used to create the access
204
+ * bindings for the property.
205
+ */
206
+ function accessor(desc) {
207
+ if ((typeof(desc) == "object") &&
208
+ (("get" in desc) || ("set" in desc))) {
209
+ return {
210
+ [ACCESSOR]: undefined,
211
+ get: desc.get,
212
+ set: desc.set
213
+ }
214
+ }
215
+ }
216
+
217
+ /**
218
+ * A class wrapper that blocks construction of an instance if the class being
219
+ * instantiated is not a descendant of the current class.
220
+ * @param {Constructor|string} klass If a function, the constructor of the current
221
+ * class. If a string, the name of the function being abstracted.
222
+ * @returns {Function} Either an extended class that denies direct construction
223
+ * or a function that immediately throws.
224
+ */
225
+ function abstract(klass) {
226
+ let retval;
227
+ if (typeof(klass) == "function") {
228
+ let name = klass.name ? klass.name : "";
229
+ retval = class extends klass {
230
+ constructor (...args) {
231
+ if (new.target === retval) {
232
+ throw new TypeError(`Class constructor ${name} is abstract and cannot be directly invoked with 'new'`);
233
+ }
234
+ super(...args);
235
+ }
236
+ };
237
+
238
+ if (memos.has(klass)) {
239
+ let memo = memos.get(klass);
240
+ memos.set(retval, memo);
241
+ memo.set(retval, memo.get(klass));
242
+ }
243
+ }
244
+ else if (typeof(klass) == "string") {
245
+ retval = function() {
246
+ throw new TypeError(`${klass}() must be overridden`);
247
+ }
248
+ }
249
+ else {
250
+ throw new TypeError(`abstract parameter must be a function or string`)
251
+ }
252
+
253
+ return retval;
254
+ };
255
+
256
+ /**
257
+ * A class wrapper that blocks construction of an instance if the class being
258
+ * constructed is a descendant of the current class. It also attempts to block
259
+ * extending the targeted class.
260
+ * @param {Constructor} klass The constructor of the current class.
261
+ */
262
+ function final(klass) {
263
+ /**
264
+ * Replaces the first parameter in the args list with the given class (klass)
265
+ * and calls the original method to bypass
266
+ * @param {string} fname Name of the ProxyHandler method being called.
267
+ * @param {any[]} args Arguments to the ProxyHandler method
268
+ * @returns {any}
269
+ */
270
+ function handleDefault(fname, args) {
271
+ args.shift();
272
+ args.unshift(klass);
273
+ return Reflect[fname](...args);
274
+ }
275
+
276
+ let retval = new Proxy(function() {}, {
277
+ construct(_, args, newTarget) {
278
+ if (newTarget !== retval) {
279
+ throw new TypeError("Cannot create an instance of a descendant of a final class");
280
+ }
281
+ let inst = Reflect.construct(klass, args, newTarget);
282
+ let proto = Object.create(klass.prototype, {
283
+ constructor: {
284
+ enumerable: true,
285
+ configurable: true,
286
+ writable: true,
287
+ value: retval
288
+ }
289
+ });
290
+ Object.setPrototypeOf(inst, proto);
291
+ return inst;
292
+ },
293
+ get(_, prop, receiver) {
294
+ return (prop == "prototype")
295
+ ? void 0
296
+ : Reflect.get(klass, prop, receiver);
297
+ },
298
+
299
+ set(...args) { return handleDefault("set", args); },
300
+ apply(...args) { return handleDefault("apply", args); },
301
+ defineProperty(...args) { return handleDefault("defineProperty", args); },
302
+ deleteProperty(...args) { return handleDefault("deleteProperty", args); },
303
+ getOwnPropertyDescriptor(...args) { return handleDefault("getOwnPropertyDescriptor", args); },
304
+ getPrototypeOf(...args) { return handleDefault("getPrototypeOf", args); },
305
+ has(...args) { return handleDefault("has", args); },
306
+ isExtensible(...args) { return handleDefault("isExtensible", args); },
307
+ ownKeys(...args) { return handleDefault("ownKeys", args); },
308
+ preventExtensions(...args) { return handleDefault("preventExtensions", args); },
309
+ setPrototypeOf(...args) { return handleDefault("setPrototypeOf", args); }
310
+ });
311
+
312
+ if (memos.has(klass)) {
313
+ let memo = memos.get(klass);
314
+ memos.set(retval, memo);
315
+ memo.set(retval, memo.get(klass));
316
+ }
317
+
318
+ return retval;
319
+ };
320
+
321
+ /**
322
+ * Adds specified definitions to the class prototype. All supplied definitions will
323
+ * default to {enumerable: true, configurable: true, writable: true} unless otherwise
324
+ * specified. The {writable} attribute will not be defaulted if {value} is not specified.
325
+ * This is for providing public class members that are bound to the prototype instead of
326
+ * the instance objects. Use this function in the `static {}` block of the class.
327
+ * @param {function} tgt The constructor function whose prototype will be modified.
328
+ * @param {object} defs The set of property definitions to be applied to the prototype.
329
+ */
330
+ function define(tgt, defs) {
331
+ if (typeof(tgt) !== "function") {
332
+ throw new TypeError("Invalid target type in first parameter.");
333
+ }
334
+ if (!defs || (typeof(defs) !== "object")) {
335
+ throw new TypeError("Invalid definition type in second parameter.");
336
+ }
337
+
338
+ for (let key in defs) {
339
+ let def = defs[key];
340
+ let isDef = Object.getOwnPropertyNames(def).reduce((rv, cv) => {
341
+ return rv && ["enumerable", "configurable", "writable", "value"].includes(cv);
342
+ }, true);
343
+
344
+ if (!def || (typeof(def) !== "object") || !isDef || !("value" in def)) {
345
+ continue;
346
+ }
347
+ if (!("enumerable" in def)) {
348
+ def.enumerable = true;
349
+ }
350
+ if (!("configurable" in def)) {
351
+ def.configurable = true;
352
+ }
353
+ if (!("writable" in def)) {
354
+ def.writable = true;
355
+ }
356
+ }
357
+
358
+ Object.defineProperties(tgt.prototype, defs);
359
+ }
360
+
361
+ module.exports = { share, saveSelf, accessor, abstract, final, define };
package/index.mjs CHANGED
@@ -3,11 +3,35 @@
3
3
  const memos = new WeakMap();
4
4
  const ACCESSOR = Symbol();
5
5
 
6
+ /**
7
+ * General definition of a class constructor function.
8
+ * @typedef Constructor
9
+ * @type new (...args: any[]) => object
10
+ */
11
+
12
+
13
+ /**
14
+ * Returns all public keys of the specified object.
15
+ * @param {object} o
16
+ * @returns {(string|symbol)[]}
17
+ */
6
18
  function getAllOwnKeys(o) {
7
- return Object.getOwnPropertyNames(o)
8
- .concat(Object.getOwnPropertySymbols(o));
19
+ /** @type {(string|symbol)[]} */
20
+ let retval = Object.getOwnPropertyNames(o);
21
+ return retval.concat(Object.getOwnPropertySymbols(o));
9
22
  }
10
23
 
24
+ /**
25
+ * @typedef Descriptor
26
+ * @property {*} [value]
27
+ * @property {Function} [get]
28
+ * @property {Function} [set]
29
+ *
30
+ * Calls the given function against each descriptor property that itself is
31
+ * also a function.
32
+ * @param {Descriptor} desc
33
+ * @param {Function} fn
34
+ */
11
35
  function useDescriptor(desc, fn) {
12
36
  if ("value" in desc) {
13
37
  if (typeof(desc.value) == "function") {
@@ -24,6 +48,11 @@ function useDescriptor(desc, fn) {
24
48
  }
25
49
  }
26
50
 
51
+ /**
52
+ * Binds all functions of the descriptor to the given context object.
53
+ * @param {Descriptor} desc
54
+ * @param {object} context
55
+ */
27
56
  function bindDescriptor(desc, context) {
28
57
  useDescriptor(desc, (key) => {
29
58
  desc[key] = desc[key].bind(context);
@@ -32,11 +61,13 @@ function bindDescriptor(desc, context) {
32
61
 
33
62
  /**
34
63
  * Used to both store inherited property information as well as retrieve it.
35
- * @param {Object} inst The instance object that will own the shared members.
36
- * @param {Function?} klass The constructor function of the class sharing
64
+ * @overload { (inst, klass, members) => object }
65
+ * @overload { (inst, members) => object }
66
+ * @param {object} inst The instance object that will own the shared members.
67
+ * @param {Constructor|object} klass The constructor function of the class sharing
37
68
  * members. Optional. If omitted, defaults to inst.
38
- * @param {Object} members The object containing the properties being shared.
39
- * @returns {Object} The fully constructed inheritance object.
69
+ * @param {object=} members The object containing the properties being shared.
70
+ * @returns {object} The fully constructed inheritance object.
40
71
  */
41
72
  function share(inst, klass, members) {
42
73
  let retval = {};
@@ -94,18 +125,21 @@ function share(inst, klass, members) {
94
125
 
95
126
  //Add the new members to the prototype chain of protData.
96
127
  let prototype = Object.getPrototypeOf(protData);
128
+
97
129
  let proto = Object.create(prototype,
98
130
  Object.fromEntries(mKeys
99
131
  .map(k => {
100
- if (mDesc[k].value?.hasOwnProperty(ACCESSOR)) {
101
- Object.assign(mDesc[k], mDesc[k].value);
102
- mDesc[k].enumerable = true;
103
- delete mDesc[k][ACCESSOR];
104
- delete mDesc[k].value;
105
- delete mDesc[k].writable;
132
+ // @ts-ignore
133
+ let desc = mDesc[k];
134
+ if (desc.value?.hasOwnProperty(ACCESSOR)) {
135
+ Object.assign(desc, desc.value);
136
+ desc.enumerable = true;
137
+ delete desc[ACCESSOR];
138
+ delete desc.value;
139
+ delete desc.writable;
106
140
  }
107
- bindDescriptor(mDesc[k], inst);
108
- return [k, mDesc[k]];
141
+ bindDescriptor(desc, inst);
142
+ return [k, desc];
109
143
  })));
110
144
  Object.setPrototypeOf(protData, proto);
111
145
 
@@ -147,12 +181,12 @@ function share(inst, klass, members) {
147
181
  return retval;
148
182
  }
149
183
 
150
- /**
184
+ /**
151
185
  * Binds the class instance to itself to allow code to selectively avoid Proxy
152
186
  * issues, especially ones involving private fields. Also binds the class
153
187
  * constructor to the instance for static referencing as "cla$$".
154
- * @param {Object} self The current class instance as seen from the constructor.
155
- * @param {String} name The name of the field on which to bind the instance.
188
+ * @param {object} self The current class instance as seen from the constructor.
189
+ * @param {string} name The name of the field on which to bind the instance.
156
190
  */
157
191
  function saveSelf(self, name) {
158
192
  Object.defineProperty(self, name, {value: self});
@@ -164,9 +198,9 @@ function saveSelf(self, name) {
164
198
  /**
165
199
  * Marks the property as a shared, overrideable accessor and defines the
166
200
  * getter and setter methods for it.
167
- * @param {Object} desc Object containing get and/or set definitions for the
201
+ * @param {object} desc Object containing get and/or set definitions for the
168
202
  * accessor.
169
- * @returns {Object} A tagged object that will be used to create the access
203
+ * @returns {object} A tagged object that will be used to create the access
170
204
  * bindings for the property.
171
205
  */
172
206
  function accessor(desc) {
@@ -183,15 +217,15 @@ function accessor(desc) {
183
217
  /**
184
218
  * A class wrapper that blocks construction of an instance if the class being
185
219
  * instantiated is not a descendant of the current class.
186
- * @param {function|string} klass If a function, the constructor of the current
220
+ * @param {Constructor|string} klass If a function, the constructor of the current
187
221
  * class. If a string, the name of the function being abstracted.
188
- * @returns {function} Either an extended class that denies direct construction
222
+ * @returns {Function} Either an extended class that denies direct construction
189
223
  * or a function that immediately throws.
190
224
  */
191
225
  function abstract(klass) {
192
226
  let retval;
193
227
  if (typeof(klass) == "function") {
194
- let name = klass.name?klass.name : "";
228
+ let name = klass.name ? klass.name : "";
195
229
  retval = class extends klass {
196
230
  constructor (...args) {
197
231
  if (new.target === retval) {
@@ -223,15 +257,23 @@ function abstract(klass) {
223
257
  * A class wrapper that blocks construction of an instance if the class being
224
258
  * constructed is a descendant of the current class. It also attempts to block
225
259
  * extending the targeted class.
226
- * @param {Function} klass The constructor of the current class.
260
+ * @param {Constructor} klass The constructor of the current class.
227
261
  */
228
262
  function final(klass) {
263
+ /**
264
+ * Replaces the first parameter in the args list with the given class (klass)
265
+ * and calls the original method to bypass
266
+ * @param {string} fname Name of the ProxyHandler method being called.
267
+ * @param {any[]} args Arguments to the ProxyHandler method
268
+ * @returns {any}
269
+ */
270
+ function handleDefault(fname, args) {
271
+ args.shift();
272
+ args.unshift(klass);
273
+ return Reflect[fname](...args);
274
+ }
275
+
229
276
  let retval = new Proxy(function() {}, {
230
- handleDefault(fname, args) {
231
- args.shift();
232
- args.unshift(klass);
233
- return Reflect[fname](...args);
234
- },
235
277
  construct(_, args, newTarget) {
236
278
  if (newTarget !== retval) {
237
279
  throw new TypeError("Cannot create an instance of a descendant of a final class");
@@ -253,17 +295,18 @@ function final(klass) {
253
295
  ? void 0
254
296
  : Reflect.get(klass, prop, receiver);
255
297
  },
256
- set(...args) { return this.handleDefault("set", args); },
257
- apply(...args) { return this.handleDefault("apply", args); },
258
- defineProperty(...args) { return this.handleDefault("defineProperty", args); },
259
- deleteProperty(...args) { return this.handleDefault("deleteProperty", args); },
260
- getOwnPropertyDescriptor(...args) { return this.handleDefault("getOwnPropertyDescriptor", args); },
261
- getPrototypeOf(...args) { return this.handleDefault("getPrototypeOf", args); },
262
- has(...args) { return this.handleDefault("has", args); },
263
- isExtensible(...args) { return this.handleDefault("isExtensible", args); },
264
- ownKeys(...args) { return this.handleDefault("ownKeys", args); },
265
- preventExtensions(...args) { return this.handleDefault("preventExtensions", args); },
266
- setPrototypeOf(...args) { return this.handleDefault("setPrototypeOf", args); }
298
+
299
+ set(...args) { return handleDefault("set", args); },
300
+ apply(...args) { return handleDefault("apply", args); },
301
+ defineProperty(...args) { return handleDefault("defineProperty", args); },
302
+ deleteProperty(...args) { return handleDefault("deleteProperty", args); },
303
+ getOwnPropertyDescriptor(...args) { return handleDefault("getOwnPropertyDescriptor", args); },
304
+ getPrototypeOf(...args) { return handleDefault("getPrototypeOf", args); },
305
+ has(...args) { return handleDefault("has", args); },
306
+ isExtensible(...args) { return handleDefault("isExtensible", args); },
307
+ ownKeys(...args) { return handleDefault("ownKeys", args); },
308
+ preventExtensions(...args) { return handleDefault("preventExtensions", args); },
309
+ setPrototypeOf(...args) { return handleDefault("setPrototypeOf", args); }
267
310
  });
268
311
 
269
312
  if (memos.has(klass)) {
@@ -275,4 +318,39 @@ function final(klass) {
275
318
  return retval;
276
319
  };
277
320
 
278
- export { share, saveSelf, accessor, abstract, final };
321
+ /**
322
+ * Adds specified definitions to the class prototype. All supplied definitions will
323
+ * default to {enumerable: true, configurable: true, writable: true} unless otherwise
324
+ * specified. The {writable} attribute will not be defaulted if {value} is not specified.
325
+ * This is for providing public class members that are bound to the prototype instead of
326
+ * the instance objects. Use this function in the `static {}` block of the class.
327
+ * @param {function} tgt The constructor function whose prototype will be modified.
328
+ * @param {object} defs The set of property definitions to be applied to the prototype.
329
+ */
330
+ function define(tgt, defs) {
331
+ if (typeof(tgt) !== "function") {
332
+ throw new TypeError("Invalid target type in first parameter.");
333
+ }
334
+ if (!defs || (typeof(defs) !== "object")) {
335
+ throw new TypeError("Invalid definition type in second parameter.");
336
+ }
337
+
338
+ for (let key in defs) {
339
+ let def = defs[key];
340
+ if (typeof def !== 'object' || def === null) continue;
341
+
342
+ if (!("enumerable" in def)) {
343
+ def.enumerable = true;
344
+ }
345
+ if (!("configurable" in def)) {
346
+ def.configurable = true;
347
+ }
348
+ if (("value" in def) && !("writable" in def)) {
349
+ def.writable = true;
350
+ }
351
+ }
352
+
353
+ Object.defineProperties(tgt.prototype, defs);
354
+ }
355
+
356
+ export { share, saveSelf, accessor, abstract, final, define };
package/jsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "exclude": ["**/node_modules"],
3
+ "compilerOptions": {
4
+ "module": "ES2015",
5
+ "target": "ES2022",
6
+ "checkJs": true,
7
+ "moduleResolution": "node"
8
+ }
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cfprotected",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "An implementation of protected fields on top of class fields.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -12,11 +12,15 @@
12
12
  "protected",
13
13
  "inheritance"
14
14
  ],
15
+ "exports": {
16
+ "import": "./index.mjs",
17
+ "require": "./index.js"
18
+ },
15
19
  "author": "Ranando D. King",
16
20
  "license": "Apache-2.0",
17
21
  "repository": {
18
22
  "type": "git",
19
- "url": "https://github.com/rdking/CFProtected.git"
23
+ "url": "git+https://github.com/rdking/CFProtected.git"
20
24
  },
21
25
  "devDependencies": {
22
26
  "jest": "^27.4.7",
package/test/test.mjs CHANGED
@@ -1,222 +1,284 @@
1
- import { TestWatcher } from "@jest/core";
2
- import { share, saveSelf, accessor, abstract, final } from "../index"; //require("cfprotected");
3
-
4
- class Base {
5
- static #greeting = "Hello!";
6
- static #sprot = share(this, {
7
- getGreeting() { return this.#greeting; }
8
- });
9
- #prot = share(this, Base, {
10
- num: 42,
11
- name: "John Jacob Jingleheimerschmidt",
12
- method: () => {
13
- return "It works.";
14
- },
1
+ import { share, saveSelf, accessor, abstract, final, define } from "../index.mjs";
2
+
3
+ describe('CFProtected Library', () => {
4
+
5
+ describe('share()', () => {
6
+ const greeting = "Hello!";
7
+ const name = "John Jacob Jingleheimerschmidt";
8
+ const num = 42;
9
+ const methodResult = "It works.";
10
+ const propTestValue = "I can make this return anything!";
11
+ const superTestResult = 1;
12
+ const subTestResult = 2;
13
+ const symbolKey = Symbol('test');
14
+ const symbolValue = 'symbol value';
15
+
16
+ class Base {
17
+ static #greeting = greeting;
18
+ static #sprot = share(this, {
19
+ getGreeting: () => this.#greeting,
20
+ superTest: () => 0,
21
+ });
22
+
23
+ #propTestVal = propTestValue;
24
+ #prot = share(this, Base, {
25
+ num,
26
+ name,
27
+ method: () => methodResult,
15
28
  prop: accessor({
16
- get: () => this.propTestVal
29
+ get: () => this.#propTestVal,
30
+ set: (v) => this.#propTestVal = v
17
31
  }),
18
- superTest: () => {
19
- return 1;
20
- }
21
- });
32
+ superTest: () => superTestResult,
33
+ [symbolKey]: symbolValue
34
+ });
22
35
 
23
- constructor() {
24
- saveSelf(this, "pvt");
36
+ constructor() {
37
+ saveSelf(this, 'pvt');
38
+ }
39
+
40
+ getProt() { return this.#prot; }
41
+ getPropTestVal() { return this.#propTestVal; }
42
+ static getSprot() { return this.#sprot; }
25
43
  }
26
44
 
27
- testName = "John Jacob Jingleheimerschmidt";
28
- propTestVal = "I can make this return anything!";
45
+ class Sub extends Base {
46
+ static #sprot = share(this, {
47
+ superTest: () => 1
48
+ });
49
+ static getSprot() { return this.#sprot; } // Add this
50
+ #prot = share(this, Sub, {
51
+ superTest: () => subTestResult,
52
+ });
29
53
 
30
- checkProp(proxied) {
31
- expect((proxied?this.pvt:this).#prot.prop).toBe(this.propTestVal);
32
- }
54
+ constructor() {
55
+ super();
56
+ }
33
57
 
34
- run() {
35
- test(`Access to shared members should just work on the instance`, () => {
36
- expect(this.#prot.num).toBe(42);
37
- expect(this.#prot.name).toBe(this.testName);
38
- expect(this.#prot.method()).toBe("It works.");
39
- this.checkProp();
40
- });
41
-
42
- test(`Access to shared members should work even through a Proxy`, () => {
43
- let that = new Proxy(this, {});
44
- expect(that.pvt.#prot.num).toBe(42);
45
- expect(that.pvt.#prot.name).toBe(this.testName);
46
- expect(that.pvt.#prot.method()).toBe("It works.");
47
- this.checkProp(true);
48
- });
58
+ getProt() { return this.#prot; }
49
59
  }
50
- }
51
60
 
52
- class Derived extends Base {
53
- #prot = share(this, Derived, {
54
- otherMethod: () => {
55
- this.#prot.name = this.testName;
56
- },
57
- prop: accessor({
58
- get: () => this.propTestVal2
59
- })
60
- });
61
+ let baseInst;
62
+ let subInst;
61
63
 
62
- constructor() {
63
- super();
64
- saveSelf(this, "pvt");
65
- this.propTestVal2 = this.propTestVal;
66
- }
64
+ beforeEach(() => {
65
+ baseInst = new Base();
66
+ subInst = new Sub();
67
+ });
67
68
 
68
- run() {
69
- super.run();
69
+ test('should allow access to basic properties', () => {
70
+ expect(baseInst.getProt().num).toBe(num);
71
+ expect(baseInst.getProt().name).toBe(name);
72
+ });
70
73
 
71
- test(`Should be able to change a shared property value`, () => {
72
- this.testName = "A. Nony Mouse";
73
- expect(() => { this.#prot.otherMethod(); }).not.toThrow();
74
- this.checkProp();
75
- });
76
- }
77
- }
74
+ test('should allow calling methods', () => {
75
+ expect(baseInst.getProt().method()).toBe(methodResult);
76
+ });
78
77
 
79
- class NonParticipant extends Base {}
78
+ test('should handle accessor properties (get)', () => {
79
+ expect(baseInst.getProt().prop).toBe(propTestValue);
80
+ });
81
+
82
+ test('should handle accessor properties (set)', () => {
83
+ const newValue = "new value";
84
+ baseInst.getProt().prop = newValue;
85
+ expect(baseInst.getPropTestVal()).toBe(newValue);
86
+ });
80
87
 
81
- class GrandChild extends NonParticipant {
82
- static #sprot = share(this, {
83
- getGreeting() {
84
- return `${this.#sprot.$uper.getGreeting()} My name is`;
85
- }
88
+ test('should handle symbol properties', () => {
89
+ expect(baseInst.getProt()[symbolKey]).toBe(symbolValue);
86
90
  });
87
- #prot = share(this, GrandChild, {
88
- otherMethod: () => {
89
- this.#prot.name = this.testName;
90
- },
91
- superTest: () => {
92
- return 1 + this.pvt.#prot.$uper.superTest();
93
- }
91
+
92
+ test('should handle inheritance correctly', () => {
93
+ const subProt = subInst.getProt();
94
+ expect(subProt.num).toBe(num);
95
+ expect(subProt.name).toBe(name);
96
+ expect(subProt.method()).toBe(methodResult);
94
97
  });
95
98
 
96
- constructor() {
97
- super();
98
- saveSelf(this, "pvt");
99
- }
99
+ test('should handle overriding properties', () => {
100
+ expect(baseInst.getProt().superTest()).toBe(superTestResult);
101
+ expect(subInst.getProt().superTest()).toBe(subTestResult);
102
+ });
100
103
 
101
- run() {
102
- super.run();
104
+ test('should provide access to superclass implementations via $uper', () => {
105
+ const subProt = subInst.getProt();
106
+ expect(subProt.$uper.superTest()).toBe(superTestResult);
107
+ });
108
+
109
+ test('should handle static property sharing', () => {
110
+ expect(Base.getSprot().getGreeting()).toBe(greeting);
111
+ });
112
+
113
+ test('should handle static property inheritance and $uper', () => {
114
+ expect(Sub.getSprot().$uper.superTest()).toBe(0);
115
+ expect(Sub.getSprot().superTest()).toBe(1);
116
+ });
103
117
 
104
- test(`Should be able to change a shared property value`, () => {
105
- this.testName = "A. Nony Mouse 1";
106
- expect(() => { this.#prot.otherMethod(); }).not.toThrow();
107
- this.checkProp();
108
- });
109
- }
110
- }
118
+ test('should throw TypeError for invalid arguments', () => {
119
+ expect(() => share(null, Base, {})).toThrow(TypeError);
120
+ expect(() => share({}, null, {})).toThrow(TypeError);
121
+ expect(() => share({}, Base, null)).toThrow(TypeError);
122
+ });
123
+
124
+ test('should work with share(klass, members) overload', () => {
125
+ class A {
126
+ static #shared = share(this, { val: 1 });
127
+ static getShared() { return this.#shared; }
128
+ }
129
+ expect(A.getShared().val).toBe(1);
130
+ });
131
+ });
111
132
 
112
- class SuperTest extends GrandChild {
113
- static #sprot = share(this, {
114
- getGreeting() {
115
- return `${this.#sprot.$uper.getGreeting()} "${this.name}"!`
133
+ describe('saveSelf()', () => {
134
+ test('should bind the instance to a property', () => {
135
+ class Test {
136
+ constructor() {
137
+ saveSelf(this, 'myself');
116
138
  }
139
+ }
140
+ const inst = new Test();
141
+ expect(inst.myself).toBe(inst);
117
142
  });
118
- #prot = share(this, SuperTest, {
119
- superTest: () => {
120
- return 1 + this.pvt.#prot.$uper.superTest();
143
+
144
+ test('should add "cla$$" property to prototype when saving constructor', () => {
145
+ class Test {
146
+ static {
147
+ saveSelf(this, 'pvt');
121
148
  }
149
+ }
150
+ const inst = new Test();
151
+ expect(Test.pvt).toBe(Test);
152
+ expect(inst.cla$$).toBe(Test);
122
153
  });
154
+ });
123
155
 
124
- constructor() {
125
- super();
126
- saveSelf(this, "pvt");
127
- }
156
+ describe('accessor()', () => {
157
+ test('should return a descriptor for a valid getter/setter object', () => {
158
+ const desc = { get: () => 'foo', set: () => {} };
159
+ const acc = accessor(desc);
160
+ expect(typeof acc).toBe('object');
161
+ expect('get' in acc).toBe(true);
162
+ expect('set' in acc).toBe(true);
163
+ expect(Object.getOwnPropertySymbols(acc).length).toBe(1); // The ACCESSOR symbol
164
+ });
128
165
 
129
- run() {
130
- test(`Should be able to call super through the entire inheritance chain`, () => {
131
- expect(this.pvt.#prot.superTest()).toBe(3);
132
- });
133
- test(`Should be able to call super through the entire static inheritance chain`, () => {
134
- expect(SuperTest.#sprot.getGreeting()).toBe(`Hello! My name is "SuperTest"!`);
135
- });
136
- }
137
- }
166
+ test('should return undefined for invalid input', () => {
167
+ expect(accessor({})).toBeUndefined();
168
+ expect(accessor({ foo: 'bar' })).toBeUndefined();
169
+ expect(accessor(123)).toBeUndefined();
170
+ });
171
+ });
138
172
 
139
- describe(`Testing shared elements in the base class`, () => {
140
- (new Base).run();
141
- });
142
- describe(`Testing shared elements in a direct descendant`, () => {
143
- (new Derived).run();
144
- });
173
+ describe('abstract()', () => {
174
+ const A = abstract(class A {});
175
+ class B extends A {}
145
176
 
146
- describe(`Testing shared elements inherited from a non-participant`, () => {
147
- (new GrandChild).run();
148
- });
177
+ test('should throw when directly instantiating an abstract class', () => {
178
+ expect(() => new A()).toThrow(TypeError);
179
+ expect(() => new A()).toThrow("Class constructor A is abstract and cannot be directly invoked with 'new'");
180
+ });
149
181
 
150
- describe(`Testing that $uper works in all cases`, () => {
151
- (new SuperTest).run();
152
- });
182
+ test('should not throw when instantiating a derived class', () => {
183
+ expect(() => new B()).not.toThrow();
184
+ });
153
185
 
154
- describe(`Testing that abstract classes function as expected`, () => {
155
- const key = Symbol();
156
- const ATest = abstract(class ATest {
157
- static #sshared = share(this, {
158
- [key]: true
159
- });
160
- #shared = share(this, ATest, {
161
- [key]: true
162
- });
186
+ test('should create a function that throws when given a string', () => {
187
+ const abstractFn = abstract('myFunc');
188
+ expect(typeof abstractFn).toBe('function');
189
+ expect(() => abstractFn()).toThrow(TypeError);
190
+ expect(() => abstractFn()).toThrow('myFunc() must be overridden');
163
191
  });
164
- class DTest extends ATest {
165
- static #sshared = share(this, {});
166
- #shared = share(this, DTest, {});
167
- run() { return this.#shared[key]; }
168
- static run() { return this.#sshared[key]; }
169
- };
170
192
 
171
- test(`Should not be able to instantiate directly`, () => {
172
- expect(() => { new ATest; }).toThrow();
193
+ test('should throw a TypeError for invalid input', () => {
194
+ expect(() => abstract(123)).toThrow(TypeError);
195
+ expect(() => abstract(123)).toThrow('abstract parameter must be a function or string');
173
196
  });
174
- test(`Should be able to instantiate a derived class`, () => {
175
- expect(() => { new DTest; }).not.toThrow();
197
+
198
+ test('should handle classes without names', () => {
199
+ const NoName = abstract(class {});
200
+ expect(() => new NoName()).toThrow("Class constructor is abstract and cannot be directly invoked with 'new'");
176
201
  });
177
- test(`Should see shared members from constructed instance`, () => {
178
- expect((new DTest).run()).toBe(true);
202
+ });
203
+
204
+ describe('final()', () => {
205
+ const F = final(class F {});
206
+
207
+ test('should allow direct instantiation', () => {
208
+ expect(() => new F()).not.toThrow();
179
209
  });
180
- test(`Should see static shared members from constructed instance`, () => {
181
- expect(DTest.run()).toBe(true);
210
+
211
+ test('should throw when trying to extend a final class', () => {
212
+ // The error happens at class definition time
213
+ expect(() => {
214
+ class D extends F {}
215
+ }).toThrow(TypeError);
216
+ });
217
+
218
+ test('should throw when trying to create an instance of a descendant', () => {
219
+ // This simulates a bypass of the extension block
220
+ function cheat() {
221
+ const D = function() {};
222
+ Object.setPrototypeOf(D, F);
223
+ Reflect.construct(F, [], D);
224
+ }
225
+ expect(cheat).toThrow("Cannot create an instance of a descendant of a final class");
182
226
  });
183
- });
184
227
 
185
- describe(`Testing that final classes function as expected`, () => {
186
- const key = Symbol();
187
- class TestBase {
188
- static #sshared = share(this, {
189
- [key]: true
190
- });
191
- #shared = share(this, TestBase, {
192
- [key]: true
193
- });
194
- }
195
- const FTest = final(class FTest extends TestBase {
196
- static { saveSelf(this, "pvt"); }
197
- static #sshared = share(this, {});
198
- #shared = share(this, FTest, {});
199
- run() { return this.#shared[key]; }
200
- static run() { return this.pvt.#sshared[key]; }
228
+ test('prototype property should be undefined', () => {
229
+ expect(F.prototype).toBeUndefined();
230
+ });
231
+ });
232
+
233
+ describe('define()', () => {
234
+ class MyClass {}
235
+
236
+ define(MyClass, {
237
+ prop1: { value: 10 },
238
+ prop2: {
239
+ get: function() { return 20; },
240
+ enumerable: false
241
+ },
242
+ prop3: {
243
+ value: () => 30,
244
+ writable: false,
245
+ }
201
246
  });
202
247
 
203
- test(`Should be able to instantiate an instance directly`, () => {
204
- expect(() => { new FTest; }).not.toThrow();
248
+ const inst = new MyClass();
249
+
250
+ test('should define properties on the class prototype', () => {
251
+ expect(MyClass.prototype.hasOwnProperty('prop1')).toBe(true);
252
+ expect(inst.prop1).toBe(10);
205
253
  });
206
- test(`Should not be able to extend directly`, () => {
207
- expect(() => { class DTest extends FTest {}; }).toThrow();
254
+
255
+ test('should set default attributes (enumerable, configurable, writable)', () => {
256
+ const desc1 = Object.getOwnPropertyDescriptor(MyClass.prototype, 'prop1');
257
+ expect(desc1.enumerable).toBe(true);
258
+ expect(desc1.configurable).toBe(true);
259
+ expect(desc1.writable).toBe(true);
208
260
  });
209
- test(`Should not be able to cheat and create an instance of a derived class`, () => {
210
- expect(() => {
211
- FTest.prototype = {};
212
- class DTest extends FTest {}
213
- new DTest;
214
- }).toThrow();
261
+
262
+ test('should respect specified attributes', () => {
263
+ const desc2 = Object.getOwnPropertyDescriptor(MyClass.prototype, 'prop2');
264
+ expect(desc2.enumerable).toBe(false); // Overridden
265
+ expect(desc2.configurable).toBe(true); // Defaulted
266
+ expect(desc2.writable).toBeUndefined(); // Not a value property
215
267
  });
216
- test(`Should see shared members from constructed instance`, () => {
217
- expect((new FTest).run()).toBe(true);
268
+
269
+ test('should not default writable if value is not present', () => {
270
+ const desc = Object.getOwnPropertyDescriptor(MyClass.prototype, 'prop2');
271
+ expect('writable' in desc).toBe(false);
218
272
  });
219
- test(`Should see static shared members from constructed instance`, () => {
220
- expect(FTest.run()).toBe(true);
273
+
274
+ test('should respect specified writable attribute', () => {
275
+ const desc3 = Object.getOwnPropertyDescriptor(MyClass.prototype, 'prop3');
276
+ expect(desc3.writable).toBe(false);
221
277
  });
222
- });
278
+
279
+ test('should throw TypeError for invalid arguments', () => {
280
+ expect(() => define(null, {})).toThrow(TypeError);
281
+ expect(() => define(MyClass, null)).toThrow(TypeError);
282
+ });
283
+ });
284
+ });