cfprotected 0.1.0 → 1.1.2
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/.vscode/launch.json +9 -5
- package/README.md +4 -7
- package/{index.js → index.mjs} +75 -21
- package/package.json +18 -3
- package/test/{test.js → test.mjs} +73 -3
package/.vscode/launch.json
CHANGED
|
@@ -4,16 +4,20 @@
|
|
|
4
4
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
5
5
|
"version": "0.2.0",
|
|
6
6
|
"configurations": [
|
|
7
|
+
|
|
7
8
|
{
|
|
8
9
|
"type": "node",
|
|
9
10
|
"request": "launch",
|
|
10
11
|
"name": "Jest Tests",
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"${workspaceRoot}/
|
|
12
|
+
"runtimeArgs": [
|
|
13
|
+
"--experimental-vm-modules",
|
|
14
|
+
"--inspect-brk",
|
|
15
|
+
"${workspaceRoot}/node_modules/jest/bin/jest.js",
|
|
16
|
+
"--runInBand"
|
|
15
17
|
],
|
|
16
|
-
"
|
|
18
|
+
"internalConsoleOptions": "neverOpen",
|
|
19
|
+
"console": "integratedTerminal",
|
|
20
|
+
"port": 9229
|
|
17
21
|
}
|
|
18
22
|
]
|
|
19
23
|
}
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ to work around some the issues inherent in using private fields.
|
|
|
8
8
|
Let's just start with a simple example:
|
|
9
9
|
|
|
10
10
|
```js
|
|
11
|
-
|
|
11
|
+
import { share, accessor } from "cfprotected";
|
|
12
12
|
|
|
13
13
|
class Example {
|
|
14
14
|
//shared static private fields
|
|
@@ -111,19 +111,16 @@ const Example = abstract(class {
|
|
|
111
111
|
});
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
### Note:
|
|
115
|
-
This method uses a proxy internally to keep things consistent. However, the Proxy is not a membrane. So it's best if any use of static private fields is accessed through the property created by using `saveSelf(...)`.
|
|
116
|
-
|
|
117
114
|
## **final(klass)**
|
|
118
|
-
This method is a class wrapper that prevents instances of the class from being created using descendant classes.
|
|
115
|
+
This method is a class wrapper that prevents instances of the class from being created using descendant classes. It also attempts to prevent creation of descendant classes.
|
|
119
116
|
```js
|
|
120
117
|
const Example = final(class {
|
|
121
118
|
...
|
|
122
119
|
});
|
|
123
120
|
```
|
|
124
121
|
|
|
125
|
-
###
|
|
126
|
-
|
|
122
|
+
### Notes:
|
|
123
|
+
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.
|
|
127
124
|
|
|
128
125
|
## Other features
|
|
129
126
|
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.
|
package/{index.js → index.mjs}
RENAMED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
1
3
|
const memos = new WeakMap();
|
|
2
4
|
const ACCESSOR = Symbol();
|
|
3
5
|
|
|
@@ -14,7 +16,7 @@ function getAllOwnKeys(o) {
|
|
|
14
16
|
* @param {Object} members The object containing the properties being shared.
|
|
15
17
|
* @returns {Object} The fully constructed inheritance object.
|
|
16
18
|
*/
|
|
17
|
-
|
|
19
|
+
function share(inst, klass, members) {
|
|
18
20
|
let retval = {};
|
|
19
21
|
|
|
20
22
|
if ((typeof(inst) == "function")
|
|
@@ -60,7 +62,8 @@ module.exports.share = function share(inst, klass, members) {
|
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
//Get the protected data object.
|
|
63
|
-
let
|
|
65
|
+
let ancestorKey = (inst === klass) ? ancestor : inst;
|
|
66
|
+
let memo = ancestorMemo.get(ancestorKey) || {data: {}, $uper: {}, inheritance: null};
|
|
64
67
|
let protData = memo.data;
|
|
65
68
|
|
|
66
69
|
//Get the details of the protected properties.
|
|
@@ -124,7 +127,7 @@ module.exports.share = function share(inst, klass, members) {
|
|
|
124
127
|
* @param {Object} self The current class instance as seen from the constructor.
|
|
125
128
|
* @param {String} name The name of the field on which to bind the instance.
|
|
126
129
|
*/
|
|
127
|
-
|
|
130
|
+
function saveSelf(self, name) {
|
|
128
131
|
Object.defineProperty(self, name, {value: self});
|
|
129
132
|
if (typeof(self) == "function") {
|
|
130
133
|
Object.defineProperty(self.prototype, "cla$$", {value: self});
|
|
@@ -139,7 +142,7 @@ module.exports.share = function share(inst, klass, members) {
|
|
|
139
142
|
* @returns {Object} A tagged object that will be used to create the access
|
|
140
143
|
* bindings for the property.
|
|
141
144
|
*/
|
|
142
|
-
|
|
145
|
+
function accessor(desc) {
|
|
143
146
|
if ((typeof(desc) == "object") &&
|
|
144
147
|
(("get" in desc) || ("set" in desc))) {
|
|
145
148
|
return {
|
|
@@ -155,29 +158,80 @@ module.exports.accessor = function(desc) {
|
|
|
155
158
|
* instantiated is not a descendant of the current class.
|
|
156
159
|
* @param {Function} klass The constructor of the current class.
|
|
157
160
|
*/
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
function abstract(klass) {
|
|
162
|
+
let name = klass.name?klass.name : "";
|
|
163
|
+
let retval = class extends klass {
|
|
164
|
+
constructor (...args) {
|
|
165
|
+
if (new.target === retval) {
|
|
166
|
+
throw new TypeError(`Class constructor ${name} is abstract and cannot be directly invoked with 'new'`);
|
|
167
|
+
}
|
|
168
|
+
super(...args);
|
|
165
169
|
}
|
|
166
|
-
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
if (memos.has(klass)) {
|
|
173
|
+
let memo = memos.get(klass);
|
|
174
|
+
memos.set(retval, memo);
|
|
175
|
+
memo.set(retval, memo.get(klass));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return retval;
|
|
167
179
|
};
|
|
168
180
|
|
|
169
181
|
/**
|
|
170
182
|
* A class wrapper that blocks construction of an instance if the class being
|
|
171
|
-
* constructed is a descendant of the current class.
|
|
183
|
+
* constructed is a descendant of the current class. It also attempts to block
|
|
184
|
+
* extending the targeted class.
|
|
172
185
|
* @param {Function} klass The constructor of the current class.
|
|
173
186
|
*/
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
187
|
+
function final(klass) {
|
|
188
|
+
let retval = new Proxy(function() {}, {
|
|
189
|
+
handleDefault(fname, args) {
|
|
190
|
+
args.shift();
|
|
191
|
+
args.unshift(klass);
|
|
192
|
+
return Reflect[fname](...args);
|
|
193
|
+
},
|
|
194
|
+
construct(_, args, newTarget) {
|
|
195
|
+
if (newTarget !== retval) {
|
|
196
|
+
throw new TypeError("Cannot create an instance of a descendant of a final class");
|
|
197
|
+
}
|
|
198
|
+
let inst = Reflect.construct(klass, args, newTarget);
|
|
199
|
+
let proto = Object.create(klass.prototype, {
|
|
200
|
+
constructor: {
|
|
201
|
+
enumerable: true,
|
|
202
|
+
configurable: true,
|
|
203
|
+
writable: true,
|
|
204
|
+
value: retval
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
Object.setPrototypeOf(inst, proto);
|
|
208
|
+
return inst;
|
|
209
|
+
},
|
|
210
|
+
get(_, prop, receiver) {
|
|
211
|
+
return (prop == "prototype")
|
|
212
|
+
? void 0
|
|
213
|
+
: Reflect.get(klass, prop, receiver);
|
|
214
|
+
},
|
|
215
|
+
set(...args) { return this.handleDefault("set", args); },
|
|
216
|
+
apply(...args) { return this.handleDefault("apply", args); },
|
|
217
|
+
defineProperty(...args) { return this.handleDefault("defineProperty", args); },
|
|
218
|
+
deleteProperty(...args) { return this.handleDefault("deleteProperty", args); },
|
|
219
|
+
getOwnPropertyDescriptor(...args) { return this.handleDefault("getOwnPropertyDescriptor", args); },
|
|
220
|
+
getPrototypeOf(...args) { return this.handleDefault("getPrototypeOf", args); },
|
|
221
|
+
has(...args) { return this.handleDefault("has", args); },
|
|
222
|
+
isExtensible(...args) { return this.handleDefault("isExtensible", args); },
|
|
223
|
+
ownKeys(...args) { return this.handleDefault("ownKeys", args); },
|
|
224
|
+
preventExtensions(...args) { return this.handleDefault("preventExtensions", args); },
|
|
225
|
+
setPrototypeOf(...args) { return this.handleDefault("setPrototypeOf", args); }
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (memos.has(klass)) {
|
|
229
|
+
let memo = memos.get(klass);
|
|
230
|
+
memos.set(retval, memo);
|
|
231
|
+
memo.set(retval, memo.get(klass));
|
|
232
|
+
}
|
|
179
233
|
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
});
|
|
234
|
+
return retval;
|
|
183
235
|
};
|
|
236
|
+
|
|
237
|
+
export { share, saveSelf, accessor, abstract, final };
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cfprotected",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "An implementation of protected fields on top of class fields.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "jest"
|
|
7
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules npx jest"
|
|
8
8
|
},
|
|
9
9
|
"keywords": [
|
|
10
10
|
"class",
|
|
@@ -19,6 +19,21 @@
|
|
|
19
19
|
"url": "https://github.com/rdking/CFProtected.git"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"jest": "^27.4.
|
|
22
|
+
"jest": "^27.4.7",
|
|
23
|
+
"jest-esm-transformer": "^1.0.0"
|
|
24
|
+
},
|
|
25
|
+
"jest": {
|
|
26
|
+
"verbose": true,
|
|
27
|
+
"moduleFileExtensions": [
|
|
28
|
+
"js",
|
|
29
|
+
"mjs"
|
|
30
|
+
],
|
|
31
|
+
"transform": {
|
|
32
|
+
"\\.m?[j|t]sx?$": "babel-jest"
|
|
33
|
+
},
|
|
34
|
+
"testMatch": [
|
|
35
|
+
"**/__tests__/**/*.?(m)[jt]s?(x)",
|
|
36
|
+
"**/?(*.)+(spec|test).?(m)[tj]s?(x)"
|
|
37
|
+
]
|
|
23
38
|
}
|
|
24
39
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { TestWatcher } from "@jest/core";
|
|
2
|
+
import { share, saveSelf, accessor, abstract, final } from "../index"; //require("cfprotected");
|
|
3
3
|
|
|
4
4
|
class Base {
|
|
5
5
|
#prot = share(this, Base, {
|
|
@@ -135,4 +135,74 @@ describe(`Testing shared elements inherited from a non-participant`, () => {
|
|
|
135
135
|
|
|
136
136
|
describe(`Testing that $uper works in all cases`, () => {
|
|
137
137
|
(new SuperTest).run();
|
|
138
|
-
})
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe(`Testing that abstract classes function as expected`, () => {
|
|
141
|
+
const key = Symbol();
|
|
142
|
+
const ATest = abstract(class ATest {
|
|
143
|
+
static #sshared = share(this, {
|
|
144
|
+
[key]: true
|
|
145
|
+
});
|
|
146
|
+
#shared = share(this, ATest, {
|
|
147
|
+
[key]: true
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
class DTest extends ATest {
|
|
151
|
+
static #sshared = share(this, {});
|
|
152
|
+
#shared = share(this, DTest, {});
|
|
153
|
+
run() { return this.#shared[key]; }
|
|
154
|
+
static run() { return this.#sshared[key]; }
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
test(`Should not be able to instantiate directly`, () => {
|
|
158
|
+
expect(() => { new ATest; }).toThrow();
|
|
159
|
+
});
|
|
160
|
+
test(`Should be able to instantiate a derived class`, () => {
|
|
161
|
+
expect(() => { new DTest; }).not.toThrow();
|
|
162
|
+
});
|
|
163
|
+
test(`Should see shared members from constructed instance`, () => {
|
|
164
|
+
expect((new DTest).run()).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
test(`Should see static shared members from constructed instance`, () => {
|
|
167
|
+
expect(DTest.run()).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe(`Testing that final classes function as expected`, () => {
|
|
172
|
+
const key = Symbol();
|
|
173
|
+
class TestBase {
|
|
174
|
+
static #sshared = share(this, {
|
|
175
|
+
[key]: true
|
|
176
|
+
});
|
|
177
|
+
#shared = share(this, TestBase, {
|
|
178
|
+
[key]: true
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
const FTest = final(class FTest extends TestBase {
|
|
182
|
+
static { saveSelf(this, "pvt"); }
|
|
183
|
+
static #sshared = share(this, {});
|
|
184
|
+
#shared = share(this, FTest, {});
|
|
185
|
+
run() { return this.#shared[key]; }
|
|
186
|
+
static run() { return this.pvt.#sshared[key]; }
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test(`Should be able to instantiate an instance directly`, () => {
|
|
190
|
+
expect(() => { new FTest; }).not.toThrow();
|
|
191
|
+
});
|
|
192
|
+
test(`Should not be able to extend directly`, () => {
|
|
193
|
+
expect(() => { class DTest extends FTest {}; }).toThrow();
|
|
194
|
+
});
|
|
195
|
+
test(`Should not be able to cheat and create an instance of a derived class`, () => {
|
|
196
|
+
expect(() => {
|
|
197
|
+
FTest.prototype = {};
|
|
198
|
+
class DTest extends FTest {}
|
|
199
|
+
new DTest;
|
|
200
|
+
}).toThrow();
|
|
201
|
+
});
|
|
202
|
+
test(`Should see shared members from constructed instance`, () => {
|
|
203
|
+
expect((new FTest).run()).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
test(`Should see static shared members from constructed instance`, () => {
|
|
206
|
+
expect(FTest.run()).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
});
|