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.
@@ -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
- "program": "${workspaceRoot}/node_modules/jest/bin/jest.js",
12
- "internalConsoleOptions": "openOnSessionStart",
13
- "outFiles": [
14
- "${workspaceRoot}/dist/**/*"
12
+ "runtimeArgs": [
13
+ "--experimental-vm-modules",
14
+ "--inspect-brk",
15
+ "${workspaceRoot}/node_modules/jest/bin/jest.js",
16
+ "--runInBand"
15
17
  ],
16
- "envFile": "${workspaceRoot}/.env"
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
- const { share, accessor } = require("cfprotected");
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
- ### Note:
126
- Just as with `abstract(...)`, 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(...)`. Unfortunately, there's also a limitation with final. Normally in compiled languages, it is a syntax error to even create a class that extends from a final class. However, without taking over the means of building the class (as can be done with Babel or my ClassicJS library) it's just not possible to restrict that event. As a result, the only thing left to check for is construction of instances of such an error.
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.
@@ -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
- module.exports.share = function share(inst, klass, members) {
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 memo = ancestorMemo.get(inst) || {data: {}, $uper: {}, inheritance: null};
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
- module.exports.saveSelf = function(self, name) {
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
- module.exports.accessor = function(desc) {
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
- module.exports.abstract = function(klass) {
159
- return new Proxy(klass, {
160
- construct(target, args, newTarget) {
161
- if (newTarget.prototype === klass.prototype)
162
- throw new TypeError(`Cannot construct instance of abstract class.`);
163
-
164
- return Reflect.construct(target, args, newTarget);
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
- module.exports.final = function(klass) {
175
- return new Proxy(klass, {
176
- construct(target, args, newTarget) {
177
- if (newTarget.prototype !== klass.prototype)
178
- throw new TypeError(`Cannot extend final class.`);
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
- return Reflect.construct(target, args, newTarget);
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": "0.1.0",
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.3"
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
- const { TestWatcher } = require("@jest/core");
2
- const { share, saveSelf, accessor } = require("../index"); //require("cfprotected");
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
+ });