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 +25 -0
- package/README.md +21 -0
- package/index.js +361 -0
- package/index.mjs +118 -40
- package/jsconfig.json +9 -0
- package/package.json +6 -2
- package/test/test.mjs +240 -178
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
|
-
|
|
8
|
-
|
|
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
|
-
* @
|
|
36
|
-
* @
|
|
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 {
|
|
39
|
-
* @returns {
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
delete
|
|
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(
|
|
108
|
-
return [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 {
|
|
155
|
-
* @param {
|
|
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 {
|
|
201
|
+
* @param {object} desc Object containing get and/or set definitions for the
|
|
168
202
|
* accessor.
|
|
169
|
-
* @returns {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cfprotected",
|
|
3
|
-
"version": "1.
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
29
|
+
get: () => this.#propTestVal,
|
|
30
|
+
set: (v) => this.#propTestVal = v
|
|
17
31
|
}),
|
|
18
|
-
superTest: () =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
});
|
|
32
|
+
superTest: () => superTestResult,
|
|
33
|
+
[symbolKey]: symbolValue
|
|
34
|
+
});
|
|
22
35
|
|
|
23
|
-
|
|
24
|
-
saveSelf(this,
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
54
|
+
constructor() {
|
|
55
|
+
super();
|
|
56
|
+
}
|
|
33
57
|
|
|
34
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
baseInst = new Base();
|
|
66
|
+
subInst = new Sub();
|
|
67
|
+
});
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this.checkProp();
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
}
|
|
74
|
+
test('should allow calling methods', () => {
|
|
75
|
+
expect(baseInst.getProt().method()).toBe(methodResult);
|
|
76
|
+
});
|
|
78
77
|
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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(
|
|
140
|
-
(
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
});
|
|
182
|
+
test('should not throw when instantiating a derived class', () => {
|
|
183
|
+
expect(() => new B()).not.toThrow();
|
|
184
|
+
});
|
|
153
185
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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(
|
|
172
|
-
expect(() =>
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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
|
+
});
|