├── .github └── workflows │ ├── bun.yml │ └── node.js.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.ts ├── package-lock.json ├── package.json └── test.ts /.github/workflows/bun.yml: -------------------------------------------------------------------------------- 1 | name: Bun CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: oven-sh/setup-bun@v1 17 | - run: bun install 18 | - run: bun run test:bun 19 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [12.x, 20.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | cache: 'npm' 25 | - run: npm i 26 | - run: npm test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | node_modules/ 3 | cjs 4 | esm 5 | *.tgz 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | node_modules/ 3 | test.ts 4 | *.tgz 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 A-yon Lee 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JS-Magic 2 | 3 | **JavaScript magic methods support.** 4 | 5 | We know that ES6 brings the capability of Proxy that allows us observing an 6 | object, and setters ang getters are build-in support in JavaScript, but if we 7 | need to build those things every time, that's just mess and pain. With this 8 | package, you can define setters and getters, along with other functions, right 9 | in the class definition itself, and when instantiating the class, the instance 10 | will always have the the benefits of the magical calling functionalities. 11 | 12 | This package is inspired by PHP magic methods, and currently supports these 13 | methods: `__get`, `__set`, `__has`, `__delete`, `__invoke`. Other methods like 14 | `toString` and `toJSON` are built-in support in JavaScript. 15 | 16 | ## Install 17 | 18 | ```sh 19 | npm i js-magic 20 | ``` 21 | 22 | ### In Deno 23 | 24 | Just import this package directly: 25 | 26 | ```ts 27 | import { applyMagic } from "https://deno.land/x/js_magic/index.ts"; 28 | ``` 29 | 30 | ## Example 31 | 32 | ```typescript 33 | // This example is coded in TypeScript, be aware of the difference between TS 34 | // and JS. All the magic methods are optional, but here I'll show all the usage 35 | // of them. 36 | 37 | import { applyMagic, MagicalClass } from "js-magic"; 38 | 39 | @applyMagic 40 | export class Car implements MagicalClass { 41 | name!: string; 42 | wheels?: number; 43 | 44 | constructor(name?: string, wheels?: number) { 45 | if (name !== undefined) this.name = name; 46 | if (wheels !== undefined) this.wheels = wheels; 47 | } 48 | 49 | /** 50 | * If a property doesn't exist, returns `null` instead of `undefined`, and 51 | * if the property is 'name', returns it according to the class name plus 52 | * 'Instance'. 53 | */ 54 | __get(prop: string | symbol): any { 55 | return prop in this ? this[prop] 56 | : (prop == "name" ? this.constructor.name + " Instance" : null); 57 | } 58 | 59 | /** If the property is name, appends it with 'Instance'. */ 60 | __set(prop: string | symbol, value: any): void { 61 | this[prop] = prop == "name" ? value + " Instance" : value; 62 | } 63 | 64 | /** 65 | * Ignores the properties starts with '__', and always returns `true` when 66 | * testing 'name'. 67 | */ 68 | __has(prop: string | symbol): boolean { 69 | return (typeof prop != "string" || prop.slice(0, 2) != "__") 70 | && (prop in this || prop == "name"); 71 | } 72 | 73 | /** If the property starts with '__' or is 'name', DO NOT delete. */ 74 | __delete(prop: string | symbol): void { 75 | if (prop.slice(0, 2) == "__" || prop == "name") return; 76 | delete this[prop]; 77 | } 78 | 79 | /** 80 | * This method will be called when the class is invoked as a function. You 81 | * may be a little confused since being told that ES6 class cannot be called 82 | * as function, AKA without `new` operator, well, when using this package, 83 | * you CAN. 84 | * 85 | * NOTE: prior to v1.2, __invoke without `static` modifier is permitted, but 86 | * it's now deprecated, always add `static` instead. 87 | */ 88 | static __invoke(...args: any[]): any { 89 | return "invoking Car as a function"; 90 | } 91 | } 92 | ``` 93 | 94 | ## How It Works? 95 | 96 | The decorator `applyMagic` is a function that returns a highly-customized ES5 97 | pseudo-class, it will replace the original class, so that when instantiating, 98 | the magic methods will be auto-applied to the instance wrapped by a `Proxy`. 99 | Since `applyMagic` is a function, so if you're coding in JavaScript without 100 | decorator support, you can manually call it to generate the wrapping class and 101 | assign to the old one. Like this: 102 | 103 | ```javascript 104 | import { applyMagic } from "js-magic"; 105 | 106 | class Car { 107 | // ... 108 | } 109 | 110 | Car = applyMagic(Car); 111 | ``` 112 | 113 | Since the returned class is wrapped in ES5 style, so that it allows you calling 114 | it as a function, where the `__invoke` method will called under the hood. 115 | 116 | ## Support of Inheritance 117 | 118 | This package also supports native inheritance, allows you inheriting the magical 119 | calling functionalities from a super class to sub-classes. Also you can rewrite 120 | the magic methods in the sub-class, and call the super's via `super` keyword. 121 | 122 | NOTE: this feature DOESN'T work with `__invoke`, unless using `applyMagic` on 123 | the sub-class as well. 124 | 125 | ## Support of Objects Other Than Class 126 | 127 | Since v1.1, this package also supports other objects other than class, if 128 | calling `applyMagic` on a non-function object, it will returns a proxy of the 129 | original object that supports magic functions. Moreover, if you want this 130 | feature be apply to a function, you can pass the second argument `proxyOnly` to 131 | `applyMagic`, and it will not treat the function as a potential class. 132 | 133 | ## Additional Symbols 134 | 135 | This package also provides symbols according to the magic method names (`__get`, 136 | `__set`, `__has`, `__delete`, `__invoke`), you can use them if you want to hide 137 | the methods from IDE IntelliSense, but generally they are not common used. 138 | 139 | ## Supported Environments 140 | 141 | Any environment that supports ES6 `Proxy` will work with this package perfectly, 142 | generally, NodeJS `6.0+`, Deno and modern browsers (`IE` aside) should support 143 | `Proxy` already. 144 | 145 | In browsers, if you're not using any module resolution, access the global 146 | variable `window.magic` instead. 147 | 148 | ## More Examples 149 | 150 | For more examples, please check out the [Test](./test.js). 151 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export const __get = Symbol("__get"); 2 | export const __set = Symbol("__set"); 3 | export const __has = Symbol("__has"); 4 | export const __delete = Symbol("__delete"); 5 | export const __invoke = Symbol("__invoke"); 6 | 7 | export interface MagicalClass { 8 | __get?(prop: string | symbol): any; 9 | __set?(prop: string | symbol, value: any): void; 10 | __has?(prop: string | symbol): boolean; 11 | __delete?(prop: string | symbol): void; 12 | /** @deprecated */ 13 | __invoke?(...args: any[]): any; 14 | } 15 | 16 | let warnedInvokeDeprecation = false; 17 | 18 | export function applyMagic any>(ctor: T, ctx?: object): T; 19 | export function applyMagic any>(fn: T, proxyOnly: boolean): T; 20 | export function applyMagic(obj: T): T; 21 | export function applyMagic(target: any, ctx: boolean | object = false) { 22 | if (typeof target === "function") { 23 | if (ctx === true) { 24 | return proxify(target); 25 | } 26 | 27 | const PseudoClass = function PseudoClass(this: any, ...args: any[]) { 28 | // Must use `new.target` instead of `this`, otherwise it won't work 29 | // in Bun (may be a bug). 30 | if (typeof new.target === "undefined") { // function call 31 | let invoke = target[__invoke] || target["__invoke"]; 32 | 33 | if (invoke) { // use static __invoke 34 | checkType(target, invoke, "__invoke"); 35 | 36 | return invoke 37 | ? invoke.apply(target, args) 38 | : target(...args); 39 | } 40 | 41 | let proto = target.prototype; 42 | invoke = proto[__invoke] || proto.__invoke; 43 | 44 | if (invoke && !warnedInvokeDeprecation) { 45 | warnedInvokeDeprecation = true; 46 | console.warn( 47 | "applyMagic: using __invoke without 'static' modifier is deprecated"); 48 | } 49 | 50 | checkType(target, invoke, "__invoke"); 51 | 52 | return invoke ? invoke(...args) : target(...args); 53 | } else { 54 | Object.assign(this, new (target)(...args)); 55 | return proxify(this); 56 | } 57 | }; 58 | 59 | Object.setPrototypeOf(PseudoClass, target); 60 | Object.setPrototypeOf(PseudoClass.prototype, target.prototype); 61 | 62 | setProp(PseudoClass, "name", target.name); 63 | setProp(PseudoClass, "length", target.length); 64 | setProp(PseudoClass, "toString", function toString(this: any) { 65 | let obj = this === PseudoClass ? target : this; 66 | return Function.prototype.toString.call(obj); 67 | }, true); 68 | 69 | return PseudoClass; 70 | } else if (typeof target === "object") { 71 | return proxify(target); 72 | } else { 73 | throw new TypeError("'target' must be a function or an object"); 74 | } 75 | } 76 | 77 | function checkType( 78 | ctor: Function, 79 | fn: Function, 80 | name: string, 81 | argLength: number | undefined = void 0 82 | ) { 83 | if (fn !== undefined) { 84 | if (typeof fn != "function") { 85 | throw new TypeError( 86 | `${ctor.name}.${name} must be a function` 87 | ); 88 | } else if (argLength !== undefined && fn.length !== argLength) { 89 | throw new SyntaxError( 90 | `${ctor.name}.${name} must have ` + 91 | `${argLength} parameter${argLength === 1 ? "" : "s"}` 92 | ); 93 | } 94 | } 95 | } 96 | 97 | function setProp(target: Function, prop: string, value: any, writable = false) { 98 | Object.defineProperty(target, prop, { 99 | configurable: true, 100 | enumerable: false, 101 | writable, 102 | value 103 | }); 104 | } 105 | 106 | function proxify(target: any) { 107 | let get = target[__get] || target.__get; 108 | let set = target[__set] || target.__set; 109 | let has = target[__has] || target.__has; 110 | let _delete = target[__delete] || target.__delete; 111 | 112 | checkType(new.target, get, "__get", 1); 113 | checkType(new.target, set, "__set", 2); 114 | checkType(new.target, has, "__has", 1); 115 | checkType(new.target, _delete, "__delete", 1); 116 | 117 | return new Proxy(target, { 118 | get: (target, prop) => { 119 | return get ? get.call(target, prop) : target[prop]; 120 | }, 121 | set: (target, prop, value) => { 122 | set ? set.call(target, prop, value) : (target[prop] = value); 123 | return true; 124 | }, 125 | has: (target, prop) => { 126 | return has ? has.call(target, prop) : (prop in target); 127 | }, 128 | deleteProperty: (target, prop) => { 129 | _delete ? _delete.call(target, prop) : (delete target[prop]); 130 | return true; 131 | } 132 | }); 133 | } 134 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-magic", 3 | "version": "1.4.2", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "js-magic", 9 | "version": "1.4.2", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@types/mocha": "^5.2.7", 13 | "@types/node": "^20.6.2", 14 | "mocha": "^5.2.0", 15 | "ts-node": "^10.9.1", 16 | "typescript": "^4.9.5" 17 | }, 18 | "engines": { 19 | "node": ">=6" 20 | } 21 | }, 22 | "node_modules/@cspotcode/source-map-support": { 23 | "version": "0.8.1", 24 | "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", 25 | "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", 26 | "dev": true, 27 | "dependencies": { 28 | "@jridgewell/trace-mapping": "0.3.9" 29 | }, 30 | "engines": { 31 | "node": ">=12" 32 | } 33 | }, 34 | "node_modules/@jridgewell/resolve-uri": { 35 | "version": "3.1.1", 36 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", 37 | "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", 38 | "dev": true, 39 | "engines": { 40 | "node": ">=6.0.0" 41 | } 42 | }, 43 | "node_modules/@jridgewell/sourcemap-codec": { 44 | "version": "1.4.15", 45 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", 46 | "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", 47 | "dev": true 48 | }, 49 | "node_modules/@jridgewell/trace-mapping": { 50 | "version": "0.3.9", 51 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", 52 | "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", 53 | "dev": true, 54 | "dependencies": { 55 | "@jridgewell/resolve-uri": "^3.0.3", 56 | "@jridgewell/sourcemap-codec": "^1.4.10" 57 | } 58 | }, 59 | "node_modules/@tsconfig/node10": { 60 | "version": "1.0.9", 61 | "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", 62 | "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", 63 | "dev": true 64 | }, 65 | "node_modules/@tsconfig/node12": { 66 | "version": "1.0.11", 67 | "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", 68 | "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", 69 | "dev": true 70 | }, 71 | "node_modules/@tsconfig/node14": { 72 | "version": "1.0.3", 73 | "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", 74 | "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", 75 | "dev": true 76 | }, 77 | "node_modules/@tsconfig/node16": { 78 | "version": "1.0.4", 79 | "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", 80 | "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", 81 | "dev": true 82 | }, 83 | "node_modules/@types/mocha": { 84 | "version": "5.2.7", 85 | "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", 86 | "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", 87 | "dev": true 88 | }, 89 | "node_modules/@types/node": { 90 | "version": "20.6.2", 91 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz", 92 | "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==", 93 | "dev": true 94 | }, 95 | "node_modules/acorn": { 96 | "version": "8.10.0", 97 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", 98 | "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", 99 | "dev": true, 100 | "bin": { 101 | "acorn": "bin/acorn" 102 | }, 103 | "engines": { 104 | "node": ">=0.4.0" 105 | } 106 | }, 107 | "node_modules/acorn-walk": { 108 | "version": "8.2.0", 109 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", 110 | "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", 111 | "dev": true, 112 | "engines": { 113 | "node": ">=0.4.0" 114 | } 115 | }, 116 | "node_modules/arg": { 117 | "version": "4.1.3", 118 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 119 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 120 | "dev": true 121 | }, 122 | "node_modules/balanced-match": { 123 | "version": "1.0.0", 124 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 125 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 126 | "dev": true 127 | }, 128 | "node_modules/brace-expansion": { 129 | "version": "1.1.11", 130 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 131 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 132 | "dev": true, 133 | "dependencies": { 134 | "balanced-match": "^1.0.0", 135 | "concat-map": "0.0.1" 136 | } 137 | }, 138 | "node_modules/browser-stdout": { 139 | "version": "1.3.1", 140 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 141 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 142 | "dev": true 143 | }, 144 | "node_modules/commander": { 145 | "version": "2.15.1", 146 | "resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz", 147 | "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", 148 | "dev": true 149 | }, 150 | "node_modules/concat-map": { 151 | "version": "0.0.1", 152 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 153 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 154 | "dev": true 155 | }, 156 | "node_modules/create-require": { 157 | "version": "1.1.1", 158 | "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", 159 | "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", 160 | "dev": true 161 | }, 162 | "node_modules/debug": { 163 | "version": "3.1.0", 164 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 165 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 166 | "dev": true, 167 | "dependencies": { 168 | "ms": "2.0.0" 169 | } 170 | }, 171 | "node_modules/diff": { 172 | "version": "3.5.0", 173 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 174 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 175 | "dev": true, 176 | "engines": { 177 | "node": ">=0.3.1" 178 | } 179 | }, 180 | "node_modules/escape-string-regexp": { 181 | "version": "1.0.5", 182 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 183 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 184 | "dev": true, 185 | "engines": { 186 | "node": ">=0.8.0" 187 | } 188 | }, 189 | "node_modules/fs.realpath": { 190 | "version": "1.0.0", 191 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 192 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 193 | "dev": true 194 | }, 195 | "node_modules/glob": { 196 | "version": "7.1.2", 197 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 198 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 199 | "dev": true, 200 | "dependencies": { 201 | "fs.realpath": "^1.0.0", 202 | "inflight": "^1.0.4", 203 | "inherits": "2", 204 | "minimatch": "^3.0.4", 205 | "once": "^1.3.0", 206 | "path-is-absolute": "^1.0.0" 207 | }, 208 | "engines": { 209 | "node": "*" 210 | } 211 | }, 212 | "node_modules/growl": { 213 | "version": "1.10.5", 214 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 215 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 216 | "dev": true, 217 | "engines": { 218 | "node": ">=4.x" 219 | } 220 | }, 221 | "node_modules/has-flag": { 222 | "version": "3.0.0", 223 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 224 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 225 | "dev": true, 226 | "engines": { 227 | "node": ">=4" 228 | } 229 | }, 230 | "node_modules/he": { 231 | "version": "1.1.1", 232 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", 233 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", 234 | "dev": true, 235 | "bin": { 236 | "he": "bin/he" 237 | } 238 | }, 239 | "node_modules/inflight": { 240 | "version": "1.0.6", 241 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 242 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 243 | "dev": true, 244 | "dependencies": { 245 | "once": "^1.3.0", 246 | "wrappy": "1" 247 | } 248 | }, 249 | "node_modules/inherits": { 250 | "version": "2.0.3", 251 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 252 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 253 | "dev": true 254 | }, 255 | "node_modules/make-error": { 256 | "version": "1.3.6", 257 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 258 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 259 | "dev": true 260 | }, 261 | "node_modules/minimatch": { 262 | "version": "3.0.4", 263 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 264 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 265 | "dev": true, 266 | "dependencies": { 267 | "brace-expansion": "^1.1.7" 268 | }, 269 | "engines": { 270 | "node": "*" 271 | } 272 | }, 273 | "node_modules/minimist": { 274 | "version": "0.0.8", 275 | "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 276 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 277 | "dev": true 278 | }, 279 | "node_modules/mkdirp": { 280 | "version": "0.5.1", 281 | "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 282 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 283 | "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", 284 | "dev": true, 285 | "dependencies": { 286 | "minimist": "0.0.8" 287 | }, 288 | "bin": { 289 | "mkdirp": "bin/cmd.js" 290 | } 291 | }, 292 | "node_modules/mocha": { 293 | "version": "5.2.0", 294 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", 295 | "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", 296 | "dev": true, 297 | "dependencies": { 298 | "browser-stdout": "1.3.1", 299 | "commander": "2.15.1", 300 | "debug": "3.1.0", 301 | "diff": "3.5.0", 302 | "escape-string-regexp": "1.0.5", 303 | "glob": "7.1.2", 304 | "growl": "1.10.5", 305 | "he": "1.1.1", 306 | "minimatch": "3.0.4", 307 | "mkdirp": "0.5.1", 308 | "supports-color": "5.4.0" 309 | }, 310 | "bin": { 311 | "_mocha": "bin/_mocha", 312 | "mocha": "bin/mocha" 313 | }, 314 | "engines": { 315 | "node": ">= 4.0.0" 316 | } 317 | }, 318 | "node_modules/ms": { 319 | "version": "2.0.0", 320 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 321 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 322 | "dev": true 323 | }, 324 | "node_modules/once": { 325 | "version": "1.4.0", 326 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 327 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 328 | "dev": true, 329 | "dependencies": { 330 | "wrappy": "1" 331 | } 332 | }, 333 | "node_modules/path-is-absolute": { 334 | "version": "1.0.1", 335 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 336 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 337 | "dev": true, 338 | "engines": { 339 | "node": ">=0.10.0" 340 | } 341 | }, 342 | "node_modules/supports-color": { 343 | "version": "5.4.0", 344 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", 345 | "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", 346 | "dev": true, 347 | "dependencies": { 348 | "has-flag": "^3.0.0" 349 | }, 350 | "engines": { 351 | "node": ">=4" 352 | } 353 | }, 354 | "node_modules/ts-node": { 355 | "version": "10.9.1", 356 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", 357 | "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", 358 | "dev": true, 359 | "dependencies": { 360 | "@cspotcode/source-map-support": "^0.8.0", 361 | "@tsconfig/node10": "^1.0.7", 362 | "@tsconfig/node12": "^1.0.7", 363 | "@tsconfig/node14": "^1.0.0", 364 | "@tsconfig/node16": "^1.0.2", 365 | "acorn": "^8.4.1", 366 | "acorn-walk": "^8.1.1", 367 | "arg": "^4.1.0", 368 | "create-require": "^1.1.0", 369 | "diff": "^4.0.1", 370 | "make-error": "^1.1.1", 371 | "v8-compile-cache-lib": "^3.0.1", 372 | "yn": "3.1.1" 373 | }, 374 | "bin": { 375 | "ts-node": "dist/bin.js", 376 | "ts-node-cwd": "dist/bin-cwd.js", 377 | "ts-node-esm": "dist/bin-esm.js", 378 | "ts-node-script": "dist/bin-script.js", 379 | "ts-node-transpile-only": "dist/bin-transpile.js", 380 | "ts-script": "dist/bin-script-deprecated.js" 381 | }, 382 | "peerDependencies": { 383 | "@swc/core": ">=1.2.50", 384 | "@swc/wasm": ">=1.2.50", 385 | "@types/node": "*", 386 | "typescript": ">=2.7" 387 | }, 388 | "peerDependenciesMeta": { 389 | "@swc/core": { 390 | "optional": true 391 | }, 392 | "@swc/wasm": { 393 | "optional": true 394 | } 395 | } 396 | }, 397 | "node_modules/ts-node/node_modules/diff": { 398 | "version": "4.0.2", 399 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 400 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 401 | "dev": true, 402 | "engines": { 403 | "node": ">=0.3.1" 404 | } 405 | }, 406 | "node_modules/typescript": { 407 | "version": "4.9.5", 408 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", 409 | "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", 410 | "dev": true, 411 | "bin": { 412 | "tsc": "bin/tsc", 413 | "tsserver": "bin/tsserver" 414 | }, 415 | "engines": { 416 | "node": ">=4.2.0" 417 | } 418 | }, 419 | "node_modules/v8-compile-cache-lib": { 420 | "version": "3.0.1", 421 | "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", 422 | "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", 423 | "dev": true 424 | }, 425 | "node_modules/wrappy": { 426 | "version": "1.0.2", 427 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 428 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 429 | "dev": true 430 | }, 431 | "node_modules/yn": { 432 | "version": "3.1.1", 433 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 434 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 435 | "dev": true, 436 | "engines": { 437 | "node": ">=6" 438 | } 439 | } 440 | } 441 | } 442 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-magic", 3 | "version": "1.4.2", 4 | "description": "JavaScript magic methods support.", 5 | "main": "cjs/index.js", 6 | "module": "esm/index.js", 7 | "types": "index.ts", 8 | "exports": { 9 | "require": "./cjs/index.js", 10 | "import": "./esm/index.js", 11 | "types": "./index.ts" 12 | }, 13 | "scripts": { 14 | "build:cjs": "tsc --module commonjs --target es2015 --outDir cjs --sourceMap --strict --noImplicitThis --noImplicitAny --noImplicitReturns index.ts", 15 | "build:esm": "tsc --module esnext --target es2015 --outDir esm --sourceMap --strict --noImplicitThis --noImplicitAny --noImplicitReturns index.ts", 16 | "build": "npm run build:cjs && npm run build:esm", 17 | "prepublishOnly": "npm run build", 18 | "test": "mocha -r ts-node/register test.ts", 19 | "test:bun": "bun run ./node_modules/mocha/bin/mocha test.ts" 20 | }, 21 | "engines": { 22 | "node": ">=6" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/ayonli/js-magic.git" 27 | }, 28 | "keywords": [ 29 | "Magic Methods", 30 | "getter", 31 | "setter" 32 | ], 33 | "author": "A-yon Lee ", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/ayonli/js-magic/issues" 37 | }, 38 | "homepage": "https://github.com/ayonli/js-magic#readme", 39 | "devDependencies": { 40 | "@types/mocha": "^5.2.7", 41 | "@types/node": "^20.6.2", 42 | "mocha": "^5.2.0", 43 | "ts-node": "^10.9.1", 44 | "typescript": "^4.9.5" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "mocha"; 2 | import * as assert from "assert"; 3 | import * as magic from "."; 4 | 5 | class Car { 6 | name?: string; 7 | wheels?: number; 8 | windows?: number; 9 | 10 | constructor(wheels: number = 0) { 11 | if (wheels) 12 | this.wheels = wheels; 13 | } 14 | 15 | __get(prop: string) { 16 | // @ts-ignore 17 | return (prop in this && this[prop] !== undefined) 18 | // @ts-ignore 19 | ? this[prop] 20 | // @ts-ignore 21 | : (prop == "name" ? this.constructor.name + " Instance" : null); 22 | } 23 | 24 | __set(prop: string, value: any) { 25 | // @ts-ignore 26 | this[prop] = prop == "name" ? value + " Instance" : value; 27 | } 28 | 29 | __has(prop: string) { 30 | return prop.slice(0, 2) != "__" && (prop in this || prop == "name"); 31 | } 32 | 33 | __delete(prop: string) { 34 | if (prop.slice(0, 2) == "__" || prop == "wheels") return; 35 | // @ts-ignore 36 | delete this[prop]; 37 | } 38 | 39 | __invoke() { 40 | return "invoking Car as a function"; 41 | } 42 | 43 | test(str: string) { 44 | return str; 45 | } 46 | } 47 | 48 | class Car2 { 49 | static __invoke() { 50 | return `invoking ${this.name} as a function`; 51 | } 52 | } 53 | 54 | describe("applying magic methods to class", () => { 55 | it("should generate a proxy class looks exactly like the original one", () => { 56 | let _Car = magic.applyMagic(Car); 57 | assert.strictEqual(_Car.name, Car.name); 58 | assert.strictEqual(_Car.length, Car.length); 59 | assert.strictEqual(_Car.toString(), Car.toString()); 60 | assert.strictEqual(_Car.prototype instanceof Car, true); 61 | var car = new _Car(4); 62 | assert.strictEqual(car instanceof Car, true); 63 | assert.strictEqual(car.wheels, 4); 64 | assert.strictEqual(car.test("Hello, World!"), "Hello, World!"); 65 | }); 66 | 67 | it("should apply __get method as expected", () => { 68 | let _Car = magic.applyMagic(Car); 69 | var car = new _Car; 70 | car.wheels = 4; 71 | assert.strictEqual(car.wheels, 4); 72 | assert.strictEqual(car.name, "Car Instance"); 73 | }); 74 | 75 | it("should apply __set method as expected", () => { 76 | let _Car = magic.applyMagic(Car); 77 | var car = new _Car; 78 | car.wheels = 4; 79 | car.name = "MyCar"; 80 | assert.strictEqual(car.wheels, 4); 81 | assert.strictEqual(car.name, "MyCar Instance"); 82 | }); 83 | 84 | it("should apply __has method as expected", () => { 85 | let _Car = magic.applyMagic(Car); 86 | var car = new _Car; 87 | car.wheels = 4; 88 | assert.strictEqual("wheels" in car, true); 89 | assert.strictEqual("name" in car, true); 90 | assert.strictEqual("__has" in car, false); 91 | }); 92 | 93 | it("should apply __delete method as expected", () => { 94 | let _Car = magic.applyMagic(Car); 95 | var car = new _Car; 96 | car.wheels = 4; 97 | delete car.wheels; 98 | assert.strictEqual("wheels" in car, true); 99 | assert.strictEqual(car.wheels, 4); 100 | }); 101 | 102 | it("should apply __invoke method as expected", () => { 103 | let _Car = magic.applyMagic(Car); 104 | // @ts-ignore 105 | assert.strictEqual(_Car(), "invoking Car as a function"); 106 | }); 107 | 108 | it("should apply static __invoke method as expected", () => { 109 | let _Car2 = magic.applyMagic(Car2); 110 | // @ts-ignore 111 | assert.strictEqual(_Car2(), "invoking Car2 as a function"); 112 | }); 113 | }); 114 | 115 | describe("class inheritance of magical class", () => { 116 | let _Car = magic.applyMagic(Car); 117 | 118 | it("should define an ES6 class extends the magical class as expected", () => { 119 | class Auto extends _Car { } 120 | 121 | let classStr = Auto.toString(); 122 | 123 | assert.strictEqual(Auto.name, "Auto"); 124 | assert.strictEqual(Auto.length, 0); 125 | assert.strictEqual(Auto.toString(), classStr); 126 | var auto = new Auto(4); 127 | assert.strictEqual(auto instanceof Car, true); 128 | assert.deepStrictEqual(auto.name, "Auto Instance"); 129 | assert.deepStrictEqual(auto.wheels, 4); 130 | assert.deepStrictEqual(auto.windows, null); 131 | 132 | auto.name = "MyAuto"; 133 | auto.windows = 4; 134 | assert.strictEqual(auto.name, "MyAuto Instance"); 135 | assert.strictEqual(auto.windows, 4); 136 | assert.strictEqual(auto.test("Hello, World!"), "Hello, World!"); 137 | }); 138 | 139 | it("should define an ES5 class extends the magical class as expected", () => { 140 | function Auto() { 141 | return Reflect.construct(_Car, arguments, new.target); 142 | } 143 | 144 | let classStr = Auto.toString(); 145 | 146 | Object.setPrototypeOf(Auto, _Car); 147 | Object.setPrototypeOf(Auto.prototype, _Car.prototype); 148 | 149 | assert.strictEqual(Auto.name, "Auto"); 150 | assert.strictEqual(Auto.length, 0); 151 | assert.strictEqual(Auto.toString(), classStr); 152 | // @ts-ignore 153 | var auto = new Auto(4); 154 | assert.strictEqual(auto instanceof Car, true); 155 | assert.deepStrictEqual(auto.name, "Auto Instance"); 156 | assert.deepStrictEqual(auto.wheels, 4); 157 | assert.strictEqual(auto.windows, null); 158 | auto.name = "MyAuto"; 159 | auto.windows = 4; 160 | assert.strictEqual(auto.name, "MyAuto Instance"); 161 | assert.strictEqual(auto.windows, 4); 162 | assert.strictEqual(auto.test("Hello, World!"), "Hello, World!"); 163 | }); 164 | }); 165 | 166 | describe("apply magic functions on objects other than class", () => { 167 | it("should apply magic functions on an object as expected", () => { 168 | let obj = magic.applyMagic({ 169 | __get(prop: string) { 170 | return "bar"; 171 | } 172 | }); 173 | 174 | // @ts-ignore 175 | assert.strictEqual(obj.foo, "bar"); 176 | }); 177 | 178 | it("should apply magic functions on a function as expected", () => { 179 | let fn = new Function(); 180 | 181 | // @ts-ignore 182 | fn["__get"] = (prop: string) => { 183 | // @ts-ignore 184 | return prop === "name" ? "fn" : prop in fn ? fn[prop] : void 0; 185 | }; 186 | // @ts-ignore 187 | fn = magic.applyMagic(fn, true); 188 | 189 | assert.strictEqual(fn.name, "fn"); 190 | }); 191 | }); 192 | --------------------------------------------------------------------------------